Easen.co.uk The home of Marc Easen

17May/110

PHP – The Visitor Pattern

Recently I have come across a wonderful like pattern that I would like to share with you, it's called the Visitor Pattern. If you haven't come across it before it's a object-oriented way of separating your logic/algorithms from your models, therefore allowing your models can do what they do best - store and retrieve data; and your algorithms can focus on being more abstract allowing you to apply to other sets of data in the future.

Where I currently work I'm an Senior Software Engineer in a team which primarily writes PHP code - why? Because it's quick and flexible language which allows us to be reactive to change within the business. Since my first day there we have been focused on using the MVC pattern, but with a twist - MLVC; the L being Logic. The purpose being to force you to write your algorithms into a abstract way so other applications/tasks can make use of them. Until recently this was fine... a new application came along (call it App A) and we wrote a new set of models and wrote some logic to populate/manipulate/update/etc. them. The problem came when we want to use these models in another application (App B), the logic we wrote for those models was built with App A in mind, without a few weeks of abstracting, refactoring, testing we wouldn't have been able to make use of these models for App B. This is where the visitor pattern comes in, if we used it from the word go we wouldn't have had such a problem when App B came along. Consider this example...

  1. class Model_A {
  2.     public $data = null; // Please use accessors, not public vars
  3. }
  4.  
  5. class Logic_A {
  6.     public static function getModelA() {
  7.         $model = new Model_A();
  8.         $model->data = DB::fetchData(/** Use App A's data source */);
  9.         return $model;
  10.     }
  11. }
  12.  
  13. $modelA = Logic_A::getModelA();
class Model_A {
	public $data = null; // Please use accessors, not public vars
}

class Logic_A {
	public static function getModelA() {
		$model = new Model_A();
		$model->data = DB::fetchData(/** Use App A's data source */);
		return $model;
	}
}

$modelA = Logic_A::getModelA();

There's nothing technically wrong with that (except the public $data, see comment), but if I want to use another data source or I want to expand it to let's say save the data to another data source, like a cache for example, I would expand the Logic_A to do this. What if another application doesn't want to save the model to a cache, or worse, it wants to write the data to a file... expand Logic_A again. Now you should see that's there a problem here, you're putting all of your logic into one class, which has been design to application agnostic and it's going to become 100s, if not 1000s of lines code long in no time at all...

Now consider the following instead...

  1. class Model_A implements Interface_AcceptVisitor {
  2.     public $data = null; // Once again this is bad practise, this is only an example
  3.  
  4.     public function acceptVisitor(Visitor_Abstract $visitor) {
  5.         $visitor->visit($this);
  6.     }
  7. }
  8.  
  9. interface Interface_AcceptVisitor {
  10.     public function acceptVisitor(Visitor_Abstract $visitor);
  11. }
  12.  
  13. abstract class Visitor_Abstract {
  14.     abstract public function visit($instance);
  15. }
  16.  
  17. class Visitor_A {
  18.     public $dataSource = null;
  19.     public function visit($instance) {
  20.         if (!$this->dataSource) return; // Visitor requires a data source
  21.         $instance->data = DB::fetchData($this->dataSource);
  22.     }
  23. }
  24.  
  25. $loadDataVisitor = new Visitor_A();
  26. $loadDataVisitor->dataSource = /** Use App X's data source */;
  27.  
  28. $modelA = new Model_A();
  29. $modelA->acceptVisitor($loadDataVisitor);
class Model_A implements Interface_AcceptVisitor {
	public $data = null; // Once again this is bad practise, this is only an example

	public function acceptVisitor(Visitor_Abstract $visitor) {
		$visitor->visit($this);
	}
}

interface Interface_AcceptVisitor {
	public function acceptVisitor(Visitor_Abstract $visitor);
}

abstract class Visitor_Abstract {
	abstract public function visit($instance);
}

class Visitor_A {
	public $dataSource = null;
	public function visit($instance) {
		if (!$this->dataSource) return; // Visitor requires a data source
		$instance->data = DB::fetchData($this->dataSource);
	}
}

$loadDataVisitor = new Visitor_A();
$loadDataVisitor->dataSource = /** Use App X's data source */;

$modelA = new Model_A();
$modelA->acceptVisitor($loadDataVisitor);

Well what's different, apart from the use of the visitor pattern, the output is the same, but now I'm able to use another data source and allows the logic and the model to do what they do best. In addition to this, I'm able to re-use this visitor on another model too.

Ok what if I wanted to expand the second example to save the model to a cache then, how would I do that - easy, write another visitor to save the model. What if another application wants to save the data to a file - easy, write another visitor to-do that. Now here's the another nice feature of this pattern, another developer can come along and pick and chose which visitors they want to apply to their models, without getting stuck by reading 1000's of code to find the one function that does what they want. If a visitor doesn't exist, then they can write a new one, instead of try to hack in some custom logic into monster of a logic class.

From my own point of view this allows me break down really complex logic into manageable algorithms, which I don't have to worry about trying to force into one huge class and at the same time make it abstract enough that someone else can use it.

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

(required)

No trackbacks yet.