Re: [RFC][Discussion] Why can constructors violate LSP?
| From: | Robert Landers | Date: | Thu, 23 Nov 2023 21:50:18 +0000 |
| Subject: | Re: [RFC][Discussion] Why can constructors violate LSP? | ||
| References: | 1 2 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-121792@lists.php.net to get a copy of this message | ||
On Thu, Nov 23, 2023 at 10:30 PM Deleu <deleugyn@gmail.com> wrote:
>
>
>
> On Thu, Nov 23, 2023 at 5:31 PM Robert Landers <landers.robert@gmail.com> wrote:
>>
>> Hello Internals,
>>
>> As you may know, an inherited method cannot reduce the visibility of
>> an overridden method. For example, this results in a fatal error
>> during compilation:
>>
>> class P {
>> public function hello($name = 'world') {
>> echo "hello $name\n";
>> }
>> }
>>
>> class C extends P {
>> private function hello($name = 'world') {
>> parent::hello($name);
>> echo "goodbye $name\n";
>> }
>> }
>>
>> However, we can make certain methods private anyway, namely,
>> constructors (I haven't gone hunting for other built-in methods yet).
>> This is perfectly allowed:
>>
>> class P {
>> public function __construct($name = 'waldo') {
>> echo "hello $name\n";
>> }
>> }
>>
>> class C extends P {
>> private function __construct($name = 'world') {
>> parent::__construct($name);
>> echo "goodbye $name\n";
>> }
>> }
>>
>> To my somewhat trained eye, this appears to violate the Liskov
>> Substitution Principle, for example, this now can have hidden errors:
>>
>> function create(P $class) {
>> return new (get_class($class))();
>> }
>>
>> proven by:
>>
>> $c = (new ReflectionClass(C::class))
>> ->newInstanceWithoutConstructor();
>>
>> create($c);
>>
>> Even though we thought we knew that the constructor was declared public.
>>
>> I'd like to propose an RFC to enforce the covariance of constructors
>> (just like is done for other methods), to take effect in PHP 9, with a
>> deprecation notice in 8.3.x.
>>
>> I'm more than happy to implement it.
>>
>> Does anyone feel strongly about this one way or the other?
>
>
> Constructors are an implementation detail of a specialized class and as such they're not
> subject to LSP because the goal of LSP is to be able to make sure that any object of a given type
> hierarchy can be used to accomplish a certain behavior. If you take a step back from PHP's
> dynamic nature and think about LSP from a more pure type system, the fact you're expecting an
> object of type C, but then you completely disregard everything about the object itself and dive into
> it's metadata to build another object, that's the moment you're no longer playing by
> the rules of OOP. It's like those mathematical equations that prove that 1 = 2, they all have
> one thing in common: they end up dividing by 0 at some point.
>
> OOP here dictates that you should reach for patterns like Builder, Abstract Factory or similar.
> That way you constraint yourself to the rules of OOP and you won't get weird outcomes.
>
> From another point of view, when a type is expected by a function or method, all we can expect
> from it is whatever was defined as the blueprint (class/interface) of that object and the
> __construct() is a special method that is not assumed to be part of that blueprint because it's
> not reasonable to do
$object->__construct(); after receiving an
> object. As such, a constructor cannot break LSP because the constructor is not part of the
> object's API from a "receptor" point of view.
>
> I don't have a vote so take my opinion with a bucket of salt, but if I could I would
> definitely vote against such RFC.
>
>
> --
> Marco Deleu
Thanks Marco,
That's an interesting perspective and one I would agree with for the
most part, especially if you take my illustration at face value. Where
it gets weird/breaks down is when you have a class-string, that you
assert is the correct type, and then try to instantiate it:
// from somewhere
$class = "C";
if(is_subclass_of($class, P::class)) {
$example = new $class("world");
}
If PHP didn't offer these built-in methods, then I would fully agree
with you, but it does, which puts it into a weird position where
sometimes a class is substitutable, and in this one special case, it
is not.
Robert Landers
Software Engineer
Utrecht NL