|
Unleash the power of object orientation with a better
persistence strategy
Carlos
Eduardo Villela (mailto:carlos@stage2.com.br?cc=&subject=An
introduction to object prevalence) Developer and owner, Stage2
Sistemas Web 1 August 2002
Persisting state and data has always been a problem with
object-oriented software. Over the years, developers have stored object
data in many ways, including relational databases, flat files,and XML.
None of these approaches really managed to keep the software purely
object-oriented. The Prevayler team is changing this with the object
prevalence concept. This article introduces object
prevalence.
Note: You will find it helpful to be familiar with the
concept of a Command Object. The subject is very well covered in the
literature on Design Patterns.
Why persistence as you know it is
bad Today, data persistence for object-oriented systems is
an incredibly cumbersome task to deal with when building many kinds of
applications. The developer must map objects to database tables, XML files
or use some other non-OO way to represent data, destroying encapsulation
completely. One solution to this problem is object prevalence.
The Prevalent
way Object prevalence is a concept that was developed by
Klaus Wuestefeld and some colleagues at Objective Solutions. Its first
implementation, known as Prevayler, became available in November 2001 as
an open-source project. (See Resources.)
Today, Prevayler is at version 1.3.0 and has about 350 lines of code. You
may think that the code is too small to do anything useful but, based upon
my experience on a recent project, I can confirm that Prevayler is several
orders of magnitude faster than one of the leading open source relational
databases. It is all about simplicity.
Object prevalence is inherently and conceptually simple and can be
implemented in any object-oriented programming language that can serialize
an object -- a feature in many modern OO languages.
In a prevalent system, everything is kept in RAM, as though you were
just using a programming language. Serializable command objects manage all
the necessary changes required for persistence. Queries are run against
pure Java language objects, giving developers all the flexibility of the
Collections API and other APIs, such as the Jakarta Commons Collections
and Jutil.org. (See Resources.)
Figure 1. How a prevalent system works
Before changes are applied to business objects, each command is
serialized and written to a log file (Figure
1). Then, each command is executed immediately. Optionally, in a
low-use period, the system can take a snapshot of the business objects,
aggregating all the commands applied into one large file to save some
reloading time.
During system recovery after a shutdown or system crash, the system
retrieves its last saved state from the snapshot file (if one is
available) and then reads the commands from the log files created since
the snapshot was taken. These commands are applied to the business objects
exactly as if they had just come from the system's clients, including the
system clock. Thus, the system returns to the state it was in just before
the shutdown and is ready to run.
For a prevalent system to work correctly, its business objects must
follow two very simple rules. The business objects must be:
- Serializable - At any point in time, the system might want to
persist an object to disk or other non-volatile media.
- Deterministic - Given some input, the business object's methods must
always return the same output.
This is particularly important when a business object deals with the
system clock. Object orientation gurus often say that the system clock is
an external actor to the system. Prevayler helps you think this way by
providing an adapter class, called AlarmClock , that keeps
track of the clock ticks for the application writer.
Building your first prevalent
system Now to build your first prevalent system -- download
Prevayler from the Prevayler Web site.
Make sure that you install it correctly by putting it in your
CLASSPATH.
Starting up The
Prevayler system is a very simple command-line based login manager. You
need to track the user login ID, password, name, and e-mail address. Start
by defining the User class (see Listing
1):
package org.prevayler.intro.users;
import java.io.Serializable;
/** Represents a system's user.
* @author Carlos Villela
*/
public class User implements Serializable {
private String name = "";
private String email = "";
private String login = "";
private String password = "";
// getters and setters go here
// ...
}
|
This very straightforward property-only class is the only business
object in this simple demonstration of what object prevalence and
Prevayler can do. In real-world applications, you would define many more
business objects, as your design evolves.
Now, link a HashMap of those Users into the
PrevalentSystem as shown in Listing
2:
package org.prevayler.intro.users;
import java.util.HashMap;
import org.prevayler.implementation.AbstractPrevalentSystem;
/** Prevalent system for the Users Manager Demo.
* @author Carlos Villela
*/
public class UserLogonSystem extends AbstractPrevalentSystem {
/**
* Map to hold all user references on the system.
*/
private HashMap users;
public UserLogonSystem() {
this.users = new HashMap();
}
public HashMap getUsers() {
return users;
}
public void setUsers(HashMap users) {
this.users = users;
}
}
|
You have now defined your system using the
AbstractPrevalentSystem class. You could have implemented the
PrevalentSystem interface directly if you needed to use the
system clock or if your system needed to extend some other class.
Creating
commands Before you can do anything with this system,you
need to modify and query its data. As shown in Listing
3, start with the AddUser command:
package org.prevayler.intro.users.commands;
import java.io.Serializable;
import java.util.HashMap;
import org.prevayler.Command;
import org.prevayler.PrevalentSystem;
import org.prevayler.intro.users.User;
import org.prevayler.intro.users.UserManager;
/** Adds a user to a UserManager
* @author Carlos Villela
*/
public final class AddUser implements Command {
private User user;
/**
* @see Command#execute(PrevalentSystem)
*/
public Serializable execute(PrevalentSystem system) throws
InvalidUserException {
if (user == null)
throw new InvalidUserException("null user");
if (user.getLogin() == null)
throw new InvalidUserException("null login");
if (user.getPassword() == null)
throw new InvalidUserException("null password");
if (user.getLogin().length() < 1)
throw new InvalidUserException("invalid login size");
if (user.getPassword().length() < 6)
throw new InvalidUserException("invalid password size");
UserManager users = (UserManager) system;
HashMap usersMap = users.getUsers();
assert usersMap != null;
if (usersMap.containsKey(user.getLogin()))
throw new InvalidUserException("login already exists");
usersMap.put(user.getLogin(), user);
return this;
}
// getters and setters for user go here
}
|
Take a closer look at this code. AddUser Command is an
equivalent to an SQL INSERT -- using the
HashMap.put method is the same as inserting data into an
indexed relational table. You can create more complex commands and that is
what comes next.
Listing
4 shows the source for a more complex command,
ChangeUser :
package org.prevayler.intro.users.commands;
import java.io.Serializable;
import java.util.HashMap;
import org.prevayler.Command;
import org.prevayler.PrevalentSystem;
import org.prevayler.intro.users.User;
import org.prevayler.intro.users.UserManager;
public final class ChangeUser implements Command {
private User user;
public Serializable execute(PrevalentSystem system) throws
InvalidUserException {
if (user == null)
throw new InvalidUserException("null user");
if (user.getLogin() == null)
throw new InvalidUserException("null login");
if (user.getPassword() == null)
throw new InvalidUserException("null password");
if (user.getLogin().length() < 1)
throw new InvalidUserException("invalid login size");
if (user.getPassword().length() < 6)
throw new InvalidUserException("invalid password size");
UserManager users = (UserManager) system;
HashMap usersMap = users.getUsers();
if (!usersMap.containsKey(user.getLogin()))
throw new InvalidUserException("non-existent login");
usersMap.remove(user.getLogin());
usersMap.put(user.getLogin(), user);
return this;
}
// getters and setters for user go here
}
|
The ChangeUser Command introduces subtle changes: when I
want to change a User , I first check for its existence in the
system. This command is, in practice, a SELECT and an
UPDATE . Because object-oriented programmers do not want to
spend time thinking about rolling back a transaction, I check every
possible thing that can go wrong. I can do this because all the data are
available and all validation is performed before actually changing
anything. The transaction is now a method -- its execution can run to
completion or throw an Exception .
You can write rollback-free programming in other ways but I do not
cover more examples on this occasion.
These two commands can potentially make you think that you are using
SQL. You are not limited, by any means, to do just that. Commands can be
much, much smarter. You can think of them as agents in your system. They
can start doing work even before you execute() them as they
validate data and state, give warnings to your application, and much more.
Your data manipulation is freed from the usual SQL and other query
language limitations.
Putting it all
together Next, I create the Main class, which
provides the command-line interface. I can create any kind of user
interaction I want, including Applets, Swing GUIs, Servlets or anything
else that can be used to create new commands and apply them on the
system.
Listing
5 shows the code that creates the command-line interface:
/**
* Main class for the Users Manager Demo.
*/
public class Main {
public static void usage() {
System.out.println(
"Usage: Main <list|add|change|del> login <parameters>\n\n"
+ "Parameters:\n"
+ " list: none\n"
+ " add: <login> <password> <name> <email>\n"
+ " change: <login> <password> <name> <email>\n"
+ " del: <login>\n");
System.exit(0);
}
public static void main(String[] args) {
try {
SnapshotPrevayler prevayler =
new SnapshotPrevayler(new UserManager());
if (args.length < 1) {
usage();
} else if (args[0].equalsIgnoreCase("list")) {
listUsers(prevayler);
} else if (args[0].equalsIgnoreCase("add") & args.length >= 5) {
addUser(prevayler, args[1], args[2], args[3], args[4]);
} else if (args[0].equalsIgnoreCase("del") & args.length >= 2) {
deleteUser(prevayler, args[1]);
} else if (args[0].equalsIgnoreCase("change")) {
changeUser(prevayler, args[1], args[2], args[3], args[4]);
} else {
usage();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// client methods go here...
}
|
Listing
6 and Listing
7 show the addUser and changeUser methods,
respectively. For brevity, I have omitted the listings for
deleteUser and listUsers , although they are
included in the demo code (see Resources).
You will notice how they are very simple client methods.
/** Adds a new user to the system.
* @param prevayler the where we will execute the command
* @param login user's login name
* @param password user's password
* @param name user's name
* @param email user's e-mail address
*/
private static void addUser(Prevayler prevayler, String login,
String password, String name, String email)
throws Exception {
System.out.println("Adding user '" + login + "'");
User u = new User();
u.setLogin(login);
u.setPassword(password);
u.setName(name);
u.setEmail(email);
AddUser cmd = new AddUser();
cmd.setUser(u);
try {
prevayler.executeCommand(cmd);
} catch (InvalidUserException e) {
System.out.println("Error: " + e.getMessage());
}
}
|
/** Changes the user's data.
* @param prevayler the where we will execute the command
* @param login user's login name to change data
* @param password user's new password
* @param name user's new name
* @param email user's new e-mail address
*/
private static void changeUser(Prevayler prevayler, String login,
String password, String name, String address)
throws Exception {
System.out.println("Changing user '" + login + "'");
User u = (User)
((UserManager) prevayler.system()).getUsers().get(login);
u.setPassword(password);
u.setName(name);
u.setEmail(address);
ChangeUser cmd = new ChangeUser();
cmd.setUser(u);
prevayler.executeCommand(cmd);
}
|
Prevalent systems and the
Web With Prevayler, you can develop many kinds of
applications. Its simplicity and ease of use makes it ideal for quickly
prototyping Web applications. You can then put these applications into
production with very little effort. As they are simple Java language
classes and beans, you can use your favorite IDE to create the beans and
their relationships very quickly. One of the Web applications that I
developed with Prevayler was prototyped very quickly. It performed so well
that the customer decided to put the system into immediate production
without even considering use of a database system. The deployment of the
application was also very simple as there were no need to set up database
servers. I did change the data backup policies a little and put the
PrevalenceBase directory (where Prevayler stores the serialized Commands)
under backup control. Figure
2 shows how a Web application can benefit from object prevalence.
Figure 2. Building a prevalent Web
application
The architecture in Figure
2 has one problem. With only one set of Business Objects is in each
Web Container, the architecture does not support clustering or
replication. One possible solution to this problem, as Prevayler is
implemented today, is to add a shared command-log storage (like an NFS
share or Windows shared drive). Then, all Web containers can read the same
command log and snapshots and elect one of the machines as the hot
system. With a shared command log, the system can have a replica of the
business objects on another virtual machine. The replica can also read all
commands applied to the hot system and apply them in the exact same
order. At backup time, the replica stops reading the commands and its
snapshot is safely taken. Then, the replica resumes reading the command
queue and gets back into synchronization with the hot system. The
replication approach just described was not needed on the customer
prototype application that I mentioned earlier, as the backup time was
synchronized with the snapshot time of the prevalent system. This way,
only one file was backed up, instead of the full command log. Figure
3 graphically illustrates replication.
Figure 3. Replication inside a prevalent Web
application
The Prevayler team is working hard to create a better replication
system, for those who really need it. After working on a number of
applications, I was most surprised to find that the most common bottleneck
in a Prevalent system is not the data access and manipulation logic, but
rather the Web server.
Conclusion Object
prevalence is a very useful concept, which can be used for many kinds of
systems. Its simplicity is one of its major strengths. But, simplicity
sometimes frightens people. People often ask me questions:
- Is it robust?
- Is it scalable?
- What is performance like?
The best way to answer these questions is to run the
ScalabilityTest in the
org.prevayler.test.scalability package and see for yourself.
With this test, you can see the power of the object prevalence
concept.
Remember, Prevayler is open source and object prevalence is a concept,
not a tool. You can implement it in other languages (as others have
already done with SmallTalk and Ruby). If you do, please drop the
Prevayler team a note. We will be glad to support you!
Resources
About the
author Carlos Eduardo Villela is a 19-year old
Brazilian graduate in Information Systems. Programming since he was
very young, almost 8 years experience has made him a Java and Python
enthusiast. He currently runs his own consulting company, Stage2 Sistemas Web. He is the
maintainer and editor of the Prevayler Web site and has successfully
used Prevayler in a number of projects. You can reach him at carlos@stage2.com.br.
|
|
|