IS-STRICTLY-EQUIVALENT-TO-A
Favor object composition over class inheritance.
Maxims like this drive me crazy. On the one hand, there in one sentence is some of the best advice I can imagine for designing Object-Oriented software. On the other hand, following this advice without any idea why one favors one over the other, without any idea when class inheritance is the right thing to do…
As
Basho said, “Do not seek to follow in the footsteps of the men of old; seek what they sought.”
Perhaps of the maxim “Favor object composition over class inheritance” is what happens when we seek what they sought, when we question the fundamentals, when we ask ourselves, “
What does inheritance really mean?So: What does it mean when we go to the design white board (or pick up a 3x5 card in a design session) and boldly write:
Manager IS-AN Employee
???
Commenting on
IS-A IS-A HAS-A, Robert Fisher pointed out that there is no definitive meaning to inheritance in programming languages. Another reader reminded him that Bertrand Meyer’s
Object-Oriented Software Construction identifies no fewer than twelve(!) kinds of inheritance. I took that to support Robert’s point: if there really are twelve different kinds of inheritance, it is easy to see how two reasonable, intelligent people could each write “A IS-A B” and mean two completely different things.
Should the compiler always win?There is one argument—the compiler always wins—where the only thing that matters is what a program
does. A program’s code means whatever effect the program has when it runs. So when you organize your code around a result like the fastest possible performance, or the smallest possible program, and in doing so you use features like inheritance strictly because they help your technical objective and not because you think they carry any meaning.
This practice is programming for the compiler. Smeone who is programming for the compiler doesn’t even think in terms of “IS-A,”, they think directly in their language’s features—like interfaces, abstract classes, or modules.
I appreciate that point of view, but I believe that we can produce
even better software if we decide that language features like inheritance have a semantic meaning above and beyond what the compiler does with them. Especially when we are considering projects that have multiple programmers working on them over a substantial period of time. Such projects must produce
sustainable code, not just code that works today.
When we have a shared understanding of inheritance’s semantic meaning, our code more closely resembles the problem it solves. If it is just an engineering tool, a statement like Manager IS-AN Employee may have no parallel in the real world. Are managers really employees? When we declare other relationships such as HAS-MANY, we work very hard to make our code model the real world. Why would IS-AN be any different?
That being said, there seem to be as many opinions of what class inheritance means as there are programmers. Do any two programmers—much less a full team— agree on exactly what “Manager IS-AN Employee” means if Employee and Manager are both classes?
All I can do is share is a meaning that has worked well for me: Strict Equivalence. I’m going to describe what I mean and illustrate some of the consequences of adopting this as the standard meaning of “IS-A” on your project.
(I am not trying to say that Strict Equivalence is the only or even best way to build OO systems: I have worked on many projects with a different understanding, and they worked out just fine.)
Strict EquivalenceYou know the
Liskov Substitution Principle, of course. In short, if X IS-A Y, then anywhere you could use a Y, you can substitute an X and things should Just Work. So if every Manager IS-AN Employee, every piece of code that operates on Employees can operate on Managers and Just Work.
What I just said is the Liskov Substitution Principle, but there is a lot of room for discussion over what ‘just works’ means. So here is something much more specific, Strict Equivalence.
A
client is a piece of code that
uses an entity. Strict Equivalence is the following property: X IS-A Y if and only if it is the case that for every client that uses an object of class Y, you can substitute an object of class X and the results from the perspective of the client are
indistinguishable.
Note that clients expecting an
X can presumably distinguish an X from a Y, but that’s because they know about the differences between X and Y. Note also, and with emphasis, that we are talking about
classes. Specifically, concrete classes. If X and Y are Java interfaces or C++ abstract base classes, there is no difference between Strict Equivalence and Liskov Substitutability, which leads to a completely different maxim, “program to interfaces and not implementations,” or my preferred interpretation—“Favour exposing interfaces over exposing implementations.”
This is not the same as Liskov Substitutability. With Liskov Substitutability, X IS-A Y if and only if it is the case that for every client that uses an object of class Y, you can substitute an object of class X and the results will be
semantically valid although they may differ from the perspective of the client. Again, we are talking about a case where the client is using a Y, not using an interface. For example:
public class OurClient {
public void consumeY(final Y someY) {
...
}
}
This code is a client that uses and depends on objects of class Y, not an interface that Y implements.
Strict Equivalence is a specialization of Liskov Substitutability
One of the nice properties of Strict Equivalence is that it also guarantees semantic validity: It guarantees that you can substitute an X for a Y and since the results are indistinguishable to a client expecting a Y, the results will be semantically valid as well. Therefore, Strict Equivalence is a specialization of Liskov Substitutability.
So if you like Liskov Substitutability, you ought to also like Strict Equivalence. But it is still a specialization, and a very restricted specialization at that. Let’s see how.
Consider something very basic. Overriding a method. Isn’t that what you’re supposed to do? You have a method in a superclass, but you need a different, more specialized behaviour in the subclass. This is OOP 101. And it’s usually wrong. Okay, wrong is a strong word. It’s usually a violation of Strict Equivalence. Let’s have a look at why.
Take our Employees and Managers, where every Manager IS-AN Employee. We want to have a calculate_annual_bonus method for employees. You can imagine writing it, blah-blah personal goals set in performance review, blah-blah overall division profit.
Now what happens with managers? Perhaps the business rule is that managers get 10% of the bonus of each of their reports, like an
MLM plan. So you write something like:
class Employee
def calculate_annual_bonus
personal_goals.select(&:achieved).map(&:incentive).inject(&:+) +
self.division.declared_profit_share_amount
end
end
class Manager < Employee
def calculate_annual_bonus
reports.map(&:calculate_annual_bonus).inject(&:+) * 0.1
end
end
(You can easily imagine the Java equivalent, starting with public class Manager extends Employee…)
With Liskov Substitutability, everything is fine: you have some code that transfers a bonus to an employee’s bank account. It asks the employee object to calculate the annual bonus, and it transfers that amount. Nice. But Strict Equivalence prohibits this simple and common construction.
The reason this is prohibited is that if we allow a subclass to override the bonus calculation, we have effectively declared that the amount of annual bonus is arbitrary for employees. Our code
says that the correct calculation is
personal_goals.select(&:achieved).map(&:incentive).inject(&:+)
+
self.division.declared_profit_share_amount
, but in reality when we have an employee object, that may not be true.
Overriding the annual bonus calculation breaks the declared behaviour of the Employee class. It is no different than if some code in your Manager class reached out like a
ninja and redefined the calculate_annual_bonus method in the employee class. From the perspective of a client using an employee object, how is that different?
With Strict Equivalence, you may only override the annual bonus calculation if client code like pay_annual_bonuses cannot tell the difference. So when I am looking at the Employee class, I can’t tell whether someone has written a Manager class. But I do have confidence that if they do write a Manager class, they are not to override the calculate_annual_bonus calculation in a way that breaks my code: whatever they do, it must be indistinguishable to me.
But… but… but…Strict Equivalence seems at first to violate the whole point of polymorphism. Why should a client know or care what a method does? Why should it rely on a what an Employee defines as the correct calculation for an annual bonus?
One argument in favour of overriding is to talk about the separation of interface and implementation. Orthodox OO teaches that a class’s
interface is its collection of method signatures, and its
implementation is what those methods happen to do. The notion is that clients should program to the interface only: the implementation is the class’s private business.
This practice is reified in C++, where the public interface of a class is found in a separate header file from the implementation. Sounds good. Now what happens with Java? In Java, a class’s interface and its implementation are in the same file. But if you want to give someone just the collection of method signatures, Java provides interfaces. So if you as the author wish to have clients program strictly to method signatures, you can give them an interface.
The point here is that Strict Equivalence is
not in conflict with orthodox interface-oriented design: they can live happily side-by-side if you simply use interfaces when you mean interfaces, abstract methods when you mean abstract methods, and if you think twice before providing a concrete method in a class your provide to clients.
The difference comes up when you ask a client to use a class and provide an actual working method. In our example, we deliberately put behaviour in the Employee class, even though we have alternatives. In Ruby, we could borrow an idiom from Smalltalk and write:
def calculate_annual_bonus
raise 'implemented by subclass'
end
In Java, we could declare it to be an abstract method:
abstract class Employee {
abstract public Money calculateAnnualBonus();
protected final Money baseAnnualBonus() {
// helper calculation
}
}
public class Contributor extends Employee
public Money calculateAnnualBonus() {
return baseAnnualBonus();
}
}
public class Manager extends Employee
public Money calculateAnnualBonus() {
return ...
}
}
Is that so hard? This conforms perfectly to Strict Equivalence. The Employee class guarantees that there is a calculateAnnualBonus method, but it doesn’t guarantee what it will be. There is a protected helper method so that each subclass doesn’t have to repeat the calculation. And you can see that both the Contributor and Manager classes guarantee the exact calculation.
We
chose to provide clients with an Employee class that defined the exact calculation for the annual bonus. Given that we could have written our code another way, why shouldn’t a client assume that we deliberately wanted them to depend on the calculation? Why shouldn’t a client infer that the annual bonus calculation is part of the definition of what it means to be an employee?
Theories X, Y, and Law EnforcementIn Management Science, a
Theory X manager believes “workers need to be closely supervised and comprehensive systems of controls developed.” A
Theory Y manager “will try to remove the barriers that prevent workers from fully actualizing themselves.”
Some languages provide tools for enforcing some of the semantics of Liskov Equivalence. I personally have mixed feelings about this. I actually like this idea of the compiler noticing that I am trying to call the non-existent annual_bonus method of an Employee object. And enforcing Strict Equivalence would be terrific.
But what I don’t like is actually not the language’s fault. I don’t like it when I have to deal with a code base where people assume that as long as they are following the language’s rules, the result must be well-designed OO software. So net-net, I like languages providing mechanisms for enforcing obvious things about inheritance. But philosophically, I want to
seek what the language designers sought and design to the principle of the law, not the letter of the law.
Let’s talk about Java first. Java provides two tools—the abstract and final keywords—for partially enforcing Strict Equivalence. As shown above, an abstract method lets us declare that all Employees have a calculate_annual_bonus method without making any guarantee as to what it will return. A final method lets us declare that its calculation is invariant. If we write:
class Employee {
final public Money calculateAnnualBonus() {
// calculation...
}
}
Then we know that all Employees have the same calculation. It is illegal to implement a different calculation for the Manager subclass. And by extension, if we fail to mark a method final, we know that its implementation is
not guaranteed. A non-final method in a non-final Java class is unreliable, it’s just like functionality without a test in a test-infected program. It is
not part of a class’ contract with its clients.
I conjecture that this is the basis of the suggestion that
methods should be final by default. If a class is not final and it has a non-final method, you can override it and break Strict Equivalence. So with Java, if you want a method’s behaviour to be part of a class’ contract with its clients, you have to make it final.
There is a shortcoming to this system. It
is possible to override a method without breaking Strict Equivalence.
The limitation of using the final keyword (or whatever tool your compiler gives you to prevent overriding) is that it prohibits a very large class of specializations that are useful and do not violate Strict Equivalence. It is as if the Police Chief decided to fight crime by imposing a curfew on the entire town.
It is not that
all ways of overriding a method break Strict Equivalence. Let’s go back to our employees for a moment:
class Employee
attr_accessor :manager
def something_serious
# ... just do it
end
end
class ProbationaryEmployee < Employee
def something_serious
returning(super) do
self.manager.notify(self, 'did something serious')
end
end
end
This is a little contrived, but the point here is that although a ProbationaryEmployee overrides the something_serious method, it does so in a way that is presumably indistinguishable from the perspective of client code. It does something serious and returns the same result, but there is an extra side-effect that matters to probationary employees and their managers.
This conforms to Strict Equivalence because to a client method, the results are identical. A ProbationaryEmployee will pass the same tests and you can insert assertions as you see fit.
The only problem is, how do we know that ProbationaryEmployee adheres to Strict Equivalence and Manager does not? How can programmers write code like something_serious? Just writing any old method, calling super, and hoping that you haven’t broken the Employee class is rather like tossing salt over your shoulder to ward off evil spirits. And never overriding Employee methods is rather drastic. What to do?
When the going gets tough, the tough redecorateSolving problems like this is a lot of the motivation behind Aspect-Oriented Programming systems like Lisp’s
Flavors & CLOS, the
Decorator Pattern, and method chaining in Rails.
The principle is that a subclass doesn’t really override a superclass’ method, it
extends its functionality. Meaning, it typically does something before it, something after it, or something around it. It
decorates the method, it doesn’t replace it.
In the example above, it is not really obvious that we are decorating the Employee#something_serious method. That happens to be a side-effect of what we are doing, but it is not obvious at all. Compare and contrast with a little easy AOP magic based on Rails’ object life-cycle style of coding:
class ProbationaryEmployee < Employee
include EasyAOP
after :something_serious do
self.manager.notify(self, 'did something serious')
end
end
(No, there is no “EasyAOP” module. If you are interested in adding AOP to Ruby, consider using the Aquarium gem).This style of coding makes it obvious that we are attempting to maintain Strict Equivalence. Of course, we can abuse AOP and write advice that breaks our equivalence. 100% enforcement of Strict Equivalence while permitting things like AOP is probably impossible in the compiler. But we can make languages that encourage good programming. I honestly believe that if I have a powerful way to write advice—like CLOS or AOP—I could live with
never overriding methods. Forget methods being final by default, I think I would be happy with methods always being final if I can have subclasses decorate them.
The Art of the Metaobject Protocol is one of the most important books ever written on the subject of building abstractions with objects. The Metaobject Protocol is a system for defining your own object semantics, such as the simple aspects described here. While you’re learning how to make your programs better in your language, you just might pick up a little Common Lisp. Highly recommended.
Ruby, of course, does not enforce anything. Java provides some enforcement with its final keyword, but at a cost: subclasses cannot decorate final methods. On the whole, Java is not worse than Ruby for enforcement, because if you don’t declare a method final, you are no worse off than in Ruby. In Ruby’s favour, you can actually write things like EasyAOP in a few minutes. Better still, you can tweak it to suit your project and its domain. For Java, you probably need to use a heavyweight AOP framework to do the same thing.
1Or maybe you can chuck all of that and use
CLOS. You could do a lot worse.
A Promotion: Manager WAS-AN EmployeeWow, that’s a lot of
flummery over the statement “Manager IS-AN Employee.” As we have seen, there are a lot of guarantees that statement makes, especially if you adopt Strict Equivalence. You may not want to make those guarantees. You may not really
mean that a manager is an employee, you may mean that a manager has a lot in common with an employee. So what to do?
Well, all this trouble started because we said that a Manager IS-AN Employee. What if it isn’t? What we want is a relationship often called WAS-A. Or it’s technical name, implementation inheritance. We want to say that a Manager uses the code of an Employee, but it isn’t to be used where you expect an employee.
In C++, you can use private inheritance. The Manager class would obtain all of an Employee’s members and methods, and it could expose the ones it wanted as part of its own interface:
class Manager : private Employee { ... }
A Manager is not an employee. It is its own thing that happens to behave a lot like an employee. In languages like Java that do not provide a WAS-A construct, you can achieve the same thing with delegation. You can say a Manager HAS-AN Employee. It doesn’t really have one in the sense of a manager having an executive assistant, but it uses an Employee object internally to handle some of its work.
This is very much like a manager who has two ‘hats.’ It works well if you have been careful to separate things like the implementation of database persistence from the semantic hierarchy of public inheritance. In other words, you didn’t decide that an Employee IS-AN ActiveRecord::Base.
In other words… If a Manager is
not an employee in the true sense of the word but is simply
like an employee, we can use object composition to achieve code re-use and communicate to our fellow programmers what managers and employees have in common, without muddying the meaning of IS-A.
What they soughtSo what have we seen? First, that if you adopt Strict Equivalence, standard inheritance carries a great deal of meaning with it. You can infer that the code in a class describes the behaviour of that class and all of its subclasses: The Employee class describes all employees, even probationary employees and managers. Subclasses extend functionality but never break it.
Second, that there are many alternatives to breaking Strict Equivalence, including refactoring the class hierarchy when you need polymorphism and using object composition when you do not. This means that we never
need to use inheritance, we only use it when we choose to make guarantees about equivalence.
In
Programming conventions as signals, I advanced the view that when there was more than one way to do something, each should carry a different meaning. And furthermore, when one way is the obvious way to do it, using the non-obvious way was a signal that there was something non-obvious going on.
This obviously applies to choosing between class inheritance and object composition. Both can be used to express the idea that two classes or objects have something in common. Class inheritance carries heavyweight baggage along with it about the semantics of IS-A. Object composition does not.
Therefore, class inheritance is the more “expensive” of the two to maintain, because it has the most serious implications and constraints on code. Object composition is cheaper because it makes no guarantees, it is a convenience mechanism.
If class inheritance required a pattern and some boilerplate to establish, while object composition was as simple as writing:
delegate :calculate_annual_bonus, :something_serious, :to => @employee_helper
…then there would have been no need for the maxim. Unluckily for us, the popular OO languages make class inheritance
cheap to write in code even though it is expensive to maintain, while object composition is expensive to write in code while being cheap to maintain.
Thus, we need a maxim to remind ourselves that while class inheritance is seductively easy to write, it should be reserved for those cases where it expresses our actual semantic meaning and where imposing its implications and constraints on our code is going to make things cheaper and not more expensive to maintain.
Favor object composition over class inheritance. Now we understand a little of where those six words are heading.
Digression: Test-Driven EquivalenceConsider the requirement that a user of the code be unable to distinguish a subclass from a superclass in a Test-Infected project. You have dozens or even hundreds of tests for each major class. What about all of those unit tests. Should a unit test for the Employee class work when given Manager objects?
Hmmm. If a Manager IS-AN Employee, it follows that a Manager can be substituted for an Employee and the tests will pass. If an Employee test breaks when given a manager, we must pick one of two possibilities: First, it is possible that we were wrong to state that a Manager IS-AN Employee. We should have chosen a different structure for our classes.
In a test-infected project, the meaning of “Manager IS-AN Employee” is external to the Employee class, it is in the tests.
The second possibility is that the test is inappropriate. We might say that the bonus calculation for an employee is uncertain, that the exact amount of the bonus is not part of the ‘spec’ for the Employee class. Therefore, we shouldn’t have a test that validates the amount of an employee’s bonus. I can address this point easily: if we can’t test it, it doesn’t work, therefore if there shouldn’t be a test that works for every employee, then there shouldn’t be code associated with every employee.
Object Design: Roles, Responsibilities, and Collaborations focuses on the practice of designing objects as integral members of a community where each object has specific roles and responsibilities. The authors present the latest practices and techniques of Responsibility-Driven Design and show how you can apply them as you develop modern object-based applications.
So above we stated that according to Strict Equivalence, the code in the Employee class makes a guarantee about all employees. However in a test-infected project, we can take another line. When I say test-infected, I mean specifically that the team has made a commitment to 100% functional coverage in the project, and the team considers the tests to be a form of executable documentation for the code.
If this is the case for a project, it follows that the tests for a class must reflect the contractual obligations of that class to its clients. If we write a test that only passes when an Employee’s annual bonus is such-and-such an amount, that is the amount of bonus for all employees. If we want subclasses of Employee to have their own bonus calculations, we are free to provide a default or base implementation,
but we are prohibited from writing a unit test for it.
You may think “That’s ok, my unit test directly creates an Employee object, it doesn’t create a Manager object, so the test is not making a guarantee about Managers.” If you take that line, you have removed all possibility of using the tests to make guarantees about Employees. You cannot say
anything about objects that are Employees, because your tests don’t apply to all employees. This effectively eliminates the IS-A relationship in your test-infected project, because when I am looking at a Manager class, the fact that it derives from Employee tells me
nothing, because I can’t trust any of the Employee tests to tell me something about Managers.
Therefore, if you believe that an Employee test does not constrain the behaviour of a Manager object, you are required to duplicate all of the Employee tests that apply to Manager instances in Manager tests. You have moved the problem of what is inherited and what is not inherited from the Employee and Manager classes into your test classes.
In a test-infected project, the meaning of “Manager IS-AN Employee” is external to the Employee class, it is in the tests. It also conforms to the Liskov Substitution Principle: it demands that whatever the superclass provides its clients, all of its subclasses must also provide. However, it is more relaxed than Strict Equivalence.
- There are other approaches: Eiffel’s Design by Contract implementation provides preconditions and post-conditions for methods. As long as a subclass conforms to or narrows its superclass’ post-conditions, and conforms to or widens its superclass’ preconditions, it may override a method.
Design by Contract takes the view that the pre-conditions and post-conditions are the contract for a method. Given that understanding of Liskov Equivalence, the language is provides both compiler and run-time enforcement options. Code that adds notification or logging to a superclass’ methods obviously conforms to its post-conditions, so Eiffel permits it without arguing about whether the method should be final or not.
Design by Contract also lies in between Plain and Strict Equivalence: Although the Employee class contains code, you cannot rely on it as a guide to what an Employee does. In Eiffel, you can rely on the post-conditions to be enforced, which are typically looser than the method implementation but tighter than a method signature without conditions.
[back]