Edit: Added an update with a more concrete example further down.
I just wanted to share and provide a further explanation on the Proxy Pattern example I gave at the Ann Arbor PHP/MySQL User Group today.
Source: https://gist.github.com/4302634
Now, Dan had mentioned that this could be done through simple inheritance instead of via interface contract, and after some thought, I realized that I disagree.
Typically with an API object, you will initialize with a remote URL, and perhaps a token:
public function __construct($api_url, $auth_token) |
public function __construct($api_url, $auth_token)
But remember our new Cache API proxy only takes an API object and a Cache object.
public function __construct($cache, $api) |
public function __construct($cache, $api)
This fundamentally changes the contract that the initial API object creates, and as such, should then NOT extend the original API implementation.
Part of doing OOP well involves the management of contracts we establish. Changing the rules of a function in a sub-classed entity violates the Liskov substitution principle (as referenced in SOLID object oriented design).
Now, you COULD subclass the MyApi class, and add the Cache control via setter injection, but personally I don't think that implementation is nearly as clean as providing a proxy object to add the caching functionality.
p.s. I'll leave QuickTime running but I think it did eat the recording of the presentation -- sorry guys! I'll put the slides up soon but I don't know how much context they will provide given how light they were compared to the commentary that went along with it.
Update (12/07/2012):
It has been pointed out that my example was perhaps a little too contrived, so I think I found a better one.
Let's say you're building a system out, and you know you want logging, but you don't know what sort of implementation you want to do for said logging. Given that instance, let's start with our basic interface:
1
2
3
4
5
6
| interface Logger
{
public function info($message);
public function debug($message);
public function fatal($message);
} |
interface Logger
{
public function info($message);
public function debug($message);
public function fatal($message);
}
Ok, great! So, because production schedules are tight, we decide to give a very basic implementation first:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| class MyBasicLogger implements Logger
{
public function info($message)
{
error_log('INFO: '.$message);
}
public function debug($message)
{
error_log('DEBUG: '.$message);
}
public function fatal($message)
{
error_log('FATAL: '.$message);
}
} |
class MyBasicLogger implements Logger
{
public function info($message)
{
error_log('INFO: '.$message);
}
public function debug($message)
{
error_log('DEBUG: '.$message);
}
public function fatal($message)
{
error_log('FATAL: '.$message);
}
}
Alright! We now have our basic logger implemented!
Oh, what's that you say? You want to use Monolog in place of error_log() in production? sure!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| class MyMonologLogger implements Logger
{
protected $monolog;
public function __construct($monolog)
{
$this->monolog = $monolog;
}
public function info($message)
{
$this->monolog->addInfo($message);
}
public function debug($message)
{
$this->monolog->addDebug($message);
}
public function fatal($message)
{
$this->monolog->addCritical($message);
}
} |
class MyMonologLogger implements Logger
{
protected $monolog;
public function __construct($monolog)
{
$this->monolog = $monolog;
}
public function info($message)
{
$this->monolog->addInfo($message);
}
public function debug($message)
{
$this->monolog->addDebug($message);
}
public function fatal($message)
{
$this->monolog->addCritical($message);
}
}
Now we have two ways of logging messages. Now comes the constraint that we want to be emailed when fatal errors are logged. Your first instinct would be to extend MyMonologLogger and add it by overloading fatal(), but then we can't use that functionality on any other logger we ever build. How do we build this functionality in a way that we can reuse over and over again? The Proxy pattern.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| class MyEmailingLoggerProxy implements Logger
{
protected $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function fatal($message)
{
$this->logger->fatal($message);
mail('admin@example.com', 'A fatal error has occurred', $message);
}
public function info($message)
{
$this->logger->info($message);
}
public function debug($message)
{
$this->logger->debug($message);
}
} |
class MyEmailingLoggerProxy implements Logger
{
protected $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function fatal($message)
{
$this->logger->fatal($message);
mail('admin@example.com', 'A fatal error has occurred', $message);
}
public function info($message)
{
$this->logger->info($message);
}
public function debug($message)
{
$this->logger->debug($message);
}
}
And now, no matter what we choose as our logging backend, now or in the future, we will always easily be able to have those fatal errors emailed to us by simply putting our chosen logging system inside an instance of MyEmailingLoggerProxy.
I hope this clears some things up!