View
154
Download
0
Category
Preview:
Citation preview
Refactoring an unmaintainable mess of nested condi3onals to the Open/Closed principle
-‐ using OO inheritance -‐
-‐ an example -‐
Inheritance is not always evil!
© 2014 Philip Schwarz hHps://twiHer.com/philip_schwarz philip.johann.schwarz@gmail.com
This work is licensed under a Crea3ve Commons AHribu3on-‐NonCommercial-‐NoDerivs 3.0 Unported License.
This talk complements and extends some aspects of my OCP talk:
Downloadable at hHps://github.com/philipschwarz/presenta3ons
Shortly aVer giving the OCP talk I watched the following:
Sandi’s talk complements and extends parts of mine so well
hHp://www.poodr.com/
that I just HAVE TO show you the video!
So this is not your regular talk.
I’ll wrap my slides around her talk • slides at the beginning:
• to give you addi3onal background that helps you follow the video
• to link back to the topics of my other presenta3on
• slides at the end: to recap and elaborate some of Sandi’s key points
What I’ll actually be doing is showing you the video of Sandi’s talk (A talk within a talk!).
The Open-‐Closed Principle
Modules should be both open and closed
Bertrand Meyer Object Oriented SoVware Construc3on
1988
We want modules to be both for extension and for modifica3on
A B C
D
E
A module and its clients
A’ F
G H I
New clients which need A’, an adapted or extended version of A
Typical situa3on where the needs for Open and Closed modules are hard to reconcile
= client of
Analysability Stability Reliability
soluEon
-‐ -‐ -‐
Analysability Stability Reliability
soluEon
-‐ -‐ -‐
With non-‐OO methods, there are only only 2 soluEons available to us, BOTH UNSATISFACTORY
mulEple maintenance problem
Change by Modifica3on
CHANGE
COPY
A B C
D
E
A’ F
G H I
So how can we have modules that are both and ?
How can we keep A and everything in the top part of the figure unchanged, …
…while providing A’ to the boHom clients, and avoiding duplicaEon of soTware?
A B C
D
E
A’ F
G H I
With the OO concept of inheritance
Inheritance allows us to get out of the CHANGE OR COPY dilemma…
…because inheritance allows us to define a new module A' in terms of an exisEng module A, …by staEng only the differences between the two
A’ defines new features, and redefines (i.e. modifies) one or more of A’s features
inherits from
Change by Addi3on
Hacking = Slipshod approach to building and modifying code
Slipshod = Done poorly or too quickly; careless.
The Hacker may seem bad
but oVen his heart is pure.
He sees a useful piece of soVware, which is almost able to address the needs of the moment, more general than the soVware’s original purpose.
Hacker
Spurred by a laudable desire not to redo what can be reused, our hacker starts modifying the original to add provisions for new cases
soluEon
The impulse is good but the effect is oVen to pollute the soVware with many clauses of the form if that_special_case then…
if (<special case D>) then …
if (<special case C>) then …
if (<special case B>) then …
if (<special case A>) then …
switch!
Open-Closed Principle =
One way to describe the OCP and the consequent OO techniques is to think of them as organised hacking
Hacking
The organised form of hacking will enable us to cater to the variants without affecEng the consistency of the original version.
Inheritance
Change by Modifica3on
Change by Addi3on
extends is evil!!!!!
But, using inheritance is no longer the main approach to sa3sfying the OCP
Allen Holub 2004
2003
Using inheritance is s3ll one of the ways of saEsfying the OCP, and was considered THE approach for a long while
Why extends is evil
1988 -‐ 1st ed. 1997 – 2nd ed.
1995
That started changing with the emergence of the design techniques presented in Design Pa\erns
2003
mulEple maintenance problem
Change by Modifica3on
CHANGE soluEon
COPY soluEon
Hacker
Change by Addi3on
OCP soluEon
Chooses
Chooses switch!
extends is evil!!!!!
But…
Sandi deals with a perfect example of the unmaintainable mess that you end up with when you keep extending code by adding condi3onals
switch!
REFACTOR
Sandi deals with the 43-‐line condi3onal by refactoring it so it sa3sfies the Open Closed Principle
switch!
Inheritance
Sandi uses the original approach to sa3sfying the OCP: using OO inheritance So we get a nice example of the technique
inheritance is not evil, and I can tell you exactly when it is safe to use it
switch!
Sandi’s talk also…
looks a liHle bit at why switch creep takes root
acts as an example of refactoring from procedural code to OO code
contains other bits of her design wisdom
Refactoring
A code kata is an exercise in programming which helps a programmer hone their skills through prac3ce and repe33on
A kata is an exercise in karate where you repeat a form many, many 3mes, making liHle improvements in each
The intent behind a code kata is similar
The Gilded Rose Inn
An Inn is where travellers can seek lodging and, usually, food and drink
The Gilded Rose Kata centers around a fic3onal Inn in a game called World of WarcraT
Hi and welcome to team Gilded Rose. As you know, we are a small inn with a prime loca3on in a prominent city ran by a friendly innkeeper named Allison.
hHp://iamnotmyself.com/2011/02/13/refactor-‐this-‐the-‐gilded-‐rose-‐kata/
The Original Gilded Rose Kata
Aged Brie
Sulfuras, Hand of Ragnaros
Backstage Pass to a TAFKAL80ETC concert +5 Dexterity Vest
Elixir of the Mongoose
We also buy and sell only the finest goods. Unfortunately, our goods are constantly degrading in quality as they approach their sell by date. We have a system in place that updates our inventory for us. It was developed by a no-‐nonsense type named Leeroy, who has moved on to new adventures.
Your task is to add the new feature to our system so that we can begin selling a new category of items.
First an introduc3on to our system…
‘sellIn’ denotes the number of days we have to sell the Item
int sellIn = 4;!
All items have a sellIn value
At the end of each day the system lowers both values for every item
for (Item item : items) !{!!!!!!}!
sellIn -= X! quality -= Y!
• Once the sell by date has passed, Quality degrades twice as fast • The Quality of an item is never nega3ve • “Aged Brie” actually increases in Quality the older it gets • The Quality of an item is never more than 50 • “Sulfuras”, being a legendary item, never has to be sold or decreases in Quality • “Backstage passes”, like aged brie, increases in Quality as it’s SellIn value
approaches; Quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but Quality drops to 0 aVer the concert
That’s just the first of several addi3onal ‘rules’ affec3ng items:
It is not hard to imagine developers implemen3ng those requirements one at a 3me, each 3me adding one or more branches to the condi3onal.
switch!
We have recently signed a supplier of conjured items.
Feel free to make any changes to the UpdateQuality method and add any new code as long as everything s3ll works correctly.
However, do not alter the Item class or Items property as those belong to the goblin in the corner who will insta-‐rage and one-‐shot you as he doesn’t believe in shared code ownership
Conjured Item
Conjured Mana Cake
X
This requires an update to our system: “Conjured” items degrade in Quality twice as fast as normal items
Emily Bache
This is designed as a refactoring kata, where you take this less than clean code, and transform it via small steps into something that can be maintained and extended.
Conjured Item
When you have the code under control, it should be easy to add the new feature for “Conjured” items.
Two approaches to this Kata • In the original version of this Kata, there were no tests provided,
only a textual descrip3on of the requirements. • In a later addiEon, I added Text-‐Based (aka Approval) tests.
Emily Bache
So you can do this kata in two ways, • either wriEng your own tests, and prac3ce wri3ng really good ones • or just jump straight to the refactoring part, leaning on the text-‐based tests.
Sandi takes neither of these approaches:
https://github.com/jimweirich/gilded_rose_kata
The original had no tests. Since this is a refactoring kata, I feel the tests are important and provide a fairly complete test suite. Just delete the tests if you wish to "go it alone".
she uses a Ruby version of the kata that already has tests
Sandi also ignores the restric3on we saw earlier: ‘do not alter the Item class or Items property’
In fact she goes further: she doesn’t even look at the explana3on of the problem.
She just takes the code and tries to add the required new func3onality
X
“But I didn’t do that [look at the problem explana3on]. I wanted to treat this problem as if it was a real produc3on problem, and that my only source of informa3on was the tests and the code”
Watch ‘All the LiHle Things’ (just under 40 minutes long)
hHps://www.youtube.com/watch?v=8bZh5LMaSmE hHp://www.confreaks.com/videos/3358-‐railsconf-‐all-‐the-‐liHle-‐things
RailsConf 2014
Ruby on Ales 2014
There are two versions of Sandi’s Talk:
In recapping, I’ll sample from both versions.
Your task is to add the new feature to our system so that we can begin selling a new category of items.
I went and I tried.
I tried really hard, but I failed miserably. I could not do it.
I spent hours trying… I found it impossible
if … !then … !else …!
IMPOSSIBLE
ALL
If it is so hard, if it is impossible to change that if statement,
and I am supposed to be all OOP,
then you have to wonder why I even tried. What made me choose as my strategy, changing that if statement?
Well it is because I felt I was supposed to do it.
And here is what happens, right?
you write some code
someone asks for a change
What do we do? You go looking around the codebase for code that is the closest thing to the thing that you are trying to do. You put the new code there
if … !then … !else …!
if … !then … !else …!
Maybe the first person that wrote it put an if statement in.
And if statements exert gravita3onal pull.
and if that thing already has an if statement, well they just put in another branch on it, right? that’s how it works.
Novices especially, they are afraid to make new objects so the just go put more code in where they can find the thing they are trying to add
So, the natural tendency of code is to grow bigger and bigger and bigger, and there comes a point when it gets big enough that it 3ps
DIS
and it feels like even if you are the kind of person who would normally make a new class,
that you would be doing the past and the future a disservice by pu|ng the code anywhere else.
at that point it is so big that you cannot imagine pu|ng code anywhere else.
if … !then … !else …!
if … !then … !else …!
when you have a 5000 line in an ac3ve record, you do not make a 20 line service object that goes with it when you have a new requirement, you feel like you have to put the code where it is
Service X
Let’s take a 2 minute detour to see a real-‐world example of a condi3onal that has grown well beyond the 3pping point.
If the paHern is a good one then the code gets beHer, and if the paHern is a bad one, we exacerbate the problem.
We have a bargain to follow the paHern.
And so the paHern failed me.
That if statement, it felt like it had evolved, evolu3onarily,
if … !then … !else …!
if … !then … !else …!
and that any change I made I was supposed to make there
But fortunately I couldn’t do it.
And so I decided I would make a new paHern:
I am not going to try to add Conjured, I am going to refactor the code so that it is simpler and so that I can then add Conjured.
Conjured Item
Despite the fact that people tell you ‘never ever ever use inheritance’…
inheritance is not evil!!!!!
X
1. The hierarchy is shallow and narrow, it is not deep and wide
If those condi3ons are all true for the thing that you are doing
I will give you dispensa3on to use it in a very specific case
2. The subclasses use every bit of code that is in the superclass 3. If you draw a mental image of your object graph, this li\le hierarchical cluster is at the edge of the graph (it is on a leaf node of your object hierarchy)
then it is hard to find another more intenEon-‐revealing way to write this code.
1. The hierarchy is shallow and narrow, it is not deep and wide 2. The subclasses use every bit of code that is in the superclass
Let’s now look at how Sandi elaborates on her first two conditions
In her book: POODR
1. The hierarchy is shallow and narrow, it is not deep and wide
A hierarchy’s shape is defined by its overall breadth and depth
It is this shape that determines ease of use, maintenance, and extension.
Here are a few of the possible variaEons of shape:
Easy to understand Slightly more complicated.
A bit more challenging
Have a tendency to get wider
Difficult to understand and costly to maintain
SHOULD BE AVOIDED
The problems with deep hierarchies
They have a very long search path for message resolu3on
They provide numerous opportuni3es for objects in that path to add behavior as the message passes by.
Objects depend on everything above them, so a deep hierarchy has a large set of built-‐in dependencies, each of which might someday change.
Programmers tend to be familiar with just the classes at their tops and boHoms;
* They tend to understand only the behavior implemented at the boundaries of the search path
* The classes in the middle get short shriT.
* Changes to these vaguely understood middle classes stand a greater chance of introducing errors.
2. “The subclasses use every bit of code that is in the superclass”
What does she mean? Let’s look at how she elaborates this in POODR
Subclasses are specializaEons of their superclasses.
A MountainBike should be everything a Bicycle is, plus more.
Any object that expects a Bicycle should be able to interact with a MountainBike in blissful ignorance of its actual class.
These are the rules of inheritance; break them at your peril.
For inheritance to work…the objects that you are modeling must truly have a generalizaEon–specializaEon rela3onship.
[When] subclasses…are not truly specializaEons of their superclasses, the hierarchy becomes untrustworthy.
IS-‐A __
trust
Untrustworthy hierarchies force objects that interact with them to know their quirks
trust
Inexperienced programmers do not understand and cannot fix a faulty hierarchy
Knowledge of the structure of the hierarchy leaks into the rest of the applica3on, creaEng dependencies that raise the cost of change.
if (bicycle instanceof MountainBike) {
// do XYZ }
if (bicycle instanceof MountainBike) {
// do XYZ }
if (bicycle instanceof MountainBike) {
// code that knows about }
oTen by explicitly checking the classes of objects.
when asked to use one they will embed knowledge of its quirks into their own code,
All of the code in an abstract superclass should apply to every class that inherits it
Superclasses should not contain code that applies to some, but not all, subclasses.
======= ======= =======
======= =======
======= =======
======= ======= =======
======= ======= =======
======= ======= =======
When interac3ng with these awkward objects, programmers are forced to know their quirks and into dependencies that are beHer avoided.
Faulty abstracEons cause inheri3ng objects to contain incorrect behavior; aHempts to work around this erroneous behavior will cause your code to decay.
Subclasses that fail to honor their contract are difficult to use. They’re “special”and cannot be freely subsEtuted for their superclasses. These subclasses are declaring that they are not really a kind-‐of their superclass and cast doubt on the correctness of the en3re hierarchy.
They are not permi\ed to do anything that forces others to check their type in order to know how to treat them or what to expect of them.
if (bicycle instanceof MountainBike) {
// code that knows }
trust
IS-‐A __
Let’s look in more detail at the LSP to beHer understand what Sandi means by contract, and subsEtutability
When you honor the contract, you are following the Liskov SubsEtuEon Principle, which is named for its creator, Barbara Liskov, and supplies the “L” in the SOLID design principles
Barbara Liskov
The Liskov SubsEtuEon Principle (LSP) -‐ 1988
Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
[in a Type hierarchy] the supertype’s behavior must be supported by the subtypes: subtype objects can be subsEtuted for supertype objects without affecEng the behavior of the using code.
Behaviour of -‐-‐-‐-‐>
supported by -‐-‐-‐-‐>
2000
[the LSP] allows using code to be wri\en in terms of the supertype specificaEon, yet work correctly when using objects of the subtype.
For example, code can be wriHen in terms of the Reader type, yet work correctly when using a BufferedFileReader.
private void foo(BufferedReader bufferedReader) throws IOException { … bar(bufferedReader); … } private void bar(Reader reader) throws IOException { … System.out.println( reader.read() ); … }
the subsEtuEon principle requires that the subtype specifica3on support reasoning based on the supertype specifica3on. Three proper3es must be supported: 1. Signature Rule 2. Methods Rule 3. ProperEes Rule
The subtype objects must have all the methods of the supertype, and the signatures of the subtype methods must be compaEble with the signatures of the corresponding supertype methods.
This rule is enforced by the compiler, so we are all familiar with it.
#1 The Signature Rule
1. Signature Rule 2. Methods Rule 3. ProperEes Rule
Before we can cover the other two rules we must briefly look at Design by Contract
The other two rules cannot be checked by a compiler because they require reasoning about the meaning of specifica3ons
Viewing the rela3onship between a class and its clients as a formal agreement , expressing each party’s rights and obligaEons
DbC uses precondiEons, postcondiEons and class invariants to document (or beHer, programma3cally assert) the contract between classes, methods and their callers.
1986
Bertrand Meyer
Design by Contract (DbC)
DbC
Condi3ons that must always be true of a class. They are implicitly added to the precondiEons and postcondiEons of every method.
DbC Object Oriented SoTware ConstrucEon
Condi3ons that must be true before a method can execute. if the condi3ons are not met, it is a bug in the client.
Condi3ons that must be true when a method is finished execuEng. if the condi3ons are not met, it is a bug in the method.
https://code.google.com/p/cofoja/wiki/AddContracts
Open-‐source library released by Google to bring DbC to Java
#2 The Methods Rule Calls of these subtype methods must “behave like” calls to the corresponding supertype methods.
The subtype objects must have all the methods of the supertype, and the signatures of the subtype methods must be compaEble with the signatures of the corresponding supertype methods.
#1 The Signature Rule
What does Liskov mean by “behave like”?
Let’s look at Meillir-‐Jones’ explana3on, in terms of precondi3ons and postcondi3ons
• Every opera3on’s precondiEon is no stronger than the corresponding opera3on in the superclass
• Every opera3on’s postcondiEon is at least as strong as the corresponding opera3on in the superclass
a subtype method can • expect the same or less • promise the same or more
Explana3on of “methods must ‘behave like’ calls to the corresponding supertype methods”
Meillir-‐Jones
A subtype method can weaken the precondiEon and can strengthen the postcondiEon
1999
#2 The Methods Rule Calls of these subtype methods must “behave like” calls to the corresponding supertype methods.
The subtype objects must have all the methods of the supertype, and the signatures of the subtype methods must be compaEble with the signatures of the corresponding supertype methods.
#1 The Signature Rule
The subtype must preserve all properEes that can be proved about supertype objects.
#3 The ProperEes Rule
The class invariant of a subclass must be equal to or stronger than that of its superclass
An Example of a subclass viola3ng the contract of its superclass
Robert MarEn (aka Uncle Bob) Agile SoTware Development Principles, Pa\erns and PracEces
2002
public class Rectangle {
private Point topLeft;
private double width; private double height;
public void setWidth(double width) {
this.width = width;
} public double getWidth() {
return width; }
public void setHeight(double height)
{
this.height = height; }
public double getHeight() {
return height; }
…
}
Imagine that this applica3on works well and is installed in many sites.
One day, the users demand the ability to manipulate squares in addi3on to rectangles.
It is oVen said that inheritance is the IS-‐A relaEonship.
In other words, if a new kind of object can be said to fulfill the IS-‐A relaEonship with an old kind of object, the class of the new object should be derived from the class of the old object.
public class Rectangle!{!
!…!
!!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
! public void setHeight(double height) {
this.height = height; }
…!
}!For all normal intents and purposes, a square is a rectangle.
So it is logical to view the Square class as being derived from the Rectangle class. IS-‐A
!public class Square extends Rectangle!{ !!}!
public class Rectangle!{!
!…!
@Ensures({“getWidth() = width”})!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
!@Ensures({“getHeight() = height”})! public void setHeight(double height) {
this.height = height; }
…!
}!
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{ !!}!
A square has the invariant that its width and height are idenEcal, but Square inherits Rectangle’s seHers, which do not preserve the invariant.
public void f(Square square) {
square.setWidth(5); assert square.getHeight() == 5;
}
There is a problem though
The asserEon fails
To see why, let’s make the contract explicit
public class Rectangle!{!
!…!
@Ensures({“getWidth() = width”})!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
!@Ensures({“getHeight() = height”})! public void setHeight(double height) {
this.height = height; }
…!
}! We can sidestep the problem by overriding the seHers.
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
Now Square and Rectangle appear to work. No maHer what you do to a Square object, it will remain consistent with a mathema3cal square, and regardless of what you do to a Rectangle object, it will remain a mathema3cal rectangle.
public class Rectangle!{!
!…!
@Ensures({“getWidth() = width”})!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
!@Ensures({“getHeight() = height”})! public void setHeight(double height) {
this.height = height; }
…!
}!
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
The asserEon now passes. The invariant of the Square is now sa3sfied.
public void f(Square square) {
square.setWidth(5); assert square.getHeight() == 5;
}
Moreover, you can pass a Square into a func3on that accepts a Rectangle, and the Square will s3ll act like a square and will remain consistent.
public class Rectangle!{!
!…!
@Ensures({“getWidth() = width”})!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
!@Ensures({“getHeight() = height”})! public void setHeight(double height) {
this.height = height; }
…!
}!
So we might conclude that the design is now self-‐consistent and correct.
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
However, this conclusion would be amiss. A design that is self-‐consistent is not necessarily consistent with all its users!
public class Rectangle!{!
!…!
@Ensures({“getWidth() = width”})!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
!@Ensures({“getHeight() = height”})! public void setHeight(double height) {
this.height = height; }
…!
}!
public void g(Rectangle rectangle) {
rectangle.setWidth(5); rectangle.setHeight(4);
assert rectangle.getArea() == 20;
}
Consider a client’s func3on g:
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
public class Rectangle!{!
!…!
@Ensures({“getWidth() = width”})!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
!@Ensures({“getHeight() = height”})! public void setHeight(double height) {
this.height = height; }
…!
}!
public void g(Rectangle rectangle) {
rectangle.setWidth(5); rectangle.setHeight(4);
assert rectangle.getArea() == 20;
}
This func3on sets the width and height of what it believes to be a Rectangle.
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
The func3on works fine for a Rectangle but fails if passed a Square. Problem: The author of g assumed that changing the width of a Rectangle leaves its height unchanged.
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
public class Rectangle!{!
!…!
@Ensures({“getWidth() = width”})!!public void setWidth(double width)!!{ !! !this.width = width; !!} !!!…!
!@Ensures({“getHeight() = height”})! public void setHeight(double height) {
this.height = height; }
…!
}!
Clearly, it is reasonable to assume that changing the width of a rectangle does not affect its height! In fact it is so obviously right that we didn’t even bother explicitly adding it as a postcondi3on of Rectangle’s seHers, we leV it implicit!
Let’s make it explicit.
public class Rectangle!{!
!…!
@Ensures({! “getWidth() = width! getHeight() = old(getHeight())”})!!public void setWidth(double width)!!{ !! !this.width = width; !
!} !!!…!!@Ensures({!
“getHeight() = height! getWidth() = old(getWidth())”})!
public void setHeight(double height)
{
this.height = height; }
!}!
Clearly, it is reasonable to assume that changing the width of a rectangle does not affect its height!
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
In fact it is so obviously right that we didn’t even bother explicitly adding it as a postcondi3on of Rectangle’s seHers, we leV it implicit!
Let’s make it explicit.
public class Rectangle!{!
!…!
@Ensures({! “getWidth() = width”! getHeight() = old(getHeight())”})!!public void setWidth(double width)!!{ !! !this.width = width; !
!} !!!…!!@Ensures({!
“getHeight() = height”! getWidth() = old(getWidth())”})!
public void setHeight(double height)
{
this.height = height; }
!}!
@Invariant(“getWidth() = getHeight()”)!public class Square extends Rectangle!{! @Ensures({“getWidth() = width”})!
!public void setWidth(double width)!!{ !! !this.width = width; !! !this.height = width; !!} !!
! @Ensures({“getHeight() = height”})!
public void setHeight(double height) { this.height = height; ! !this.width = height; } !!
}!
Clearly, the postcondi3on of Square’s setWidth is weaker than the postcondi3on of Rectangle’s setWidth, since it does not enforce the constraint (getHeight() = old(getHeight()). Similarly for Square’s setHeight.
Square’s setWidth and setHeight methods violate the contract of the Rectangle base class.
• Every opera3on’s precondiEon is no stronger than the corresponding opera3on in the superclass
• Every opera3on’s postcondiEon is at least as strong as the corresponding opera3on in the superclass
“methods must ‘behave like’ calls to the corresponding supertype methods”
#2 The Methods Rule Square’s se\ers don’t behave like Rectangle’s Se\ers!
The postcondiEons of Square’s se\ers are weaker than those of Rectangle’s se\ers!
1. Signature Rule 2. Methods Rule 3. ProperEes Rule
Square Violates Rectangle’s contract
It is oVen said that inheritance is the IS-‐A rela3onship.
e.g. a square is a rectangle, therefore Square should be derived from the Rectangle.
ISA
If a new kind of object can be said to fulfill the IS-‐A rela3onship with an old kind of object, the class of the new object should be derived from the class of the old object.
Flawed reasoning – recap
But IS-‐A is about behaviour.
In OOD, a square IS-‐A rectangle only if it behaves like a rectangle, if it honours Rectangle’s contract, if it sa3sfies the LSP.
The contract may be implicit (not so good).
A square is a rectangle, but only in a geometrical sense, not in a behavioural sense.
In OOD, Square violates Rectangle’s contract, it violates the LSP, therefore it does not behave like a Rectangle: it is not true that a Square IS-‐A Rectangle and therefore Square should not inherit from Rectangle.
IS-‐A __
X X
It may be expressed in comments (beHer). It may be expressed as automated unit tests (much beHer). Or it may be expressed using a DbC framework (rare?).
To adhere to LSP in Java, we must make sure that developers define precondi3ons and postcondi3ons for each of the methods on an abstract class.
X In order to take advantage of LSP, we must adhere to OCP because violaEons of LSP also are violaEons of OCP, but not vice versa.
X X When defining our subclasses, we must adhere to these precondi3ons and postcondi3ons. If we do not define precondiEons and postcondiEons for our methods, it becomes virtually impossible to find violaEons of LSP.
Kirk Knoernschild
The Liskov SubsEtuEon Principle is one of the prime enablers of OCP.
We can think of the Liskov Subs3tu3on Principle (LSP) as an extension to OCP.
2002
When interac3ng with these awkward objects, programmers are forced to know their quirks and into dependencies that are beHer avoided.
when asked to use one they will embed knowledge of its quirks into their own code
if (bicycle instanceof MountainBike) {
// do XYZ }
if (bicycle instanceof MountainBike) {
// do XYZ }
if (bicycle instanceof MountainBike) {
// code that knows about }
oVen by explicitly checking the classes of objects.
Recap: Sandi on consequences of untrustworthy Hierarchies
every viola3on of the LSP is a latent viola3on of the OCP X X
because in order to repair the damage … we are going to have to add if statements and hang dependencies upon subtypes
if (bicycle instanceof MountainBike) {
// do XYZ }
if (bicycle instanceof MountainBike) {
// do XYZ }
if (bicycle instanceof MountainBike) {
// code that knows about }
Uncle Bob on violaEons of LSP
Next Eme you are faced with adding logic to a non-‐trivial condiEonal, don’t just follow the pa\ern and add another branch.
See if the condi,onal is one of those that can be refactored to the OCP, using inheritance if necessary.
Next Eme you consider introducing an inheritance hierarchy, ask yourself if it meets Sandi’s criteria for using inheritance.
If not, consider the consequences of going ahead (if you decide to do so).
Next Eme you create a base class, spend some Eme thinking about how its contract needs to be expressed.
Is it really acceptable to leave it implicit? Is it so obvious?
Is it workable and effecEve to express the contract using unit tests? If not, maybe explore the possibility of using a DbC framework.
If not, express the contract using unit tests.
Next Eme you create a derived class, verify that it saEsfies the contract of its superclass.
Next Eme you come across instanceof usages, see if they are the smell of an untrustworthy hierarchy.
If it doesn’t, take remedial ac,on.
If so, fix the hierarchy.
References All images sourced from hHp://www.google.co.uk/advanced_image_search, so see there for details of which are subject to copyright PracEcal Object-‐Oriented Design in Ruby: An Agile Primer – by Sandi Metz | Publica3on Date: September 15, 2012 | ISBN-‐10: 0321721330 | ISBN-‐13: 978-‐0321721334 Agile SoTware Development, Principles, Pa\erns, and PracEces – by Robert C. MarEn | Publica3on Date: October 25, 2002 | ISBN-‐10: 0135974445 | ISBN-‐13: 978-‐0135974445 Object-‐Oriented SoTware ConstrucEon – by Bertrand Meyer | Publica3on Date: 3 April 1997 | ISBN-‐10: 0136291554 | ISBN-‐13: 978-‐0136291558 | Edi3on: 2 Program Development in Java – AbstracEon, SpecificaEon and OO Design – by Barbara Liskov, with John Gu\ag | Publica3on Date: 6 Jun 2000 | ISBN-‐10: 0201657686 | ISBN-‐13: 978-‐0201657685
Java Design: Objects, UML, and Process – by Kirk Knoernschild | Publica3on Date: December 18, 2001 | ISBN-‐10: 0201750449 | ISBN-‐13: 978-‐0201750447 The Coding Dojo Handbook -‐ a pracEcal guide to creaEng a space where good programmers can become great programmers – by Emily Bache | Publica3on Date: 29 October 2013 | hHps://leanpub.com/codingdojohandbook Fundamentals of Object Oriented Design in UML – by Meilir Page-‐Jones | Publica3on Date: November 13, 1999 | ISBN-‐13: 978-‐0201699463 ISBN-‐10: 020169946X The AnE-‐IF Campaign -‐ hHp://an3ifcampaign.com/ The Gilded Rose Kata -‐ hHp://iamnotmyself.com/2011/02/13/refactor-‐this-‐the-‐gilded-‐rose-‐kata/
References (con3nued)
Recommended