NAME
SPOPS::Manual::ObjectRules - Use rules to give your object custom behavior
DESCRIPTION
This document aims to answer the questions:
What is a rule?
What can a rule do?
What is a ruleset?
How do I implement a rule?
RULES AND RULESETS DEFINED
When a SPOPS object calls save()
, fetch()
, or remove()
, the implementing class (e.g., SPOPS::DBI) takes care of most of the details for retrieving and constructing the object. However, sometimes you want to do something more complex or different. Each data manipulation method allows you to define two methods to accomplish these things. One is called before the action is taken (usually at the very beginning of the action) and the other after the action has been successfully completed.
What kind of actions might you want to accomplish? Cascading deletes (when you delete one object, delete a number of dependent objects as well); dependent fetches (when you fetch one object, fetch all its component objects as well); implement a consistent data layer (such as full-text searching) by sending all inserts and updates to a separate module or daemon; data validation (by submitting the data in the object to a rules engine). Whatever -- it's up to you.
Each of these actions is a rule, and together they are rulesets.
Rule Guidelines
There are some fairly simple guidelines to rules:
Each rule is independent of every other rule. Why? Rules for a particular action may be executed in an arbitrary order. You cannot guarantee that the rule from one class will execute before the rule from a separate class.
A rule should not change the data of the object on which it operates. Each rule should be operating on the same data. And since guideline 1 states the rules can be executed in any order, changing data for use in a separate rule would create a dependency between them.
NOTE: This item is up for debate
If a rule fails, then the action is aborted. This is central to how the ruleset operates, since it allows inherited behaviors to have a say on whether a particular object is fetched, saved or removed.
NOTE: This will probably be dropped in favor of a more flexible scheme that allows non-essential rules to fail without forcing the entire action to fail.
Rule Uses
Rules enable you to implement a 'layer' over certain classes of data. Perhaps you want to collect how many times users from various groups visit a set of objects on your website. You can create a fairly simple class that puts a rule into the ruleset of its children that creates a log entry every time a particular object is fetch()ed. The class could also contain methods for dealing with this information.
This rule is entirely separate and independent from other rules, and does not interfere with the normal operation except to add information to a separate area of the database as the actions are happening. In this manner, you can think of them as a trigger as implemented in a relational database. However, triggers can (and often do) modify the data of the row that is being manipulated, whereas a rule should not.
Rules and Aspects
Another useful way to think of rules is in terms of aspect oriented programming (AOP). AOP works in conjunction with other methods of programming (object-oriented, procedural, functional) and allows you to create joinpoints at which you perform actions with and on different types of data.
Read up more about AOP in the Aspect module, particularly Aspect::Intro.
RULESET HOOKS
pre_fetch_action({ id => $ })
Called before a fetch is done, although if an object is retrieved from the cache this action is skipped. (NOTE: THIS MIGHT NOT BE TRUE -- WE NEED TO IMPLEMENT CACHING AND SEE HOW THIS WORKS IN REALITY.) The only argument is the ID of the object you are trying to fetch.
This hook is generally not used very often.
post_fetch_action( \% )
Called after a fetch has been successfully completed, including after a positive cache hit.
pre_save_action({ is_add => bool })
Called before a save has been attempted. If this is an add operation (versus an update), we pass in a true value for the 'is_add' parameter.
post_save_action({ is_add => bool })
Called after a save has been successfully completed. If this object was just added to the data store, we pass in a true value for the 'is_add' parameter.
pre_remove_action( \% )
Called before a remove has been attempted.
post_remove_action( \% )
Called after a remove has been successfully completed.
RULE IMPLEMENTATION
Adding rules to an object class is very simple. You have one simple method to create to add your rule(s) to the ruleset for an object, and then the actual rules.
Rule Factory
ruleset_factory( $class, \%class_ruleset )
Interface for adding rules to a class. The first argument is the class to which we want to add the ruleset, the second is the ruleset for the class. The ruleset is simply a hash reference with keys as the methods named above ('pre_fetch_action', etc.) pointing to an arrayref of code references.
This means that every phase named above above ('pre_fetch_action', etc.) can run more than one rule. Here is an example of what such a method might look like -- this one is taken from a class that implements full-text indexing. When the object is saved successfully, we want to submit the object contents to our indexing routine. When the object has been removed successfully, we want to remove the object from our index:
sub ruleset_factory {
my ( $class, $rs_table ) = @_;
my $obj_class = ref $class || $class;
push @{ $rs_table->{post_save_action} }, \&reindex_object;
push @{ $rs_table->{post_remove_action} }, \&remove_object_from_index;
return __PACKAGE__;
}
Note that the return value is always the package that inserted the rule(s) into the ruleset. This enables the module that creates the class (SPOPS::Configure::Ruleset) to ensure that the same rule does not get entered multiple times.
POSSIBLE CHANGES
Instead of the above, we may change to something like:
sub ruleset_factory {
my ( $class ) = @_;
return { post_save_action => \&reindex_object,
post_remove_action => \&remove_object_from_index };
}
This is simpler, easier to follow and more consistent with how we discover behaviors to execute during the code generation process.
Rule Processor
You should never have to worry about this since it is implemented in SPOPS and therefore part of every SPOPS class. But it is here for completeness:
ruleset_process_action( ($object|$class), $action, \%params )
This method executes all the rules in a given ruleset for a given action. For instance, when called with the action name 'pre_fetch_action' it executes all the rules in that part of the ruleset.
Return value is true if all the rules executed ok, false if not.
NOTE: THIS MAY BE MODIFIED SO THAT EACH RULE CAN REPORT A STATUS WHICH IS AVAILABLE FOR LATER INSPECTION.
COPYRIGHT
Copyright (c) 2001-2002 Chris Winters. All rights reserved.
See SPOPS::Manual for license.
AUTHORS
Chris Winters <chris@cwinters.com>