SitePoint Sponsor |
|
User Tag List
Results 1 to 25 of 25
-
Oct 25, 2004, 21:12 #1
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Patterns for Abstract Rule Processing
I have an application under way that is starting to emit bad code smells. I have a number of parameters related to each record (dates, foreign key values, text, etc.) which I need to evaluate against a number of business rules under two circumstances:
a) each time the record is updated when it is in it's "pending" state
and b) when the user attemts to "issue" the record
To date, I have a model class that is basically a Row Data Gateway to handle retrieveing and saving the data in the database. In my form processing (controller) I have been adding private methods to process some of these business rule checks (smell #1, code in the wrong location - belongs in the model, not in the controller). Having these methods private also makes them hard to test (smell #2, hard to test).
So what I am trying to dig at is resources or peoples experiences in trying to model reasonably complex validation logic. Some of this logic goes way beyond "this field required" or "match this regex" kind of validation. In some cases the logic needs to look up the prior issued revision of these records and compare fields from that revision as part of the logic, etc.
Any links or references to appropriate patterns would be greatly appreciated.Jason Sweat ZCE - jsweat_php@yahoo.com
Book: PHP Patterns
Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
Detestable (adjective): software that isn't testable.
-
Oct 26, 2004, 01:30 #2
- Join Date
- Jan 2004
- Location
- Oslo, Norway
- Posts
- 894
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
In Domain-Driven Design, Eric Evans discusses the Specification pattern. That's the most relevant I can think of. It may not be available on the Web.
Dagfinn Reiersøl
PHP in Action / Blog / Twitter
"Making the impossible possible, the possible easy,
and the easy elegant" -- Moshe Feldenkrais
-
Oct 26, 2004, 04:07 #3
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
That is actually the current book I am reading. May have to skip a bit to get there sooner
Thanks.
-
Oct 26, 2004, 05:04 #4
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
I think what I have now could be thought of as a Transactional Script (PoEAA pg 110). Here is a snipit of my code:
PHP Code:public function isValidToIssue() {
if ($this->data = $this->atmc->getData($this->sk)) {
$valid = true;
$valid &= $this->isPending();
$valid &= $this->requiredFieldsPopulated();
$valid &= $this->requiredAlloyTmprPopulated();
return $valid;
} else {
$this->log->push('Invalid key send for issuing ATMC', self::LOG_HEADING);
return false;
}
I have a sincle method isValidToIssue() that calls several private "child" methods to build up the more complex logic.
But, as Fowler warned, as the logic evolves and gets more complex, it is becoming impossible to adequatly test and maintain this kind of code.
-
Oct 26, 2004, 08:53 #5
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
I think the Specification pattern from DDD is pointing me in the right direction. I am heading towards:
PHP Code:class PolicySelector {
public function getPolicy($policy_name)
// getPolicy returns a chain of Specifications
// created by chaining the and_() methods of concrete Specification classes
}
abstract class Specification {
protected $andSpec = false;
abstract function isSatisfiedBy($doc);
protected function returnValue($doc, $ret) {
return (is_object($this->andSpec))
? $this->andSpec->isSatisfiedBy($doc) && $ret
: $ret;
}
public function and_($spec) {
if (is_object($spec)) {
$this->andSpec = $spec;
}
return $this;
}
}
Last edited by sweatje; Oct 26, 2004 at 09:02. Reason: more descriptive var names
-
Oct 26, 2004, 10:27 #6
- Join Date
- Oct 2004
- Location
- downtown
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
http://radio.weblogs.com/0124037/2004/02/03.html
What I'm reading at the moment, any help?
-
Oct 26, 2004, 11:02 #7
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
That does help confirm my direction. The tweak I made to the pattern was to nest the Specifications in a chain so I could pass around the policy and still test it with any object. This also meant I had to pass in my user notification object at the time I create the policies so I can get the deeply nested messages. All seems to be going okay and much more testable now
-
Oct 26, 2004, 11:25 #8
- Join Date
- Oct 2004
- Location
- downtown
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Off Topic:
Looking for this pattern has lead me to find some papers on model driven archetecture, and was wondering just how revelent this is to php development? does anyone use this approach(es) in their development.
EDIT
----
Seams there are a few faults with a model driven archetecture,
http://www.agilemodeling.com/essays/mda.htm
Could be interesting reading.
Thanks, and sorry for going off topic sweatje
Glad to hear it
-
Oct 26, 2004, 16:05 #9
- Join Date
- Apr 2003
- Location
- London
- Posts
- 2,423
- Mentioned
- 2 Post(s)
- Tagged
- 0 Thread(s)
Hi...
Originally Posted by sweatje
(MostlyMechanics <--> MyPolicy) --> IWantThis
IWantThis could be a completely separate package of business specific stuff and nothing to do with MostlyMechanics. Yuk.
However if the Policy/Strategy is abstract (interface) then we can do this...
(MostlyMechanics --> Policy) <-- MyPolicy --> IWantThis
This is good ol' dependency inversion. We have managed to get a visibility arrow pointing from right to left in there. In fact what we now have is the MyPolicy can see both packages, but nothing can see it. This is equivalent to saying that it is in a higher layer so I have started calling this policy promotion (TM). You push the business rules out to a new higher layer.
My worry is that to work well this has to be done whole heartily, otherwise you will get business logic in both the policy packages and the mechanics packages...
(Mechanics --> AllPolicies) <-- MyPolicy --> IWantThis
This is all off the top of my head and experimental of course. I would love to know if anyone has had any similar experiences.
Also, anyone know anything about rules engines? I think you can get Oracle plug-in ones and Java ones (e.g. Jess). I have just about no knowledge of these.
yours, MarcusMarcus Baker
Testing: SimpleTest, Cgreen, Fakemail
Other: Phemto dependency injector
Books: PHP in Action, 97 things
-
Oct 27, 2004, 07:20 #10
- Join Date
- Jun 2004
- Location
- Bogota
- Posts
- 101
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Hi,
Could you post a little bit of code? I find it easier to understand with examples
Thanks!If I have wings, why am I walking?
-
Oct 27, 2004, 07:34 #11
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by lastcraft
It does now serve my needs much better: very flexible, business logic centralized in the model classes, and a much more testable solution
-
Oct 27, 2004, 13:41 #12
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Still working on unit tests and building up the actual validation logic, but I think by the end of this adventure I may be able to submit this code for some kind of a "most logic in a single line of PHP" award of some sort
(for the return statement)
PHP Code:private function updatePolicy() {
$m = 'Update Validation';
$ret = new ValidLockSpecification($this->log, $m);
return $ret->and_(
$this->fieldEqualsField('POST_sk', 'CUR_SK'
,'The web form you submitted does not match the document you are editing,
do you have two browser editing windows open?', $m)->and_(
$this->fieldEquals(
'CUR_PNDG_INDIC','Y'
,'This document is no longer pending (has already been issued).',$m)->and_(
$this->requiredFieldWarn('CUST', $m)->and_(
$this->fieldMatchWarn('BEGIN', '~^\d{2}/\d{2}/\d{4}$~'
,'Begin date format not mm/dd/yyyy', $m)->and_(
$this->fieldMatchWarn('END', '~^\d{2}/\d{2}/\d{4}$~'
,'End date format not mm/dd/yyyy', $m)->and_(
$this->fieldGtWarn('MAX', 0, 'Maximum volume must be specified', $m)->and_(
$this->fieldGtFieldWarn('MAX', 'MIN', 'Max must be greater than Min volume', $m)
)))))));
}
-
Oct 27, 2004, 16:34 #13
- Join Date
- Apr 2003
- Location
- London
- Posts
- 2,423
- Mentioned
- 2 Post(s)
- Tagged
- 0 Thread(s)
Hi...
Originally Posted by otnemem
In web_cache.php, etc say we have...
PHP Code:class PageCache {
function PageCache($policy) { }
}
class CachePolicy { }
class FixedTimeOut extends CachePolicy {
function FixedTimeOut($max_age) { }
}
PHP Code:class LoadMonitor {
function getCurrentLoad() { }
}
So we create a third package that can see the other two and place it in there. For this to be easy for others the caching package must document this behaviour, so that the CachingPolicy subclasses become a visible interface. That's what I mean by promoted. Of course unlike Java, C++, etc. we don't split by namespaces and so "package" is a bit arbitrary. If you think of a package as a shippable component though, the problem is a bit clearer.
Now our new higher level package is still very closely tied to the caching one by that subclass. Its not just tied by interface, but by implementation as well. Code changes in the caching module could break our higher level one. To weaken this linkage we should export just the interface. This means that the CachingPolicy class must be gutted of all working code, making it a passive receiver of questions. Once this is done the dependency inversion is complete. You don't have to go that far of course unless you were genuinely shipping a modular library of some sort (e.g.PEAR).
Once the strategy is passive, most of the subclasses within the caching package will probably be trivial. Simplest then is to remove them and have only the interface.
Which reminds me about something in SimpleTest. I always meant to split the UnitTester package into Tester and Reporter and move all of the reporter stuff into it's own group. Never did get around to that.
yours, MarcusMarcus Baker
Testing: SimpleTest, Cgreen, Fakemail
Other: Phemto dependency injector
Books: PHP in Action, 97 things
-
Oct 27, 2004, 19:29 #14
- Join Date
- Sep 2003
- Location
- Bristol, UK
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Marcus, what you're describing sounds very similar to the Separated Interface pattern. Am I correct in this assumption, or have I missed something?
Always open to question or ridicule
-
Oct 28, 2004, 08:35 #15
- Join Date
- Apr 2003
- Location
- London
- Posts
- 2,423
- Mentioned
- 2 Post(s)
- Tagged
- 0 Thread(s)
Hi...
Originally Posted by sleepeasy
. Subtle isn't it?
His example (the data mapper) is kind of upside down compared with mine. He's got the O/R layer treating the domain object as a strategy for writing rows to the database. I hadn't thought of it like that before.
Most of this patterns lark seem to come down to passing one object into another and letting them sort it out between them. If the object passed in is just data then it's a State, if you ask it to run methods then it's a Strategy and if it calls back to the holding object in one of these methods it's a Visitor. Once you get a handle on the passing in part I reckon you just refactor and let others worry about whether it's a pattern or not.
I'm more intuitive than verbal though.
yours, MarcusMarcus Baker
Testing: SimpleTest, Cgreen, Fakemail
Other: Phemto dependency injector
Books: PHP in Action, 97 things
-
Oct 29, 2004, 06:55 #16
- Join Date
- Jan 2004
- Location
- Oslo, Norway
- Posts
- 894
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by lastcraft
Dagfinn Reiersøl
PHP in Action / Blog / Twitter
"Making the impossible possible, the possible easy,
and the easy elegant" -- Moshe Feldenkrais
-
Nov 9, 2004, 22:04 #17
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
A little update on where I arrived. I am pretty happy with the result, but would welcome comments. One "deviation" from the Specification pattern was I wanted not only to have a true/false for if the tested object met the criteria, I also wanted a more detailed message about why the object failed. To this end I passed in a "NotificationStack" which has a push($mesage, $heading) method for each specification to attach details of why a particular policy failed.
Here is the abstract base class for all of my specifications:
PHP Code:/**
* make sure to always pass in a NotifyStack object to the constructor and store in $this->log
* @package proj
* @subpackage models
* @abstract
*/
abstract class Specification {
protected $andSpec = false;
protected $log = false;
/**
* method to evalute this specification
*
* must always end with return $this->returnValue($atmc, $ret);
* to correctly process and() method handling
* @param DataSpace $test the WACT dataspace being evaluated
* @return Boolean
* @abstract
*/
abstract function isSatisfiedBy($test);
/**
* decend the and chain if present compounding return values
* @param DataSpace $test the WACT dataspace being evaluated
* @param Boolean $ret this specifications evaluation result
* @return Boolean
*/
protected function returnValue($test, $ret) {
$ret = (preg_match('/warning/i', get_class($this))) ? true : $ret;
return (is_object($this->andSpec))
? $this->andSpec->isSatisfiedBy($test) && $ret
: $ret;
}
/**
* evalute another specification condition as part of this policy
*
* the funky name and_ has a trailing _ because otherwise PHP would consider it
* the 'and' keyword
* @param Specification another specificaiton to be evaluated
* @return Specification a reference to this Specification object
*/
public function and_($spec) {
if (is_object($spec)) {
$this->andSpec = $spec;
}
return $this;
}
}
PHP Code:class FieldInArraySpecification extends Specification {
protected $field;
protected $set;
protected $check;
protected $msg;
protected $logdesc;
protected $empty_check;
function __construct($field, $set, $log, $key=false, $msg=false, $logdesc='Validation Warning') {
if (!is_string($field)) {
throw new SpecificationSetupException('invalid field parameter');
}
$this->field = $field;
if (!is_array($set)) {
throw new SpecificationSetupException('invalid set parameter');
}
$this->set = $set;
if (!(is_object($log) && method_exists($log, 'push'))) {
throw new SpecificationSetupException('invalid log parameter');
}
$this->log = $log;
$this->logdesc = $logdesc;
$this->check = ($key) ? 'array_key_exists' : 'in_array';
$list = '('.implode(',',$set).')';
$list_msg = (strlen($list)>50) ? 'valid list' : $list;
$this->msg = ($msg) ? $msg : "Field '$field' not in $list_msg";
$this->empty_check = preg_match('/orEmpty/i', get_class($this));
}
function isSatisfiedBy($test) {
if ($this->empty_check && !$test->get($this->field))
return $this->returnValue($test, true);
$check_funct = $this->check;
if (!$ret = $check_funct($test->get($this->field), $this->set)) {
$this->log->push($this->msg, $this->logdesc);
}
return $this->returnValue($test, $ret);
}
}
class FieldInArrayWarningSpecification extends FieldInArraySpecification {}
class FieldInArrayOrEmptySpecification extends FieldInArraySpecification {}
class FieldInArrayOrEmptyWarningSpecification extends FieldInArraySpecification {}
-
Nov 10, 2004, 03:07 #18
- Join Date
- Oct 2004
- Location
- downtown
- Posts
- 145
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Rather than duplicating the code into for different classes, each with their own minor tweak, I actually coded checks within the class itself to evalute the instances own class name and alter its behavior depending on that result.
-
Nov 10, 2004, 05:55 #19
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by Version0-00e
PHP Code:preg_match('/warning/i', get_class($this))
-
Nov 10, 2004, 07:22 #20
- Join Date
- Nov 2001
- Location
- Bath, UK
- Posts
- 2,498
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by sweatje
DouglasHello World
-
Nov 10, 2004, 08:05 #21
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by DougBTX
PHP Code:class FieldInArraySpecification extends Specification {
function isSatisfiedBy($test) {
$check_funct = $this->check;
if (!$ret = $check_funct($test->get($this->field), $this->set)) {
$this->log->push($this->msg, $this->logdesc);
}
return $this->returnValue($test, $ret);
}
}
class FieldInArrayOrEmptySpecification extends Specification {
function isSatisfiedBy($test) {
if (!$test->get($this->field))
return $this->returnValue($test, true);
$check_funct = $this->check;
if (!$ret = $check_funct($test->get($this->field), $this->set)) {
$this->log->push($this->msg, $this->logdesc);
}
return $this->returnValue($test, $ret);
}
}
PHP Code:$check_funct = $this->check;
if (!$ret = $check_funct($test->get($this->field), $this->set)) {
$this->log->push($this->msg, $this->logdesc);
}
return $this->returnValue($test, $ret);
PHP Code:class FieldInArraySpecification extends Specification {
function isSatisfiedBy($test) {
if (preg_match('/orEmpty/i', get_class($this) && !$test->get($this->field))
return $this->returnValue($test, true);
$check_funct = $this->check;
if (!$ret = $check_funct($test->get($this->field), $this->set)) {
$this->log->push($this->msg, $this->logdesc);
}
return $this->returnValue($test, $ret);
}
}
class FieldInArrayOrEmptySpecification extends FieldInArraySpecification {}
-
Nov 10, 2004, 08:22 #22
- Join Date
- Nov 2001
- Location
- Bath, UK
- Posts
- 2,498
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by sweatje
This would be it with inheritance, where that repeated block of code is inherited from the parent:
PHP Code:class FieldInArraySpecification extends Specification {
function isSatisfiedBy($test) {
$check_funct = $this->check;
if (!$ret = $check_funct($test->get($this->field), $this->set)) {
$this->log->push($this->msg, $this->logdesc);
}
return $this->returnValue($test, $ret);
}
}
class FieldInArrayOrEmptySpecification extends FieldInArraySpecification {
function isSatisfiedBy($test) {
if (!$test->get($this->field))
return $this->returnValue($test, true);
return parent::isSatisfiedBy($test);
}
}
DouglasHello World
-
Nov 10, 2004, 10:14 #23
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Fair enough, that would work for me.
-
Nov 12, 2004, 06:45 #24
- Join Date
- Jun 2003
- Location
- Iowa, USA
- Posts
- 3,749
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Looking into this more, seems like I have run into an area that would require Multiple Inheritance to actually work from that pattern. The "standard"
interitance worked fine for a single dimension, i.e.
PHP Code:function meth1() {
if (guard_condition_1) return2;
parent::meth1();
}
fieldInArray
fieldInArrayOrEmpty
fieldInArrayWarning
fieldInArrayOrEmptyWarning
Specification is the base class. "Warning" style specification override the returnVal() method, and could be a new base class for Specification to inherit from. Thus we could have:
abstract Specification
abstract WarningSpecification extends Specification
and this would work for:
[php]
FieldInArray extends Specification {}
FieldInArrayOrEmpty extends FieldInArray {
function isSatisfiedBy() {
if (guard) return one way;
parent::isSatisfiedBy();
}
}[/php
but what would I do here?
PHP Code:FieldInArrayWarn extends WarningSpecification {
//how do we get FieldInArray::isSatisfiedBy without copying?
}
FieldInArrayOrEmptyWarn extends ??? {
//how do we get FieldInArray::isSatisfiedBy?
//how do we get WarningSpecificaiton::returnVal?
}
Edit:
It just occoured to me as I hit submit, WarningSpecification should not be a subclass at all, warning should be a property of the Specification that could be set for any specification and acted on accordingly by the returnValue() method. Sorry for the noise.Last edited by sweatje; Nov 12, 2004 at 06:48. Reason: new idea
-
Jul 5, 2005, 13:58 #25
- Join Date
- Jul 2005
- Posts
- 1
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
do u think specifications will work with "lazy load"
Bookmarks