Gennadiy Filatov

NAME

Salvation - Simple and free architectural solution for huge applications

DESCRIPTION

What is it?

Salvation is a little framework which by itself does almost nothing.

In example, it does not:

manage interactions with a web-server;
manage threads or forks of a daemon process;
hide database interactions.

Salvation is some kind of human supervisor. It forces developer to:

use OOP;
write modular applications splitting the code into many different packages and subroutines, and do it from the beginning;
use strategy pattern;
use MVC pattern when you need to render textual data;

Salvation also provides transparent mechanism to load strategies and substitute parts of algorithm. And it suppresses most of the exceptions from being died with, giving you control over those via hooks definable inside of Salvation::System.

When should it be used?

Salvation is not suitable for small projects like marketing promo-actions of type "give me some promo-code and I'll give you a discount or a free product", or blogs, or dumb web-crawlers, or alike.

Salvation is best used for building huge, complex systems: CRMs or other sales leads' management systems, in example.

How to use it?

The best learning is practice, so let's look at the example.

The task is to create an abstract user request management system web application. Let's give this system a name - URMS.

URMS will, among other needs, need to display some web page with the request's user data, request's data and management controls. All these things will be tied to some request object which is needed to be created somehow. So URMS will have four services:

RequestInfoWindow

To gather and form request's data.

UserInfoWindow

To gather and form request's user data.

RequestMgmtControls

To form request management controls.

RequestLoader

To load request.

Let's create a new project files and directory tree with Salvation::CLI.

 $ salvation.pl -d -S URMS -s RequestInfoWindow,UserInfoWindow,RequestMgmtControls,RequestLoader

Following files will be created:

 ./URMS.pm
 URMS/Services/RequestInfoWindow.pm
 URMS/Services/RequestInfoWindow/Defaults/M.pm
 URMS/Services/RequestInfoWindow/Defaults/V.pm
 URMS/Services/RequestInfoWindow/Defaults/C.pm
 URMS/Services/RequestInfoWindow/Defaults/OutputProcessor.pm
 URMS/Services/RequestInfoWindow/DataSet.pm
 URMS/Services/UserInfoWindow.pm
 URMS/Services/UserInfoWindow/Defaults/M.pm
 URMS/Services/UserInfoWindow/Defaults/V.pm
 URMS/Services/UserInfoWindow/Defaults/C.pm
 URMS/Services/UserInfoWindow/Defaults/OutputProcessor.pm
 URMS/Services/UserInfoWindow/DataSet.pm
 URMS/Services/RequestMgmtControls.pm
 URMS/Services/RequestMgmtControls/Defaults/M.pm
 URMS/Services/RequestMgmtControls/Defaults/V.pm
 URMS/Services/RequestMgmtControls/Defaults/C.pm
 URMS/Services/RequestMgmtControls/Defaults/OutputProcessor.pm
 URMS/Services/RequestMgmtControls/DataSet.pm
 URMS/Services/RequestLoader.pm
 URMS/Services/RequestLoader/Defaults/M.pm
 URMS/Services/RequestLoader/Defaults/V.pm
 URMS/Services/RequestLoader/Defaults/C.pm
 URMS/Services/RequestLoader/Defaults/OutputProcessor.pm
 URMS/Services/RequestLoader/DataSet.pm

Let's edit URMS package, which is the definition of our system, and a subclass of Salvation::System.

URMS needs to define services it has.

Let's add the following code:

 sub BUILD
 {
        my $self = shift;

        my $constraint = sub
        {
                return $self -> request_page_constraint();
        };

        $self -> Service( $_, { constraint => $constraint } )
                for
                        'RequestLoader', # order matters; mind to put your loaders first
                        'RequestInfoWindow',
                        'UserInfoWindow',
                        'RequestMgmtControls'
        ;

        return;
 }

 sub request_id
 {
        my $self = shift;

        return $self -> args() -> { 'request_id' };
 }

 sub request_page_constraint
 {
        my $self = shift;

        return defined $self -> request_id();
 }

This will tell the system that when it has request_id argument - it should load and run four services: RequestInfoWindow, UserInfoWindow, RequestMgmtControls, RequestLoader.

