Recent code review dragged my attention to repeatedly delegated methods. (For nexway teammates https://github.com/NexwayGroup/N3/pull/2352/files#diff-1333ef8b1eee5de57912fa4ed14919dbR34)
example:
public function getBillingAddress()
{
return $this->getOrder()->getBillingAddress();
}
and always when I see such thingsI wonder how to refactor such thing. I thought about piece which can create such methods for us. Check out Delegator:
class Delegator {
private $definitions = [];
public function delegateSimpleChain($chain)
{
$lastInChain = array_pop($chain);
$this->definitions[$lastInChain] = array_merge($chain);
}
public function call($methodName, $arguments, $caller)
{
if(array_key_exists($methodName, $this->definitions)) {
$result = $caller;
foreach($this->definitions[$methodName] as $nextInChain) {
$result = call_user_func([$result, $nextInChain]);
}
return call_user_func_array([$result, $methodName], $arguments);
}
throw new \BadMethodCallException(
"$methodName is not defined in " . __CLASS__ . " definitions"
);
}
}
and usage:
class BaseWithDelegatorTool {
private $tool;
private $delegator;
public function __construct(Workable $tool)
{
$this->tool = $tool;
$this->delegator = new Delegator();
/**
* below (togheter with __call method below) replaces
* ```
* function work($arg) {
* return $this->getTool()->getThis()->getThat()->work($arg);
* }
*/
$this->delegator->delegateSimpleChain(
['getTool', 'getThis', 'getThat', 'work']
);
}
public function __call($methodName, $args)
{
return $this->delegator->call($methodName, $args, $this);
}
public function getTool()
{
return $this->tool;
}
}
This is working idea
stupid getters, params are only passed to last item in chain
#Ideas/possibilities
/* gives us method workWithToolThisThat */
$this->delegator->delegateSimpleChainFromString(
'getTool->getThis->getThat->work', 'workWithToolThisThat'
);
$this->delegator->delegateChain(
[
'getTool' => function() {
return $this->getTool('withParam');
},
'getThis' => function() {
return $this->getThis('with', 'other', 'params');
},
'getThat', 'work'
]
);
Also similar repetition was
public function getStrategy()
{
if ($this->getParentBlock()->getStrategy()) {
return $this->getParentBlock()->getStrategy();
}
return $this->getData('strategy');
}
looks familiar? Because it is!
$this->tryDelegatorChain(
'getParentBlock->getStrategy()', // delegator
true, // test, could be callback
function() { return $this->getData('strategy'); }, // fallback if test fails
'getStrategy' // alias
)
if looking widely sich code:
public function getSth() {
if(!$this->sth} {
$this->sht = new Sth();
}
return $this->sth;
}
```php
Is also some kind of tryDelegator. And this is most abundant SRP violation in any codebase I think.
So implementing laxy loading should be as simple as
```php
$this->delegator->delegateCached(
'getStuff->andMore->doActualWork()`
)
by the first run getStuff->andMore
result would be cached.
for reference - full working test (PHPUnit and Mockery needed)
<?php
namespace DelegatorTest;
interface Workable {
public function work($with);
}
class Delegator {
private $definitions = [];
public function delegateSimpleChain($chain)
{
$lastInChain = array_pop($chain);
$this->definitions[$lastInChain] = array_merge($chain);
}
public function call($methodName, $arguments, $caller)
{
if(array_key_exists($methodName, $this->definitions)) {
$result = $caller;
foreach($this->definitions[$methodName] as $nextInChain) {
$result = call_user_func([$result, $nextInChain]);
}
return call_user_func_array([$result, $methodName], $arguments);
}
throw new \BadMethodCallException(
"$methodName is not defined in " . __CLASS__ . " definitions"
);
}
}
class BaseWithDelegatorTool {
`
private $tool;
private $delegator;
public function __construct(Workable $tool)
{
$this->tool = $tool;
$this->delegator = new Delegator();
/**
* below (togheter with __call method below) replaces
* ```
* function work($arg) {
* return $this->getTool()->getThis()->getThat()->work($arg);
* }
*/
$this->delegator->delegateSimpleChain(
['getTool', 'getThis', 'getThat', 'work']
);
}
public function __call($methodName, $args)
{
return $this->delegator->call($methodName, $args, $this);
}
public function getTool()
{
return $this->tool;
}
}
class DelegatorTest extends \PHPUnit_Framework_TestCase {
public function tearDown()
{
\Mockery::close();
}
public function test_new()
{
$tool = \Mockery::mock('\DelegatorTest\Workable');
$tool->shouldReceive('getThis->getThat->work')
->with('foo')
->andReturn('done!');
$base = new BaseWithDelegatorTool($tool);
$this->assertEquals('done!', $base->work('foo'));
}
}
My concern is two fold. First, it's a clever code. Clever code scares the shit out of me. It's hard to learn and hard to debug. Second, it offers no support for IDE to hint at delegated methods.
I guess the question is whether "return $this->getParentBlock()->getStrategy()" is such a big pain that we need to put a pattern around it.
Perhaps just "return $this->getParentBlock()->getStrategy() ?: $this->getData('strategy');" would suffice? (unless strategy is an object, then it won't).