The Phrasebook Design Pattern
by Rani Pinchuk
|
Pages: 1, 2
The next thing we do is to load a dictionary from the phrasebook file. The $language variable above will hold the dictionary name - in our example, it will be "EN" or "NL".
Finally, when we need to get the text for an error message, we call the get method of the Class::Phrasebook object with the key of the message we want to get, and a reference to a hash that holds the name-value pairs of the placeholders within the phrase.
Some careful readers might point out that if we use the Class::Phrasebook inside another class, then we might end up with a memory problem. Suppose many of the other class objects are constructed, and all of them load the same dictionary using the Class::Phrasebook objects. We will end up with many identical dictionaries loaded into the memory.
For example, assume we have object User that uses an object of Class::Phrasebook to generate error messages. When we construct the User object, we also construct a Class::Phrasebook object and load the dictionary for the error messages in the right language. Let's assume we have 100 User objects that are supposed to provide the error messages in English. It means that those 100 User objects will hold 100 Class::Phrasebook objects and each of them will hold its own copy of the English error dictionary. Terrible loss of memory!
Well, not exactly. The Class::Phrasebook keeps the loaded dictionaries in a cache that is actually class data. That means that all the objects of Class::Phrasebook have one cache like that. The cache is managed by the class, and knows to load only one dictionary of each sort. It will know also to delete that dictionary from the memory when there are no more objects that refer to it. Therefore, the careful readers should not worry about this issue any more.
However, continuing the example above, sometimes we might load the 100 User objects one after the other. Each time, that object will be destroyed. Other careful reader might point out that in this case the dictionary will go out of scope every time, and actually we will load the same dictionary 100 times!
Because of that, the class provides for that careful reader a class method that configures the class to keep the loaded dictionaries in the cache forever. This is not the default behavior, but when coding daemons, for example, it will be desirable.
Class::Phrasebook::SQL
The class Class::Phrasebook::SQL is a daughter class of Class::Phrasebook. It provides us with some extra features that helps us to deal with SQL phrases more easily. One example is the way it deals with the update SQL statement. Imagine that you have in your application a form that the user is supposed to fill in. If the user fills in only two fields, then you are supposed to update only those two fields in his record. Usually this problem is solved by having a simple code that builds our update SQL statement from its possible parts. However, this will not be that readable. The Class::Phrasebook::SQL will let you do it in the following way:
We will place as a phrase the update SQL statement with all the possible fields:
<phrase name="UPDATE_T_ACCOUNT">
update t_account set
login = '$login',
description = '$description',
dates_id = $dates_id,
groups = $groups,
owners = $owners
where id = $id
</phrase>
The Class::Phrasebook::SQL will drop the "set" lines, where there is a placeholder that its value is undefined. As I wanted to avoid from real parsing of the update statements, the update statements must look as the example above - each "set" expression in different line. Now, if we call the get method as the following:
$statement = $sql->get("UPDATE_T_ACCOUNT",
{ description => "administrator of manuals",
id => 77 });
We will get the statement:
update t_account set
description = 'administrator of manuals'
where id = 77
"Set" lines that contained undefined placeholders in the original update statement were removed.
Debugging With the Phrasebook Classes
Both the classesClass::Phrasebook and
Class::Phrasebook::SQL provide
us with debugging services. One example is the environment variable
PHRASEBOOK_SQL_DEBUG_PRINTS. If this variable holds the value "TEXT"
then it will print debug message each time the method C<get> is called:
path = Oefeningen/logoklad.source
[DBOrderedTreeUI.pm:322-->DBOrderedTreeUI::show_list > DBOrderedTreeUI.
pm:4134-->Manual::fill_node_info_container_from_list > Manual.pm:2885--
>Document::load > /htdocs/html/projects/webiso/code/classes/Document.pm
:403-->Revisions::load > /htdocs/html/projects/webiso/code/classes/Revi
sions.pm:114-->Revision::load: ][GET_LAST_REVISION]
select path, major, minor, date, user_id,
state_id, md5, data_md5, is_patch, is_changed
from revision
where path = 'Oefeningen/logoklad.source'
and is_patch = 0
If the value of the environment is "COLOR", then the output would be with colors. The colors come from the Term::ANSIColor module. If the value is "HTML" then the output would be HTML code that generates a similar colorful representation.
path = Oefeningen/logoklad.source
[DBOrderedTreeUI.pm:322-->DBOrderedTreeUI::show_list > DBOrderedTreeUI
.pm:4134-->Manual::fill_node_info_container_from_list > Manual.pm:2885
-->Document::load > /htdocs/html/projects/webiso/code/classes/Document
.pm:403-->Revisions::load > /htdocs/html/projects/webiso/code/classes/
Revisions.pm:114-->Revision::load: ][GET_LAST_REVISION]
select path, major, minor, date, user_id,
state_id, md5, data_md5, is_patch, is_changed
from revision
where path = 'Oefeningen/logoklad.source'
and is_patch = 0
Imagine that you need to see what are the SQL statements that are generated from a certain piece of code. Setting the PHRASEBOOK_SQL_DEBUG_PRINTS environment variable within that code will do the trick in no time. This feature can help not only in debugging, but also in the optimization of SQL code.
A similar environment variable is the PHRASEBOOK_SQL_SAVE_STATEMENTS_FILE_PATH. When this environment is set to a certain file path, all the SQL statements that are generated by calling to the get method will be written to that file. That way, you can see later which SQL statements your application issued, and even to re-run them from that file.
Actually, this is the way I found a bug in one of the former versions of the database PostgreSQL. I noticed that under certain conditions, some select statements fail while they are not supposed to fail. As usual, the "certain conditions" were totally unknown. What I did was to run my application while having the environment PHRASEBOOK_SQL_SAVE_STATEMENTS_FILE_PATH set. When the bug happened, I took the file with all the SQL statements that I got and ran the SQL statements directly from it. Then, I started to clean it until I had only few SQL statements left, and still the bug happened when they were run. I sent those statements with my report of the bug and got within two hours a solution for the problem (Well - you know, when you use open source software, you get support - and the PostgreSQL team is very responsive).
Log::LogLite and Log::NullLogLite
The Log::LogLite and the Log::NullLogLite classes provide us with an excellent opportunity to introduce a beautiful design pattern called the Null Object Design Pattern.
The Log::LogLite is a simple class that let us create simple log files in our application. The synopsis from the manual page of the class gives a good overview of how to use the class:
use Log::LogLite;
my $LOG_DIRECTORY = "/where/ever/our/log/file/should/be";
my $ERROR_LOG_LEVEL = 6;
# create new Log::LogLite object
my $log = new Log::LogLite($LOG_DIRECTORY."/error.log",
$ERROR_LOG_LEVEL);
...
# we had an error
$log->write("Could not open the file ".$file_name.": $!", 4);
As we saw Class::Phrasebook demands the use of a Log::LogLite object. This allows Class::Phrasebook to generate log messages when errors occur - for example, when the parsing of the XML file fails. However, it might be that we do not want to have a log file for every use of the Class::Phrasebook. How can we avoid from having a log file without changing the code of Class::Phrasebook?
The solution for that problem comes from the beautiful Null Object Design Pattern by Bobby Woolf. The pattern guides us to inherit from our class a null class - a class that does nothing, but implement the same interface of the original class. In our example, Log::NullLogLite overrides some of the methods of Log::LogLite to do nothing. So when we call the method write, nothing is written. Because the class inherits from Log::LogLite, the classes that use Log::LogLite continue to run correctly also when we send to them Log::NullLogLite objects.
Conclusion
The phrasebook design pattern helps us to get more readable and maintainable code by separating expressions in one language from the main code that is written in other language. The Class::Phrasebook module helps us to implement this pattern in Perl.
The classes that are presented above have been used by my colleagues and me for few years. During that time, we could see in practice all the advantages that the pattern promises. For example, an application of 65,000 lines had to be ported to another database. When the SQL code is concentrated in XML files, we could achieve that kind of port very rapidly.
In my opinion, Perl gives us a nice platform to program in object-oriented techniques. However, I am not sure that this opinion is well-spread. Many programmers stop learning when things they write start running, and with Perl things start to run very soon. Nevertheless, with good design, big and sophisticated applications can be written with Perl, like with other OO languages.
Thanks
Many thanks to Ockham Technology N.V. for letting me release the above modules and others as open source on CPAN.

