If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
Uncle Bob's interpretation:
Subtypes must be substitutable for their base types.
Head First Object-Oriented Analysis and Design's interpretation:
"When you inherit from a base class, you must be able to substitute your subclass for that base class without things going terribly wrong. Otherwise, you've used inheritance incorrectly!"
If none of those interpretation makes sense, read on.
I will try to explain this using Java. But before I explain what the principle means, let's define what subtype is. A is a subtype of B if it implements or extends B. For example, ArrayList is a subtype of List because ArrayList implements List. List is a subtype Collection because it List extends Collection. Because this relationship is transitive, ArrayList is also a subtype of Collection.
Now we know what I mean when I say subtype, let's try to understand the principle. Let's say we have the following code is Java:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void removeAll(Collection<String> list, String item){ | |
while(list.contains(item)){ | |
list.remove(item); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void removeAll(List<String> list, String item){ | |
while(list.contains(item)){ | |
list.remove(item); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void removeAll(Set<String> list, String item){ | |
while(list.contains(item)){ | |
list.remove(item); | |
} | |
} |
Be careful because this is a one way street. List can be substituted Collection but Collection may not be substitutable for List because List is a child of collection.
Photo by Derick Bailey
- When the subtype Class removes functionality from the base class.
- When methods that reference the base class knows about its subtype.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void removeAll(List<String> list, String item){ | |
while(list.contains(item)){ | |
list.remove(item); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void testWithUnmodifiableList(){ | |
String item = "orange"; | |
String[] arrays = new String[2]; | |
arrays[0] = item; | |
arrays[1] = item; | |
List<String> list = Arrays.asList(arrays); | |
removeAll(list, item); | |
Assert.assertFalse(list.contains(item)); | |
} |
(Note: For the sake of simplicity, I ignored that the List interface implements Collection. But you get the idea.)
This way, our method can be changed to:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void removeAll(ModifiableList<String> list, String item){ | |
while(list.contains(item)){ | |
list.remove(item); | |
} | |
} |
The second way to violate this principle is having your method knowing details about the subclass.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public int calculateArea(Shape shape){ | |
int area = shape.getWith() * shape.getHeight(); | |
if (shape instanceof Triangle){ | |
area = area/2; | |
} | |
return area; | |
} |
The calculateArea violates the Liskov Substitution Principle because it knows about the subtype of Shape. This makes it hard for the method to close for modification. What happens when you add a circle, now you have to add another if statement to check if it's a circle. To fix this, we can move the calculateArea() to the Shape class. Then each Shape can implements its calculateArea() so we don't have to check to see what type it is.
This is another way you can violate the principle:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public String getLast(List<String> list){ | |
LinkedList<String> linkedList = (LinkedList) list; | |
return linkedList.getLast(); | |
} |
And that's Liskov Substitution Principle. If you are removing functionality, then you want to ask, does this subtype belong to this base class? Or if you find yourself downcasting like I did with the LinkedList, then you want to replace the base type with the subtype. Or if you find yourself checking for case type, then you should ask, does this method belongs in the subtype class? Doing any of these things is an indication that you may be violating the Liskov Substitution Principle.
Resource:
0 comments:
Post a Comment