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
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 thestringable
interface and atoString
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 evenextend
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--
To unsubscribe, visit: https://www.php.net/unsub.php
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 thestringable
interface and atoString
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 evenextend
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:
- A module system to define which files are part of a class
- 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
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 thestringable
interface and atoString
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 evenextend
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:
- A module system to define which files are part of a class
- 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.
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
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 thestringable
interface and atoString
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 evenextend
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:
- A module system to define which files are part of a class
- 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.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