Composition over inheritance: native support for "delegating" methods?

php.internals

Rowan Collins

7 years ago
Hi, I've been thinking about how the language could better support Composition patterns like Decorators and Proxies, using some simple syntax sugar not dissimilar to Traits. Example problem: The other day, I was writing a session handler which slightly modified the behaviour of one provided by a library. The library implementation was closed for inheritance (a final class accessed via a factory which handled configuration), so I wrote a Decorator class, which added a single method. Everything worked fine, using Composition over Inheritance. But... In order for my Decorator class to implement the SessionHandlerInterface, I needed to write six stub functions, each of the form "function foo($bar) { return $this->delegatedHandler->foo($bar); }". With whitespace and docblocks, these amount to nearly 90 lines of code, as much as the actual custom code in the Decorator. Possible solution: Methods in a Decorator or Proxy which are delegated without any additional functionality can easily be generated programmatically, so could the language provide syntactic sugar to do this for you? - To delegate to an instance held in a property, add the "delegate" keyword at the end of the property declaration, followed by a list of method names to generate, e.g. "private Foo $foo delegate { doX, doY };" - Delegating static method calls to another class could also be supported, using a standalone declaration like "static delegate ClassName { staticMethodName };" - As with Traits, the generated methods would be stitched into the class at compile time, and not distinguishable in reflection. - Delegated methods could be used to implement an interface, either completely or partially (again, like Traits). - Other features of traits could be included if they were seen as useful, such as aliasing ("delegate { foo as bar }") and changing visibility ("delegate { foo as private }") Example: class SpecialSessionHandlerDecorator implements \SessionHandlerInterface, SpecialInterface {     private \SessionHandlerInterface $delegatedHandler delegate {        close, destroy, gc, open, read, write     };     public function specialMethod() { ... } } What do people think? Would this be useful, or am I missing some major limitation or implementation problem? Does any other language provide a feature like this which we can learn from? Regards,
-- Rowan Collins [IMSoP]

Marco Pivetta

7 years ago
On Sun, Oct 21, 2018 at 7:41 PM Rowan Collins <rowan.collins@gmail.com> wrote:
> Hi, > > I've been thinking about how the language could better support > Composition patterns like Decorators and Proxies, using some simple > syntax sugar not dissimilar to Traits. >
I do not think that this syntactic sugar is needed: * If you have few methods, writing the delegation code is trivially done, tested, statically analysed. * If you have a gazillion of methods, *something is really wrong* (you can't decorate something with ~5+ methods without introducing some unexpected side-effect - that code is already a bit too complex). Implementing this proposal to aid those scenarios is probably aiding bad API design. * If you need to proxy and add behavior, userland libraries already exist (mostly `doctrine/common` at https://github.com/doctrine/common/tree/a210246d286c77d2b89040f8691ba7b3a713d2c1/lib/Doctrine/Common/Proxy, `goaop/framework` at https://github.com/goaop/framework/tree/2.2.0 and `ocramius/proxy-manager` at https://github.com/Ocramius/ProxyManager/tree/2.2.2): if you aren't happy with those, a bit of `ReflectionClass#getMethods()` grunt work is just fine. We're still talking about edge cases anyway. * If you are proposing this syntax for performance reasons, most current delegation code has almost no noticeable overhead besides a new stack frame (possibly something the engine could optimise), and when it is present, it is to avoid I/O (much heavier anyway) Example:
> > class SpecialSessionHandlerDecorator implements > \SessionHandlerInterface, SpecialInterface { > private \SessionHandlerInterface $delegatedHandler delegate { > close, destroy, gc, open, read, write > }; > > public function specialMethod() { ... } > } >
One problem with delegating calls this way, is that any new method implemented by an ancestor will not be delegated by default. A better solution would be to have a single `delegate` keyword that intercepts all method calls for methods not declared in this class, as if `__call` was implicitly implemented: class SpecialSessionHandlerDecorator implements \SessionHandlerInterface, SpecialInterface { private \SessionHandlerInterface $delegateHandler delegate; public function specialMethod() { ... } } If you don't do it this way, you end up with a BC break any time an ancestor defines new public API. Yes, in this case these are interfaces, so it would already count as BC break, but if the parent is a class, adding new methods becomes a BC break as soon as the class is not `final` (I described it more carefully at https://github.com/Roave/BackwardCompatibilityCheck/issues/111 - missed to add that check there, so I'm glad that this issue reminded me of that). Still, even if you do it this way, implicit delegation of any newly added API may also not be wished, so it is an open-ended question. Anyway, I do think that: 1. the language already has all the tooling required to implement decorators correctly 2. the addition of a `delegate` functionality would cause more confusion for something that is already really trivial to implement/test/read The suggestion requires some stronger foundations/reasoning in order to be turned into a valuable RFC. Marco Pivetta http://twitter.com/Ocramius http://ocramius.github.com/

