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