SOLID Principles
Introduction
The session centered on the Solid Principles of object-oriented design, focusing primarily on the Liskov Substitution Principle (LSP).
The discussion began with a brief recap of prior sessions covering Single Responsibility Principle and Open/Closed Principle.
Notably, the guest lecturer was introduced, Emily Bennett, who works at Google.
Liskov Substitution Principle (LSP)
Definition: The Liskov Substitution Principle states that all subclasses should be substitutable for their parent classes.
Implication: If
Ais a class andBis a subclass ofA, then instances ofAshould be replaceable with instances ofBwithout altering the correctness of the program.Example: Consider an
interface Shapewith adraw()method. IfCircleandRectangleare classes implementingShape, a function designed to work withShapeobjects should work correctly with instances ofCircleorRectanglewithout needing to know their specific types:
interface Shape { void draw(); } class Circle implements Shape { @Override public void draw() { System.out.println("Drawing a Circle"); } } class Rectangle implements Shape { @Override public void draw() { System.out.println("Drawing a Rectangle"); } } public void renderShape(Shape s) { s.draw(); // This will correctly call Circle's draw or Rectangle's draw }In this example,
renderShapecan accept anyShapeobject, and whether it's aCircleor aRectangle, thedraw()method is called correctly, and the program's behavior remains consistent as expected by theShapeinterface.Origin: Named after Barbara Liskov
Liskov is known for her work on subtyping relationships and contributed to a programming language called Clue.
Example of LSP
Consider an
Animalclass with aneatmethod that accepts a parameterfof typeFoo.Animalhas two subclasses,CatandMouse, both inheriting theeatmethod with the same parameter type,Foo.
Use Case: All subclasses of animal placed in a generic
Animallist should respond to method calls invoked on the list without knowing their specific types.Violation of LSP: If
Cat'seatmethod is modified to takeTunainstead ofFoo, substituting aCatinstance for anAnimalresults in an invalid operation.Example Code:
Animal animal = new Animal(); Cat cat = new Cat(); animal.eat(new Tuna()); // Invalid if eat in Cat requires Tuna
Importance: The principle is crucial in situations where we don't want to be aware of specific subclass implementations.
Preconditions and Postconditions
In designing class methods, developers must consider preconditions and postconditions to maintain LSP compliance.
Preconditions: Conditions that must be true before a method is executed.
Postconditions: Conditions that guarantee certain truths following a method's execution.
Rules for Subclasses:
Postconditions: Must remain the same or be more restrictive in subclasses.
Example: If a method guarantees that its return value will be in a certain range, overrides must ensure similar or stricter guarantees.
Preconditions: Can be weakened but cannot be made more strict.
Example: If a parent class method requires input
x > 0, a child class may allowx >= 0, accommodating broader input.
Violation of LSP arises if these rules are not followed, affecting method expectations.
Predicate Strength
The concept of predicate strength categorizes conditions based on their restrictiveness.
A stronger condition entails more limitations on input, while a weaker condition allows for broader latitude.
Example Comparison:
Condition A:
x > 0Condition B:
x > -1Conclusion: B is weaker than A, as A's limits are more strict.
Implications of Predicate Strength in Code
Practical exercises on determining stronger and weaker conditions illustrated the principle.
Generics and Liskov Substitution Principle
Transitioning from discussing LSP to generics facilitated understanding of subclassing relationships.
Generics allow the design of classes that can operate on typed objects without losing type information, integrating LSP in a more flexible setting.
Example:
class Holder<T>specifies that the holder can be parameterized with any type, enabling type safety without redundancy.
Bound Generics
Bounded Generics: Allow specifying constraints on the types that can be used as parameters.
Upper Bound:
T extends NumberrestrictsTto Integer, Float, etc.Lower Bound:
T super Integerallows for Integer and any superclass of Integer.
Inheritance and Generics
Discussed how inheritance works with generics, showcasing how a subclass can extend a generic class:
E.g.,
class Child<T> extends Parent<T>: keeps the type while allowing extensions.
Example with a
ChildFriendlyBookshelfthat extends aBookshelfdemonstrating bounded generics to ensure only children's books are added, thereby adhering to LSP.
Wildcards in Generics
Definition: A wildcard (
?) is an unnamed type parameter that allows flexibility in generics for cases where the specific type does not matter.You can use upper and lower bounds with wildcards to set limitations on what types can be used.
Extends Wildcard: Accepts a type or its subtypes (
List<? extends Animal>).Super Wildcard: Accepts a type or its supertypes (
List<? super Dog>).
Type Erasure
Concept of type erasure explains that generic type information is removed at compile time for backward compatibility.
Result: When a class with generics is compiled,
Tis replaced withObject, and bounded types are replaced with their upper bounds.
Implications: Developers can't rely on runtime type information for generics, leading to several language restrictions (e.g., no primitive types in generics).
Conclusion of Generics Lecture
Final thoughts from the lecture advocate for using generics for enhancing maintainability and safety, especially in data collection classes.
Emphasized that while generics can add complexity, they can significantly improve code clarity if used judiciously.
Questions and Industry Insights
After the technical lecture, Emily Bennett addressed questions regarding industry experiences, team sizes, and programming languages used in practice at Google.