Sebastian Bergmann

7 years ago
Am 21.10.2018 um 23:42 schrieb Marco Pivetta:
> 1. the language already has all the tooling required to implement > decorators correctly > 2. the addition of a `delegate` functionality would cause more confusion > for something that is already really trivial to implement/test/read
ACK

Rowan Collins

7 years ago
Hi Marco, Thanks for the response. On 21/10/2018 22:42, Marco Pivetta wrote:
>  * If you need to proxy and add behavior, userland libraries already > exist (mostly `doctrine/common` at > https://github.com/doctrine/common/tree/a210246d286c77d2b89040f8691ba7b3a713d2c1/lib/Doctrine/Common/Proxy, > `goaop/framework` at https://github.com/goaop/framework/tree/2.2.0 and > `ocramius/proxy-manager` at > https://github.com/Ocramius/ProxyManager/tree/2.2.2): if you aren't > happy with those, a bit of `ReflectionClass#getMethods()` grunt work > is just fine. We're still talking about edge cases anyway.
These are certainly interesting, but they feel rather too "heavyweight" for every day use. Imagine if you had to include a library which used reflection to generate a string and pass it to eval() every time you wanted to sub-class something! Native AOP might come closer to what I was thinking about, but seems to be a very different way of thinking about code structure, rather than something that can be easily mixed with an existing architecture.
>  * If you are proposing this syntax for performance reasons, most > current delegation code has almost no noticeable overhead besides a > new stack frame (possibly something the engine could optimise), and > when it is present, it is to avoid I/O (much heavier anyway)
My main aim is to make composition feel more like a natural choice in the language. If I may cheekily use your own words against you:
> There will be less stuffing functionality in existing code via
inheritance, which, in my opinion, is a symptom of haste combined with feature creep. I have often heard it said that "it's a shame languages provide more features for inheritance than composition"; I was thinking about what features might redress that balance.
> One problem with delegating calls this way, is that any new method > implemented by an ancestor will not be delegated by default. A better > solution would be to have a single `delegate` keyword that intercepts > all method calls for methods not declared in this class, as if > `__call` was implicitly implemented:
This is deliberate; implicitly copying everything from another class would just be reinventing (multiple) inheritance. If the delegated class adds a new public method, it should be a conscious decision whether to a) transparently delegate it; b) explicitly wrap it with custom functionality; or c) completely ignore it.
> If you don't do it this way, you end up with a BC break any time an > ancestor defines new public API. Yes, in this case these are > interfaces, so it would already count as BC break, but if the parent > is a class...
The point is not that the *ancestors* are interfaces, it's that the proxy is targeting an interface. Consider this scenario: class LibraryImplementation implements FrameworkInterface, LibrarySpecificInterface {     public function thingRequiredByFramework() {}     public function additonalThing() {} } class UserlandWrapper implements FrameworkInterface {     public LibraryImplementation $inner delegate { thingRequiredByFramework }; } Although the wrapper is tied to a particular implementation (for whatever reason), it's only used in the context of the framework. If FrameworkInterface changes, then every class implementing it needs fixing, and that includes the UserlandWrapper (e.g, it might need to wrap a new method call with custom logic); but if LibrarySpecificInterface changes, and LibraryImplementation is updated, there's no relevance to UserlandWrapper. LibraryImplementationis just a tool being used to implement FrameworkInterface; its other abilities are irrelevant.
> Anyway, I do think that: > >  1. the language already has all the tooling required to implement > decorators correctly >  2. the addition of a `delegate` functionality would cause more > confusion for something that is already really trivial to > implement/test/read
I think the existence of userland libraries like ProxyManager is rather good evidence that the language *doesn't* have very powerful tools for this. But yes, any new feature like this has to be useful enough to justify the extra complexity and learning curve; it may be that the version presented here isn't powerful enough to pass that bar. Regards,
-- Rowan Collins [IMSoP]

