Add interface implementation to class in separate file

  111850
September 12, 2020 20:22 olleharstedt@gmail.com (=?UTF-8?Q?Olle_H=C3=A4rstedt?=)
Hi internals!

Separation of data and behaviour is both a fun and hard discussion,
especially considering:

* "It should be possible to add new features without touching old code"; and
* "Principle of Least Privilege" (never expose more than you have to)
(https://en.wikipedia.org/wiki/Principle_of_least_privilege).

There should (could) be a way to add new behaviour to old data without
touching the old data (class). Traits won't work in this use-case,
since they assume the same internal structure for all trait-using
classes. Imagine the `stringable` interface and a `toString` trait. A
__toString() method needs knowledge about the internal structure of a
class Foo. Yet if we want to keep adding behaviour to Foo, we'll end
up with either exposing too much of Foo, or expanding the class file
indefinitely. Please note that composition is not a proper solution,
since it requires exposure of Foo; composition leads to lack of proper
encapsulation, or representation exposure.

In Haskell it's possible to split instance implementation of
type-classes into separate files. In Rust you can have a struct with
private fields and put impl of behaviour in different files (but same
crate).

A similar feature in PHP could look like (using new keyword `expand`
but could be anything, or even `extend` in new context):

```
// File FooStringable.php
expand Foo implements stringable {
  public function __toString() {
    // Full access to Foo's all private fields here.
    // Assumes you can autoload Foo.
    // Assumes usage of $foo->__toString(); will be configured with
autoload to dynamically find the correct behaviour of Foo.
  }
}
```

If you'd use composition instead, you'd maybe have a formatter class
with a method `$formatter->toString(stringable $foo)`. This has the
problem I mentioned with exposing too much of $foo; it breaks
encapsulation. It has the benefit of being able to provide multiple
toString methods with different formats, but would have to assume
similar structure of the objects passed to it (defined with an
interface), which is not always possible or desirable.

The other way is inheritance, which doesn't scale over multiple
behaviours. `FooWithStringable extends Foo`? No.

Was I clear here? Do you understand the issues that this design
pattern is trying to solve? Its purpose is to solve "keep adding new
feature to old data" in a clean and proper way, while keeping
information encapsulation.

Enjoy the weekend!
Olle
  111851
September 13, 2020 17:58 kontakt@beberlei.de (Benjamin Eberlei)
On Sat, Sep 12, 2020 at 10:23 PM Olle Härstedt <olleharstedt@gmail.com>
wrote:

> Hi internals! > > Separation of data and behaviour is both a fun and hard discussion, > especially considering: > > * "It should be possible to add new features without touching old code"; > and > * "Principle of Least Privilege" (never expose more than you have to) > (https://en.wikipedia.org/wiki/Principle_of_least_privilege). > > There should (could) be a way to add new behaviour to old data without > touching the old data (class). Traits won't work in this use-case, > since they assume the same internal structure for all trait-using > classes. Imagine the `stringable` interface and a `toString` trait. A > __toString() method needs knowledge about the internal structure of a > class Foo. Yet if we want to keep adding behaviour to Foo, we'll end > up with either exposing too much of Foo, or expanding the class file > indefinitely. Please note that composition is not a proper solution, > since it requires exposure of Foo; composition leads to lack of proper > encapsulation, or representation exposure. > > In Haskell it's possible to split instance implementation of > type-classes into separate files. In Rust you can have a struct with > private fields and put impl of behaviour in different files (but same > crate). > > A similar feature in PHP could look like (using new keyword `expand` > but could be anything, or even `extend` in new context): > > ``` > // File FooStringable.php > expand Foo implements stringable { > public function __toString() { > // Full access to Foo's all private fields here. > // Assumes you can autoload Foo. > // Assumes usage of $foo->__toString(); will be configured with > autoload to dynamically find the correct behaviour of Foo. > } > } > ``` > > If you'd use composition instead, you'd maybe have a formatter class > with a method `$formatter->toString(stringable $foo)`. This has the > problem I mentioned with exposing too much of $foo; it breaks > encapsulation. It has the benefit of being able to provide multiple > toString methods with different formats, but would have to assume > similar structure of the objects passed to it (defined with an > interface), which is not always possible or desirable. > > The other way is inheritance, which doesn't scale over multiple > behaviours. `FooWithStringable extends Foo`? No. > > Was I clear here? Do you understand the issues that this design > pattern is trying to solve? Its purpose is to solve "keep adding new > feature to old data" in a clean and proper way, while keeping > information encapsulation. >
Do I understand you correctly, it would be somewhat like "opening" up a class and making changes to it in another file? Certainly a powerful concept, but I would be very interested in the details how that would interact with autoloading. If I have a class Foo loaded, and its "extension" FooString with toString method not, then it would lead to the "toString" code missing.
> > Enjoy the weekend! > Olle > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php > >
  111852
September 13, 2020 19:49 olleharstedt@gmail.com (=?UTF-8?Q?Olle_H=C3=A4rstedt?=)
2020-09-13 17:58 GMT, Benjamin Eberlei <kontakt@beberlei.de>:
> On Sat, Sep 12, 2020 at 10:23 PM Olle Härstedt <olleharstedt@gmail.com> > wrote: > >> Hi internals! >> >> Separation of data and behaviour is both a fun and hard discussion, >> especially considering: >> >> * "It should be possible to add new features without touching old code"; >> and >> * "Principle of Least Privilege" (never expose more than you have to) >> (https://en.wikipedia.org/wiki/Principle_of_least_privilege). >> >> There should (could) be a way to add new behaviour to old data without >> touching the old data (class). Traits won't work in this use-case, >> since they assume the same internal structure for all trait-using >> classes. Imagine the `stringable` interface and a `toString` trait. A >> __toString() method needs knowledge about the internal structure of a >> class Foo. Yet if we want to keep adding behaviour to Foo, we'll end >> up with either exposing too much of Foo, or expanding the class file >> indefinitely. Please note that composition is not a proper solution, >> since it requires exposure of Foo; composition leads to lack of proper >> encapsulation, or representation exposure. >> >> In Haskell it's possible to split instance implementation of >> type-classes into separate files. In Rust you can have a struct with >> private fields and put impl of behaviour in different files (but same >> crate). >> >> A similar feature in PHP could look like (using new keyword `expand` >> but could be anything, or even `extend` in new context): >> >> ``` >> // File FooStringable.php >> expand Foo implements stringable { >> public function __toString() { >> // Full access to Foo's all private fields here. >> // Assumes you can autoload Foo. >> // Assumes usage of $foo->__toString(); will be configured with >> autoload to dynamically find the correct behaviour of Foo. >> } >> } >> ``` >> >> If you'd use composition instead, you'd maybe have a formatter class >> with a method `$formatter->toString(stringable $foo)`. This has the >> problem I mentioned with exposing too much of $foo; it breaks >> encapsulation. It has the benefit of being able to provide multiple >> toString methods with different formats, but would have to assume >> similar structure of the objects passed to it (defined with an >> interface), which is not always possible or desirable. >> >> The other way is inheritance, which doesn't scale over multiple >> behaviours. `FooWithStringable extends Foo`? No. >> >> Was I clear here? Do you understand the issues that this design >> pattern is trying to solve? Its purpose is to solve "keep adding new >> feature to old data" in a clean and proper way, while keeping >> information encapsulation. >> > > Do I understand you correctly, it would be somewhat like "opening" up a > class and making changes to it in another file? > > Certainly a powerful concept, but I would be very interested in the details > how that would interact with autoloading. If I have a class Foo loaded, and > its "extension" FooString with toString method not, then it would lead to > the "toString" code missing.
Yes, a little like opening up, *but* with clear restrictions. It was explained to me that this won't work without either: 1) A module system to define which files are part of a class 2) Manually write in the "main" class file which other extensions to this class should be loaded. The reason is again encapsulation - it should not be possible for any file to just get access to private fields by adding a new interface implementation. Option (2) can be achieved if we allow "include ;" in PHP *inside* a class definition. Again note that this is different from a trait, since it gives access to private properties that are *different* between the classes using it, like toString() or toQuery(). Option (2) does not need configured autoloading. Option (1) is more elaborate, maybe composer.json would need to configure something, *or* it is assumed that a class is defined in a single folder instead of a single file. Which already is kind of like a module system. In Rust, it's possible to spread out behaviour (impl) to a certain data (struct) in the same _crate_ while keeping access to private fields: https://users.rust-lang.org/t/implement-private-struct-in-different-files/29407 C# has something called "partial class": https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods Olle
  111869
September 16, 2020 03:30 mike@newclarity.net (Mike Schinkel)
> On Sep 13, 2020, at 3:49 PM, Olle Härstedt <olleharstedt@gmail.com> wrote: > > 2020-09-13 17:58 GMT, Benjamin Eberlei <kontakt@beberlei.de <mailto:kontakt@beberlei.de>>: >> On Sat, Sep 12, 2020 at 10:23 PM Olle Härstedt <olleharstedt@gmail.com> >> wrote: >> >>> Hi internals! >>> >>> Separation of data and behaviour is both a fun and hard discussion, >>> especially considering: >>> >>> * "It should be possible to add new features without touching old code"; >>> and >>> * "Principle of Least Privilege" (never expose more than you have to) >>> (https://en.wikipedia.org/wiki/Principle_of_least_privilege). >>> >>> There should (could) be a way to add new behaviour to old data without >>> touching the old data (class). Traits won't work in this use-case, >>> since they assume the same internal structure for all trait-using >>> classes. Imagine the `stringable` interface and a `toString` trait. A >>> __toString() method needs knowledge about the internal structure of a >>> class Foo. Yet if we want to keep adding behaviour to Foo, we'll end >>> up with either exposing too much of Foo, or expanding the class file >>> indefinitely. Please note that composition is not a proper solution, >>> since it requires exposure of Foo; composition leads to lack of proper >>> encapsulation, or representation exposure. >>> >>> In Haskell it's possible to split instance implementation of >>> type-classes into separate files. In Rust you can have a struct with >>> private fields and put impl of behaviour in different files (but same >>> crate). >>> >>> A similar feature in PHP could look like (using new keyword `expand` >>> but could be anything, or even `extend` in new context): >>> >>> ``` >>> // File FooStringable.php >>> expand Foo implements stringable { >>> public function __toString() { >>> // Full access to Foo's all private fields here. >>> // Assumes you can autoload Foo. >>> // Assumes usage of $foo->__toString(); will be configured with >>> autoload to dynamically find the correct behaviour of Foo. >>> } >>> } >>> ``` >>> >>> If you'd use composition instead, you'd maybe have a formatter class >>> with a method `$formatter->toString(stringable $foo)`. This has the >>> problem I mentioned with exposing too much of $foo; it breaks >>> encapsulation. It has the benefit of being able to provide multiple >>> toString methods with different formats, but would have to assume >>> similar structure of the objects passed to it (defined with an >>> interface), which is not always possible or desirable. >>> >>> The other way is inheritance, which doesn't scale over multiple >>> behaviours. `FooWithStringable extends Foo`? No. >>> >>> Was I clear here? Do you understand the issues that this design >>> pattern is trying to solve? Its purpose is to solve "keep adding new >>> feature to old data" in a clean and proper way, while keeping >>> information encapsulation. >>> >> >> Do I understand you correctly, it would be somewhat like "opening" up a >> class and making changes to it in another file? >> >> Certainly a powerful concept, but I would be very interested in the details >> how that would interact with autoloading. If I have a class Foo loaded, and >> its "extension" FooString with toString method not, then it would lead to >> the "toString" code missing. > > Yes, a little like opening up, *but* with clear restrictions. It was > explained to me that this won't work without either: > > 1) A module system to define which files are part of a class > 2) Manually write in the "main" class file which other extensions to > this class should be loaded. > > The reason is again encapsulation - it should not be possible for any > file to just get access to private fields by adding a new interface > implementation. > > Option (2) can be achieved if we allow "include ;" in PHP > *inside* a class definition. Again note that this is different from a > trait, since it gives access to private properties that are > *different* between the classes using it, like toString() or > toQuery(). > > Option (2) does not need configured autoloading. Option (1) is more > elaborate, maybe composer.json would need to configure something, *or* > it is assumed that a class is defined in a single folder instead of a > single file. Which already is kind of like a module system.
Doesn't option 2 fail the very first criteria you argued for in your initial email? Doesn't it fail the criteria "It should be possible to add new features without touching old code?" IOW, we should not have to modify any source code in Symfony to be able to expand one of its classes, for example. If modifying the original source is required, you really don't need a new syntax, just modify the class and add your toString method. (BTW, your first criteria sounds like it would be enabling the Open/closed principle of S.O.L.I.D. for PHP classes.) Also, I expect requiring a module system would eliminate this idea from even being considered given past discussions of "modules." So, back to what Benjamin implied, for this to even be considered there would need to be an elegant and performant way to handle the autoloading of such class expansions. The challenge is — without annotating the class itself — how do you know when an expansion exists without first explicitly naming it or looping through the autoloader potentially many times per class? -Mike
  111873
September 16, 2020 19:28 olleharstedt@gmail.com (=?UTF-8?Q?Olle_H=C3=A4rstedt?=)
2020-09-16 3:30 GMT, Mike Schinkel <mike@newclarity.net>:
> > >> On Sep 13, 2020, at 3:49 PM, Olle Härstedt <olleharstedt@gmail.com> >> wrote: >> >> 2020-09-13 17:58 GMT, Benjamin Eberlei <kontakt@beberlei.de >> <mailto:kontakt@beberlei.de>>: >>> On Sat, Sep 12, 2020 at 10:23 PM Olle Härstedt <olleharstedt@gmail..com> >>> wrote: >>> >>>> Hi internals! >>>> >>>> Separation of data and behaviour is both a fun and hard discussion, >>>> especially considering: >>>> >>>> * "It should be possible to add new features without touching old >>>> code"; >>>> and >>>> * "Principle of Least Privilege" (never expose more than you have to) >>>> (https://en.wikipedia.org/wiki/Principle_of_least_privilege). >>>> >>>> There should (could) be a way to add new behaviour to old data without >>>> touching the old data (class). Traits won't work in this use-case, >>>> since they assume the same internal structure for all trait-using >>>> classes. Imagine the `stringable` interface and a `toString` trait. A >>>> __toString() method needs knowledge about the internal structure of a >>>> class Foo. Yet if we want to keep adding behaviour to Foo, we'll end >>>> up with either exposing too much of Foo, or expanding the class file >>>> indefinitely. Please note that composition is not a proper solution, >>>> since it requires exposure of Foo; composition leads to lack of proper >>>> encapsulation, or representation exposure. >>>> >>>> In Haskell it's possible to split instance implementation of >>>> type-classes into separate files. In Rust you can have a struct with >>>> private fields and put impl of behaviour in different files (but same >>>> crate). >>>> >>>> A similar feature in PHP could look like (using new keyword `expand` >>>> but could be anything, or even `extend` in new context): >>>> >>>> ``` >>>> // File FooStringable.php >>>> expand Foo implements stringable { >>>> public function __toString() { >>>> // Full access to Foo's all private fields here. >>>> // Assumes you can autoload Foo. >>>> // Assumes usage of $foo->__toString(); will be configured with >>>> autoload to dynamically find the correct behaviour of Foo. >>>> } >>>> } >>>> ``` >>>> >>>> If you'd use composition instead, you'd maybe have a formatter class >>>> with a method `$formatter->toString(stringable $foo)`. This has the >>>> problem I mentioned with exposing too much of $foo; it breaks >>>> encapsulation. It has the benefit of being able to provide multiple >>>> toString methods with different formats, but would have to assume >>>> similar structure of the objects passed to it (defined with an >>>> interface), which is not always possible or desirable. >>>> >>>> The other way is inheritance, which doesn't scale over multiple >>>> behaviours. `FooWithStringable extends Foo`? No. >>>> >>>> Was I clear here? Do you understand the issues that this design >>>> pattern is trying to solve? Its purpose is to solve "keep adding new >>>> feature to old data" in a clean and proper way, while keeping >>>> information encapsulation. >>>> >>> >>> Do I understand you correctly, it would be somewhat like "opening" up a >>> class and making changes to it in another file? >>> >>> Certainly a powerful concept, but I would be very interested in the >>> details >>> how that would interact with autoloading. If I have a class Foo loaded, >>> and >>> its "extension" FooString with toString method not, then it would lead >>> to >>> the "toString" code missing. >> >> Yes, a little like opening up, *but* with clear restrictions. It was >> explained to me that this won't work without either: >> >> 1) A module system to define which files are part of a class >> 2) Manually write in the "main" class file which other extensions to >> this class should be loaded. >> >> The reason is again encapsulation - it should not be possible for any >> file to just get access to private fields by adding a new interface >> implementation. >> >> Option (2) can be achieved if we allow "include ;" in PHP >> *inside* a class definition. Again note that this is different from a >> trait, since it gives access to private properties that are >> *different* between the classes using it, like toString() or >> toQuery(). >> >> Option (2) does not need configured autoloading. Option (1) is more >> elaborate, maybe composer.json would need to configure something, *or* >> it is assumed that a class is defined in a single folder instead of a >> single file. Which already is kind of like a module system. > > Doesn't option 2 fail the very first criteria you argued for in your initial > email? Doesn't it fail the criteria "It should be possible to add new > features without touching old code?"
Yeah, kind of. Just trying to find the shortest path to make it work in PHP. At least it would be possible to split classes in this case, and also split data from behaviour if you want, while keeping full encapsulation.
> > IOW, we should not have to modify any source code in Symfony to be able to > expand one of its classes, for example. If modifying the original source is > required, you really don't need a new syntax, just modify the class and add > your toString method. > > (BTW, your first criteria sounds like it would be enabling the Open/closed > principle of S.O.L.I.D. for PHP classes.) > > Also, I expect requiring a module system would eliminate this idea from even > being considered given past discussions of "modules."
I had a thought of namespace-private class properties, but I'll start another thread for that. Swift has access level "internal" for this purpose.
> So, back to what Benjamin implied, for this to even be considered there > would need to be an elegant and performant way to handle the autoloading of > such class expansions. The challenge is — without annotating the class > itself — how do you know when an expansion exists without first explicitly > naming it or looping through the autoloader potentially many times per > class?
Yeah, good points. I don't have an answer right now.
> > -Mike
Someone told me about extensions in Swift language, which is a good example of what I mean: https://docs.swift.org/swift-book/LanguageGuide/Extensions.html Olle