Re: Add interface implementation to class in separate file
| From: | Olle Härstedt | Date: | Sun, 13 Sep 2020 19:49:37 +0000 |
| Subject: | Re: Add interface implementation to class in separate file | ||
| References: | 1 2 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-111852@lists.php.net to get a copy of this message | ||
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 <file>;" 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