Let's then look at the URMS::Services::RequestLoader::DataSet package. Semantics tells us that this module should be responsible for loading the request object. The class it defines is a subclass of Salvation::Service::DataSet. We need to edit its main method so it will return some object. For the sake of example, let it be simple HashRef.

 sub main
 {
        my $self = shift;

        my $object = {
                id => 42,
                title => 'The Question',
                product => 100500, # magic number irrelevant to example
                serial_number => 'QWER-TYUI-OPAS', # magic string irrelevant to example
                type => 1, # magic number representing type of request
                comment => 'Why I even bought your product?'
        };

        return [
                ( $self -> service() -> system() -> request_id() == $object -> { 'id' } ? (
                        $object
                ) : () )
        ];
 }

The only purpose of RequestLoader service is to load request and make it accessible to other services. So let's edit URMS::Services::RequestLoader package, which is a service definition and a subclass of Salvation::Service, and add following code which will make our request object easily accessible:

 sub main
 {
        my $self = shift;

        $self -> system() -> storage() -> put( request => $self -> dataset() -> first() );

        return;
 }

The code above stores the first row returned by DataSet into system's shared storage (see Salvation::SharedStorage man page) for key request.

We can now delete following files as we won't need them:

 URMS/Services/RequestLoader/Defaults/M.pm
 URMS/Services/RequestLoader/Defaults/V.pm
 URMS/Services/RequestLoader/Defaults/C.pm
 URMS/Services/RequestLoader/Defaults/OutputProcessor.pm

We need to form request data for further displaying now.

Let's edit URMS::Services::RequestInfoWindow::DataSet package now. It should return request object to URMS::Services::RequestInfoWindow service so it will be able to process request object. So we will change URMS::Services::RequestInfoWindow::DataSet::main to something like this:

 sub main
 {
        my $self = shift;

        my $object = $self -> service() -> system() -> storage() -> get( 'request' );

        return [
                ( defined( $object ) ? $object : () )
        ];
 }

Now we should write a template so the service will know what data should be gathered. To do this, we will edit URMS::Services::RequestInfoWindow::Defaults::V package which is the definition of view and a subclass of Salvation::Service::View. We will modify its main to return template, as it is the fastest way:

 sub main
 {
        return [
                raw => [
                        'id',
                        'serial_number',
                        'title',
                        'comment'
                ],
                custom => [
                        'type'
                ]
        ];
 }

See Salvation::Service::View man page for more information about templates.

The next step is to write a model so the service will know how to process each specified column. Let's edit URMS::Services::RequestInfoWindow::Defaults::M package. It is a model definition and a subclass of Salvation::Service::Model. We will add following code so the model will be able to process columns of type raw:

 sub __raw
 {
        my ( $self, $object, $column ) = @_;

        return $object -> { $column };
 }

We will also add following code to be able to process column with name type of type custom:

 sub custom_type
 {
        my ( $self, $object ) = @_;

        my %table = (
                1 => 'regular',
                2 => 'specific'
        );

        return $table{ $object -> { 'type' } };
 }

Okay! So now is the time to check the thing out. We will do it using this simple script named test.pl:

 #!/usr/bin/perl

 use strict;

 package test;

 use URMS ();

 print URMS
        -> new(
                args => {
                        request_id => 42
                }
        )
        -> start()
 , "\n";

 exit 0;

Running this script, you will see output like this:

 <?xml version="1.0" encoding="UTF-8"?>
 <output>
    <data>
        <stack></stack>
    </data>
    <data>
        <stack>
            <list name="raw">
                <frame title="[FIELD_ID]" name="id" type="raw"><![CDATA[42]]></frame>
                <frame title="[FIELD_SERIAL_NUMBER]" name="serial_number" type="raw"><![CDATA[QWER-TYUI-OPAS]]></frame>
                <frame title="[FIELD_TITLE]" name="title" type="raw"><![CDATA[The Question]]></frame>
                <frame title="[FIELD_COMMENT]" name="comment" type="raw"><![CDATA[Why I even bought your product?]]></frame>
            </list>
            <list name="custom">
                <frame title="[FIELD_TYPE]" name="type" type="custom"><![CDATA[regular]]></frame>
            </list>
        </stack>
    </data>
    <data>
        <stack></stack>
    </data>
    <data>
        <stack></stack>
    </data>
 </output>

