« If PHP ends up dead, it will be us who killed it | What does true language mastery look like? » |
Chances are that any developer out there who has been involved in object oriented applications very long has run across the cardinal sin of object inheritance. I know I’ve committed this sin, and you probably have too. The sin of which I speak is a grave one, and it violates several well known and established principles of object oriented application development.
What is this sin of which I speak? It is none other than the addition of new public methods to an object that extends or implements abstract class or application interface, in violation of both the Liskov Substitution Principle and the Dependency Inversion Principle.
The L in S.O.L.I.D. stands for the Liskov Substitution Principle, a principle which states:
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
This seemingly simple principle is actually rather complicated. This principle tells us that when we are relying upon a particular object type, we are establishing a contract with ourselves and with our application that we will provide objects that are compliant with that type.
But as soon as we begin adding public methods and worse, depending on those methods, our adherence to the Liskov Substitution Principle starts to fall apart. No longer are all subtypes of a particular interface or abstract class correct for the program; instead, only some of the subtypes are now applicable. This breaks the Liskov substitution principle in half.
You might think that you can get around this by typehinting on your newly created object, thus ensuring that subtypes of your new object follow the Liskov Substitution Principle. Good try, but unfortunately you run afoul of the Dependency Inversion Principle.
In S.O.L.I.D., the D stands for the Dependency Inversion Principle. Often mistaken (especially in PHP) as the dependency injection principle, this principle states:
Do not depend upon concretions; depend upon abstractions instead.
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
So, as soon as you’ve defined your new concrete class with it’s shiny public methods, and you typehint on that specific class, you’re violating this principle because you’re now depending upon concretions, not abstractions. Whoops.
There are a few things that a developer can do to properly adhere to both the Liskov Substitution Principle and the Dependency Inversion Principle.
In PHP, it’s impossible to type hint on more than one interface; however, you may find the need to use more than one interface in a particular object. If you know that you’re going to be using more than one interface, it may be worthwhile to create an abstract class that you can type hint on that contains the interfaces you need.
interface MyInterface { // My methods } interface MySecondInterface { // My other methods } abstract class MyAbstractClass implements MyInterface, MySecondInterface { // Now when I type hint on MyAbstractClass, I can count on these interfaces too. }
This might seem like overkill, but it’s not. Remember that to truly develop SOLID code, there are going to be a few things that seem silly, but are required to adhere to the principles. Also, this is not an uncommon strategy in PHP: take a close look at the SPL where many classes implement one or more interfaces and then define abstract methods.
It was pointed out on Twitter by Anthony Ferrara, Beau Simensen and Reinier Kip that you can also extend interfaces, allowing you to join several interfaces together as a single interface.
While you implement interfaces in classes, you extend interfaces when using them as part of a new interface. For example:
interface A {} interface B {} interface C extends A, B {}
Since you want to avoid type hinting on concrete classes, you can instead create an abstract class that contains the API you require, and have that abstract class extend a parent abstract class. This way, you can still type hint on an abstract class (adhering to the Dependency Inversion Principle) and subtypes of the abstract class will be correct in the program (adhering to the Liskov Substitution Principle). It’s a win-win.
Not caring is an option too. Remember, the SOLID principles are just that: principles. They’re not laws, they’re not hard and fast rules, they aren’t concrete examples of The One True Way(tm) (this isn’t Rails here). You can ignore them if you so desire, so long as you understand them first. It’s really up to you.
Brandon Savage is the author of Mastering Object Oriented PHP and Practical Design Patterns in PHP
Posted on 9/9/2013 at 8:00 am
Adriano Varoli Piazza (@adrianovaroli) wrote at 9/9/2013 9:56 am:
Using a title like “The cardinal sin of X” and ending with “Or you can just not care” makes for an interesting contrast. Nice article, otherwise. I just wish we didn’t need to bait people into reading what we write.
Benjamin wrote at 9/9/2013 2:31 pm:
Teaching programming principles is great, but IMHO it would make a lot more sense to describe the “real-life” consequences of breaking such principles, like :
– breaking Liskov’s principle can cause severe regressions when subtypes are not able anymore to replace their parent type
– breaking dependency injection will make your code harder to test (because depending on a concrete type will prevent exchanging it with a mock)Regressions are the real sin. And regresions are harder to detect if you can’t test your code ;)
Jory Geerts wrote at 9/9/2013 3:29 pm:
How does adding a new public method break Liskov? In Java, every class extends lang.Object. This means that, if a method expects an instance of lang.Object, any random object can be passed in. Stating that “addition of new public methods to an object” breaks Liskov is to say Java as a whole, at its core, breaks Liskov.
The point isn’t that you can’t add a public method, the point is that you cannot break the (public) behavior of any existing methods.
That being said, if you type hint for lang.Object and then call println() on it, you are breaking Liskov – it is now impossible to pass any instance of lang.Object into your method. (Its like throwing a horse off of a building because you assumed any animal could fly – sure, you can do that, but nobody will think you’re sane afterwards.)
Also, I feel you’re taking the word ‘abstraction’ to literal when reading the definition of the dependency inversion principle. I think you should read it as “something not concrete”, not “an abstract class” – type hinting for interfaces is just fine.
Lastly, if a method requires one object that implements two interfaces, that really deserves a second look because somewhere in there, something is probably breaking the single responsibility principle.
Stuart Carnie (@stuartcarnie) wrote at 9/10/2013 9:46 am:
This article is simply incorrect. You most certainly can add methods to derived classes, and no where does the Liskov principal state otherwise. It is quite clear by the definition:
“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
As long as as the public methods from the base class still behave in a manner that is consistent across all subtypes, you have not violated that principal.
Many frameworks / runtimes have a base or root object and derived types add properties and methods to this root. .NET has System.Object, Java has java.lang.Object, Cocoa has NSObject, and so on.
Brandon Savage (@brandonsavage) wrote at 9/11/2013 11:13 am:
Don’t take it from me then. Take it from The Gang of Four, the authors of Design Patterns:
“When inheritance is used carefully (some will say properly), all classes derived from an abstract class will share its interface. This implies that a subclass merely adds or overrides operations and does not hide operations of the parent class. All subclasses can then respond to requests in the interface of this abstract class, making them all subtypes of the abstract class.
1. Clients remain unaware of the specific types of objects they use, as long as the objects adhere to the interface that clients expect.
2. Clients remain unaware of the classes that implement these objects. Clients only know about the abstract class(es) defining the interface.This so greatly reduces implementation dependencies between subsystems that it leads to the following principle of reusable object-oriented design:
Program to an interface, not an implementation.”
Now, PHP makes interfaces explicitly available in addition to abstract classes; however, the principle remains the same: altering the interface of the child classes violates the principle. The Liskov substitution principle was introduced in 1994, along with Design Patterns, so it’s unknown whether or not the authors were aware of the principle at the time, but even if they were, they are certainly endorsing following it.
Adding public methods to the inherited class violates the LSP because it alters the interface, and requires tight coupling to the implementation details. Type hinting on this specific concrete class would solve the issue, but violate the DIP because that directs us to program to an interface, not an implementation.
Languages that have a base object class don’t violate the principle per se; the base object class is neither type hinted or used in actual practice as an object. Additionally, adding methods to an abstract class or defining an interface that a concrete class implements doesn’t violate the principle, even if you’ve inherited from another abstract class. The goal here is type hinting on a specific abstraction, and not changing the abstraction in concrete objects.
Stuart Carnie (@stuartcarnie) wrote at 9/11/2013 2:02 pm:
Taken from your GoF quote above, emphasis mine:
“This implies that a subclass merely ADDS or overrides operations and DOES NOT HIDE operations of the parent class.”
Stating the Liskov principal (from wikipedia):
“in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).”
Adding methods to a class does not violate this principal. What is clearly stated above as simply: if my logic expects type base type T anywhere, I must be able to pass ANY subclass of that and my program should execute as expected.
With PHP allowing interfaces, it provides a way to define multiple public contracts that your class must adhere to, which would contradict your interpretation. Specifically, an interface is still a type that can be used to declare a signature of a function. Take for example your abstract class implementing two interfaces. Not unexpectedly, these two interfaces define two different public contracts, however MyAbstractClass has deviated from each by implementing both of these.
Look at SplFixedArray, which implements 4 interfaces, all very different:
class SplFixedArray implements Iterator, Traversable, ArrayAccess, Countable { … }
The public interface of SplFixedArray is significantly larger that each of the individual interfaces. I can declare strict method signatures depending on my requirements of the argument as follows:
function foo1(Traversable $bar) { … } // need to foreach $bar
function foo2(ArrayAccess $bar) { … } // need to randomly access $bar
function foo3(Countable $bar) { … } // need to determine if $bar has 1 or more items
In all three of these cases, the foo functions could care less what the full public interface of $bar looks like, as long as they adhere to the specified contract.
Another example that contradicts your interpretation is subclassing an abstract class and implementing a new interface on that subclass. I’ve quite probably deviated from the public contract of the abstract class by doing this, but I’ve not violated any principals as long as the interface I’ve implemented does not conflict with the existing public interface of the abstract class. Example of a violation:
interface DogInterface {
public function eat();
public function run();
public function walk();
}abstract class JobExecutor {
abstract public function run();
…
}class Foo extends JobExecutor implements DogInterface {
// BAD, because run() on a dog is very different from running a job
}Though the concerns of a job executor and a dog are very different, languages like C# allow explicit implementation of interfaces to get around this problem. PHP does not provide a way and therefore no way to support the above scenario.
I would also not typically encourage combining interfaces unless there is good reason to define a super contract, as you put more burden on your developers to implement that larger contract. A good reason for combining interfaces is that your method requires multiple interfaces to function correctly. So, from your example above, my program may require interface A and interface B, thus I combine them to make interface C to use as follows:
function foo(C $bar) { // use things from A and B }
Cheers,
Stuart
Derrick Nelson wrote at 9/12/2013 2:40 pm:
LSP is not violated by adding new public methods on a child class. I’ve read most of the article twice now, and still can’t figure out how you came to that conclusion.
What *is* bad practice is type-hinting on a concrete parent class, and then writing code that uses child-class methods, relying on the caller passing a child instance, but this does not translate into your “cardinal sin” by any means.
It’s also best practice to typehint on interfaces rather than concrete classes, but this also doesn’t translats into your “cardinal sin” either.
So, I really just think you’ve drawn the wrong conclusion from some good principles here.
« If PHP ends up dead, it will be us who killed it | What does true language mastery look like? » |