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 die
d 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.