Discussion:
Struggling with liskov substitution principle (again).
Richard Quadling
2014-07-22 09:06:48 UTC
Permalink
Hi.

I'm developing a versioned REST-based API.

So. endpoints like ...

/v1/controller/method/id/param1/param2/param3
/v2/controller/method/id/param1/param2/param3
/v3/controller/method/id/param1/param2/param3

Simple enough.

Internally, the code is structured such that the appropriate versioned
models are injected into the controller.

There is code that is specific to the version and code that is more
general, so, internally, there are namespaces ...

api\common
api\v1
api\v2
api\v3

In many cases (probably 60%), the version specific elements are nothing
more than wrappers for the common layer.

In some cases there is code in a version specific layer that requires a
version specific model...

namespace api\v1;
class ItemController extends api\Common\ItemController {
public function store(Item $o_Item) {
\\ Do store for v1.
\\...

\\ Let the parent have its say.
parent::store($o_Item);
}
}

Now the signature for the parent cannot have api\Common\Item or nothing as
the type hint.

And this is SO frustrating.

Basically, the common layer wants to know that the incoming objects match
(at least) the common layer.

And at the same time the version specific layer wants the incoming objects
to match (at least) the version specific layer.

Can I swap a v1 for a v2 at the version specific layer? No, of course not.

But at the common layer, certainly. As long as the version specific element
is based upon the common element.

What am I supposed to do to write common code AND be able to extend that
common code to version specificness and enforce that the appropriate types
are supplied?

The only way I can see is to create dummy interfaces and use them.

So, my base/common item implements iBaseItem and then no matter how this
gets extended, I can pass it through the layers and still have it enforced
appropriately.


In trying to understand why my code is NOT working I read this ...

stackoverflow.com/questions/2254404/does-the-liskov-substitution-principle-apply-to-subtype-which-inherited-from-abs?rq=1


If I have a method that requires a piece of fruit. And I have a base fruit
object that has all the properties and methods I need to operate upon it at
the general level, why can't I extend that into a apples and oranges and
extend the controller (appleController, orangeController) both of which are
based upon fruitController (that layer wants fruit - be it apples, oranges
or pears or anything that extends fruit).

Frustrated and confused and annoyed at the fatal error when without the
error my code would operate perfectly cleanly with no errors AND it would
stop any developer from trying to send a cactus to the juicer.

Regards,

Richard.
--
Richard Quadling
Richard Quadling
2014-07-22 09:08:40 UTC
Permalink
Hi.

I'm developing a versioned REST-based API.

So. endpoints like ...

/v1/controller/method/id/param1/param2/param3
/v2/controller/method/id/param1/param2/param3
/v3/controller/method/id/param1/param2/param3

Simple enough.

Internally, the code is structured such that the appropriate versioned
models are injected into the controller.

There is code that is specific to the version and code that is more
general, so, internally, there are namespaces ...

api\common
api\v1
api\v2
api\v3

In many cases (probably 60%), the version specific elements are nothing
more than wrappers for the common layer.

In some cases there is code in a version specific layer that requires a
version specific model...

namespace api\v1;
class ItemController extends api\Common\ItemController {
public function store(Item $o_Item) {
\\ Do store for v1.
\\...

\\ Let the parent have its say.
parent::store($o_Item);
}
}

Now the signature for the parent cannot have api\Common\Item or nothing as
the type hint.

And this is SO frustrating.

Basically, the common layer wants to know that the incoming objects match
(at least) the common layer.

And at the same time the version specific layer wants the incoming objects
to match (at least) the version specific layer.

Can I swap a v1 for a v2 at the version specific layer? No, of course not.

But at the common layer, certainly. As long as the version specific element
is based upon the common element.

What am I supposed to do to write common code AND be able to extend that
common code to version specificness and enforce that the appropriate types
are supplied?

The only way I can see is to create dummy interfaces and use them.

So, my base/common item implements iBaseItem and then no matter how this
gets extended, I can pass it through the layers and still have it enforced
appropriately.


In trying to understand why my code is NOT working I read this ...

stackoverflow.com/questions/2254404/does-the-liskov-substitution-principle-apply-to-subtype-which-inherited-from-abs?rq=1


