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 A is a class and B is a subclass of A, then instances of A should be replaceable with instances of B without altering the correctness of the program.

    • Example: Consider an interface Shape with a draw() method. If Circle and Rectangle are classes implementing Shape, a function designed to work with Shape objects should work correctly with instances of Circle or Rectangle without 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, renderShape can accept any Shape object, and whether it's a Circle or a Rectangle, the draw() method is called correctly, and the program's behavior remains consistent as expected by the Shape interface.

  • 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 Animal class with an eat method that accepts a parameter f of type Foo.

    • Animal has two subclasses, Cat and Mouse, both inheriting the eat method with the same parameter type, Foo.

  • Use Case: All subclasses of animal placed in a generic Animal list should respond to method calls invoked on the list without knowing their specific types.

  • Violation of LSP: If Cat's eat method is modified to take Tuna instead of Foo, substituting a Cat instance for an Animal results 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 allow x >= 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 > 0

    • Condition B: x > -1

    • Conclusion: 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 Number restricts T to Integer, Float, etc.

    • Lower Bound: T super Integer allows 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 ChildFriendlyBookshelf that extends a Bookshelf demonstrating 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, T is replaced with Object, 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.