Skip to content

Instantly share code, notes, and snippets.

@pawlik
Last active August 29, 2015 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pawlik/b6d1253d81c9d8ce7a47 to your computer and use it in GitHub Desktop.
Save pawlik/b6d1253d81c9d8ce7a47 to your computer and use it in GitHub Desktop.
Delegator

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

limitations

stupid getters, params are only passed to last item in chain

#Ideas/possibilities

aliases and string explosion for more natural feel

/* gives us method workWithToolThisThat */
        $this->delegator->delegateSimpleChainFromString(
            'getTool->getThis->getThat->work', 'workWithToolThisThat'
        );

less stupid getters

$this->delegator->delegateChain(
    [
        'getTool' => function() {
            return $this->getTool('withParam');
        },
        'getThis' => function() {
            return $this->getThis('with', 'other', 'params');
        }, 
        'getThat', 'work'
    ]
);

other patterns emerging

try delegator

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
)

lazy loading

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.

fork me!

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'));

    }
}
@michal-niedzwiedzki
Copy link

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment