Graduate Program KB

Clean Architecture

Chapter 9 - LSP: The Liskov Substitution Principle

  • The principal states "build software systems from interchangeable parts, those parts must adhere to a contract that allows those parts to be substituted one for another"
  • LSP describes a way of defining subtypes
    • Given for each object o1 of type S, there is an object o2 of type T
    • And for all programs P defined in terms of T
    • If behaviour of P is unchanged when o1 is substituted for o2, it implies S is a subtype of T
  • If you have a subtype S which extends or inherits from type T, then S can be used wherever T is expected without changing expected behaviour

Guiding the Use of Inheritance

  • In Figure 9.1, Billing does not depend on any of the concrete implementations of License
    • Behaviour of calcFee() in the program works regardless of the subtype used, conforming to LSP

The Square / Rectangle Problem

  • Figure 9.2 is an example of violating LSP
    • Square is not a proper subtype of Rectangle, its dimensions should mutate together rather than individually
    • The user could be confused by its behaviour as they believe to be interacting with a rectangular-like object
  • In this case, preventing LSP violation requires adding conditional functionality to the User to deal with squares
    • But now, the User behaviour depends on the types used, violating LSP

LSP and Architecture

  • LSP was initially used to implement inheritance, but has expanded to apply to interfaces as well
  • Interfaces define a contract which concrete implementations must adhere too
    • Therefore, these implementations are substitutable because you can ensure functionalities exists
    • By relying on interfaces, it promotes a decoupled design between components for maintainability and extendibility
    • Ex. Using different types of repositories (in-memory, mongoDB, etc.)

Example LSP Violation

  • Consider a service which customers can find a taxi driver, selecting from a variety of taxi services regardless of company
  • Each driver has a URI retrieved from the database, then information is appended to it when the driver is dispatched
    • Base URI for a driver: purplecab.com/driver/name
    • URI with dispatch information: purplecab.com/driver/name/pickupAddress/addressA/pickupTime/timeA/destination/placeA
  • The dispatch URI is specific, and with multiple different taxi companies being considered, they must all conform to the same interface for setting fields
  • If any taxi company deviates from the specification, a special case would need to be added to our dispatch request functionality
    • Similar to the rectangle-square scenario previously, where we need to explicitly add a case for dealing with squares
    • Hardcoding specific cases can cause:
      • Security concerns: Exposing sensitive information, inconsistent validation
      • Ambiguous errors: Harder to maintain multiple URIs, risk of introducing inconsistency / errors
      • Requires the architect to add an unnecessary mechanism that adds complexity
    • Ex. For company XYZ
      let URI = 'purplecab.com/driver/name/pickupAddress/addressA/pickupTime/timeA/destination/placeA'
      
      if (taxiCompany === 'XYZ') {
          URI = 'purplecab.com/driver/name/pickupAddr/addressA/time/timeA/dest/placeA'
      }
      

Conclusion

  • LSP at the module level focuses more on the organisation of classes and functions within a module / package
  • LSP should be extended to the architectural level, focusing on the design of the entire system to make it scalable and adaptable
  • As seen before, violation of LSP causes increased complexity within components