And then, just to demonstrate how the hooks (implementation of strategy pattern) work, we will change how the request's serial_number column should be rendered. Let's imagine that the requests of type 1 will be processed only by employees of your company, but requests of type 2 will be processed by you partners and because of some major issues you can't show a serial_number to them. If it sounds like "oh, I should go and write some if...else clause" - it sounds like a crutch. To avoid crutches, we will use strategy pattern. We will create a hook for type type and value 2 via Salvation::CLI:

 salvation.pl -d -S URMS -s RequestInfoWindow -h Type -v 2

This will create following files:

 URMS/Services/RequestInfoWindow/Hooks/Type/2.pm
 URMS/Services/RequestInfoWindow/Hooks/Type/2/Defaults/M.pm
 URMS/Services/RequestInfoWindow/Hooks/Type/2/Defaults/V.pm
 URMS/Services/RequestInfoWindow/Hooks/Type/2/Defaults/C.pm
 URMS/Services/RequestInfoWindow/Hooks/Type/2/Defaults/OutputProcessor.pm

Let's remove the ones we won't use in this example, leaving only model and hook definition:

 URMS/Services/RequestInfoWindow/Hooks/Type/2/Defaults/V.pm
 URMS/Services/RequestInfoWindow/Hooks/Type/2/Defaults/C.pm
 URMS/Services/RequestInfoWindow/Hooks/Type/2/Defaults/OutputProcessor.pm

Then let's edit hook's model, URMS::Services::RequestInfoWindow::Hooks::Type::2::Defaults::M, adding following code:

 sub raw_serial_number
 {
        my ( $self, $object ) = @_;

        my $serial_number = $object -> { 'serial_number' };

        $serial_number =~ s/^(..).+?(..)$/${1}XX-XXXX-XX${2}/;

        return $serial_number;
 }

Also we need to change class's ancestor of URMS::Services::RequestInfoWindow::Hooks::Type::2::Defaults::M from Salvation::Service::Model to URMS::Services::RequestInfoWindow::Defaults::M:

 extends 'URMS::Services::RequestInfoWindow::Defaults::M';

Then let's change type of object returned by URMS::Services::RequestLoader::DataSet from 1 to 2, so it will return following object:

 my $object = {
        id => 42,
        title => 'The Question',
        product => 100500,
        serial_number => 'QWER-TYUI-OPAS',
        type => 2, # Here is the change
        comment => 'Why I even bought your product?'
 };

Then we should register a hook inside of service, editing URMS::Services::RequestLoader, adding following code:

 sub BUILD
 {
        my $self = shift;

        $self -> Hook( [ $self -> dataset() -> first() -> { 'type' }, 'Type' ] );

        return;
 }

And then let's run test.pl again, so it will produce an output kind of like the following:

 <?xml version="1.0" encoding="UTF-8"?>
 <output>
    <data>
        <stack>
            <list name="raw">
                <frame title="[FIELD_ID]" name="id" type="raw"><![CDATA[42]]></frame>
                <frame title="[FIELD_SERIAL_NUMBER]" name="serial_number" type="raw"><![CDATA[QWXX-XXXX-XXAS]]></frame>
                <frame title="[FIELD_TITLE]" name="title" type="raw"><![CDATA[The Question]]></frame>
                <frame title="[FIELD_COMMENT]" name="comment" type="raw"><![CDATA[Why I even bought your product?]]></frame>
            </list>
            <list name="custom">
                <frame title="[FIELD_TYPE]" name="type" type="custom"><![CDATA[specific]]></frame>
            </list>
        </stack>
    </data>
    <data>
        <stack></stack>
    </data>
    <data>
        <stack></stack>
    </data>
 </output>

As you can see, hook has changed the way serial_number column is rendered. So hooks can be used to change behaviour of your service and its components like view, model, contoller and OutputProcessor.

You can continue playing with example files to test things as you like, simultaneously reading the docs for appropriate base classes and other modules.