If I have a method that requires a piece of fruit. And I have a base fruit
object that has all the properties and methods I need to operate upon it at
the general level, why can't I extend that into a apples and oranges and
extend the controller (appleController, orangeController) both of which are
based upon fruitController (that layer wants fruit - be it apples, oranges
or pears or anything that extends fruit).

Frustrated and confused and annoyed at the fatal error when without the
error my code would operate perfectly cleanly with no errors AND it would
stop any developer from trying to send a cactus to the juicer.

Regards,

Richard.
--
Richard Quadling
David Harkness
2014-07-22 19:14:02 UTC
Permalink
Post by Richard Quadling
If I have a method that requires a piece of fruit. And I have a base fruit
object that has all the properties and methods I need to operate upon it at
the general level, why can't I extend that into a apples and oranges and
extend the controller (appleController, orangeController) both of which are
based upon fruitController (that layer wants fruit - be it apples, oranges
or pears or anything that extends fruit).
Some code would really help, but I'll take a stab at what I think you're
doing.

class FruitJuicer {
public function juice(Fruit $fruit) { ... }
}

class AppleJuicer {
public function juice(Apple $apple) { ... chop apple before calling
superclass ... }
}

I think you're saying that you want to be able to have the typehint for
AppleJuicer::juice to be Apple (as above) instead of Fruit. This already
violates the Liskov Substitution principle because you couldn't swap in an
AppleJuicer anywhere a FruitJuicer is required because it may receive an
Orange or Cactus. You could create a JuicingStrategy interface and register
instances with the FruitJuicer. But I'm having a hard time applying the
above structure to a versioned API, so I suspect I'm not getting the
metaphor right.

When a request comes into a V2 endpoint, will all code handling that
request be from V2 and common? If so, it may be possible to have the front
controller set up the autoloader with those two source trees and use
replacement instead of subclassing to override common components. That may
not be ideal, but if each version requires only minor changes it may be
manageable. This would allow all versions of a single class to use the same
name which will make the type-hinting happy.

If you can post a small example, that will make discussion easier.

Peace,
David
Christoph Becker
2014-07-22 23:08:09 UTC
Permalink
Post by Richard Quadling
If I have a method that requires a piece of fruit. And I have a base fruit
object that has all the properties and methods I need to operate upon it at
the general level, why can't I extend that into a apples and oranges and
extend the controller (appleController, orangeController) both of which are
based upon fruitController (that layer wants fruit - be it apples, oranges
or pears or anything that extends fruit).
In your case the Liskov Substition Principle is all about
*contravariant* parameter/argument types. There is a nice article
available at
<http://www.gnu.org/software/sather/docs-1.2/tutorial/type-conformance.html>,
which helped me to understand this issue.

They are using a very descriptive example, using omnivores, herbivores
and carnivores. At a first glance, it seems reasonable to make
herbivores and carnivores a subtype of omnivores (among others, both can
eat). Obviously, meat and plants are a subtype of food, so one can
think of:

class Food {}
class Plant extends Food {}
class Meat extends Food {}

class Omnivore {
function eat(Food $food) {}
}
class Herbivore extends Omnivore {
function eat(Plant $plant) {}
}
class Carnivore extends Omnivore {
function eat(Meat $meat) {}
}

However, that doesn't work out. Consider an animal of type Omnivore,
which is actually a cow. According to the definition of Omnivore, you
can feed it any food--but surely the cow won't eat meat! This is why
PHP emits a "strict" warning for the code above.

Actually, the problem is that this subtyping relationship is not an
*is-a* relationship (neither a herbivore nor a carnivore is an
omnivore). The same holds for your example: neither an appleController
nor an orangeController is a fruitController, because they only can deal
with apples resp. oranges, but not general fruits.

IMHO, this simple *is-a* relationship explains it all: an apple *is a*
fruit, and an orange *is a* fruit. However, an apple juicer as well as
an orange juicer *is not a* fruit juicer (at least not necessarily).
--
Christoph M. Becker
--
PHP General Mailing List (http://www.php.net/)
To unsubscribe, visit: http://www.php.net/unsub.php
Loading...