Levi Morrison

7 years ago
I have wanted something like this before. I used a trait helper. This worked for this domain because there is an interface that is well established for the helper to use. Here's an example (written in my email client, untested): trait OuterIteratorHelper { abstract function getInnerIterator(): \Iterator; function rewind(): void { $this->getInnerIterator()->rewind(); } function valid(): bool { return $this->getInnerIterator()->valid(); } function next(): void { $this->getInnerIterator()->next(); } function key() { return $this->getInnerIterator()->key(); } function current() { return $this->getInnerIterator()->current(); } } final class TransformIterator implements \OuterIterator { private $transformer, $inner; function __construct(callable $transformer, \Iterator $inner) { $this->transformer = $transformer; $this->inner = $inner; } use OuterIteratorHelper; function getInnerIterator() { return $this->inner; } function current() { return ($this->transformer)($this->inner->current()); } }

Larry Garfield

7 years ago
On Sunday, October 21, 2018 12:41:04 PM CDT Rowan Collins wrote:
> Hi, > > I've been thinking about how the language could better support > Composition patterns like Decorators and Proxies, using some simple > syntax sugar not dissimilar to Traits. > > > Example problem: > > The other day, I was writing a session handler which slightly modified > the behaviour of one provided by a library. The library implementation > was closed for inheritance (a final class accessed via a factory which > handled configuration), so I wrote a Decorator class, which added a > single method. Everything worked fine, using Composition over > Inheritance. But... > > In order for my Decorator class to implement the > SessionHandlerInterface, I needed to write six stub functions, each of > the form "function foo($bar) { return > $this->delegatedHandler->foo($bar); }". With whitespace and docblocks, > these amount to nearly 90 lines of code, as much as the actual custom > code in the Decorator. > > > Possible solution: > > Methods in a Decorator or Proxy which are delegated without any > additional functionality can easily be generated programmatically, so > could the language provide syntactic sugar to do this for you? > > - To delegate to an instance held in a property, add the "delegate" > keyword at the end of the property declaration, followed by a list of > method names to generate, e.g. "private Foo $foo delegate { doX, doY };" > - Delegating static method calls to another class could also be > supported, using a standalone declaration like "static delegate > ClassName { staticMethodName };" > - As with Traits, the generated methods would be stitched into the class > at compile time, and not distinguishable in reflection. > - Delegated methods could be used to implement an interface, either > completely or partially (again, like Traits). > - Other features of traits could be included if they were seen as > useful, such as aliasing ("delegate { foo as bar }") and changing > visibility ("delegate { foo as private }") > > > Example: > > class SpecialSessionHandlerDecorator implements > \SessionHandlerInterface, SpecialInterface { > private \SessionHandlerInterface $delegatedHandler delegate { > close, destroy, gc, open, read, write > }; > > public function specialMethod() { ... } > } > > > What do people think? Would this be useful, or am I missing some major > limitation or implementation problem? Does any other language provide a > feature like this which we can learn from? > > Regards,
I like this in concept. However, my big issue is return values. Sometimes when proxying to an object like that, you want to return the return value of the proxied method. Sometimes when proxying to an object like that, you're supposed to return $this, but your $this is different than the proxied object's $this. Detecting when the generated proxy method should pass through the return value from the method it's proxying for vs when it should override it somehow sounds non-simple. --Larry Garfield

Rowan Collins

7 years ago
On 22/10/2018 14:38, Larry Garfield wrote:
> Sometimes when proxying to an object like that, you're supposed to return > $this, but your $this is different than the proxied object's $this.
Hm, that's a very interesting limitation. I wonder if there's some way to include common manipulations like this in the syntax, while still feeling more convenient than writing out the whole method signature. Regards,
-- Rowan Collins [IMSoP]