Skip to Content
Software EngineeringSolid principle

Solid principle

  • SOLID is a mnemonic for five design principles intended to make software designs more understandable, flexible and maintainable.
  • The cost of applying these principles into a program’s architecture might be making it more complicated than it should be.
  • Striving for these principles is good, but always try to be pragmatic and don’t take everything written here as dogma.

Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should handle a single aspect of the software’s functionality. This principle aims to encapsulate this responsibility within the class.

Purpose: The primary goal is to simplify complexity. For small programs, a clean, straightforward design with well-structured methods is sufficient. However, as the software scales, maintaining simplicity becomes crucial.

Challenges in Scalable Systems:

  • Growing Complexity: As a program expands, classes may become overly large and difficult to manage. It becomes challenging to recall specific functionalities or navigate the code efficiently.
  • Maintenance Difficulties: If a class is responsible for multiple functionalities, any change in one could necessitate modifications to the class. This increases the risk of inadvertently affecting other functionalities, potentially introducing bugs.

Example

Consider a class designed to handle market data:

  • Single Responsibility: The MarketDataProcessor class is responsible solely for processing incoming market data. This includes parsing data feeds and validating the integrity of the data.
  • Encapsulation: All functionalities related to market data handling are encapsulated in this class. No trading logic or connectivity management is included.
  • Benefits: By isolating market data processing in one class, changes to data parsing or validation routines do not impact other system components, such as trading algorithms or order management systems.

Contrast with a Non-SRP Approach:

  • If the MarketDataProcessor also handled trade executions and connection management, any modifications to market data specifications or a change in the trading strategy might require changes to this class. This could lead to errors in trade execution logic or connection handling, areas unrelated to the primary functionality of processing market data.

Open/Closed Principle (OCP)

Definition: The Open/Closed Principle states that classes should be open for extension but closed for modification. This means a class should support the extension of its behavior without modifying its existing code.

Purpose: The principle aims to protect existing code from becoming unstable due to modifications when new features are added. This ensures that once a class is tested and deployed, its basic functionality remains unchanged, reducing the risk of introducing new bugs.

Implementation Strategy:

  • Open for Extension: You can extend the functionality of a class by inheriting from it and adding or overriding behaviors as needed without altering the original class’s source code.
  • Closed for Modification: The original class should not be altered once it has been finalized, tested, and used in production. Its interface should remain consistent and reliable for other parts of the application.

Example

Consider a high-frequency trading system with a base class called OrderExecutionEngine:

  • Base Functionality: The OrderExecutionEngine handles order executions based on predefined strategies.
  • Extensibility: To accommodate new trading strategies, developers can create subclasses like AggressiveOrderExecutionEngine or ConservativeOrderExecutionEngine that extend the OrderExecutionEngine and modify the execution logic accordingly.
  • Stability: The original OrderExecutionEngine remains unchanged, ensuring that existing trading strategies that rely on it continue to operate without disruption.

Contrast with a Non-OCP Approach:

  • Modifying the OrderExecutionEngine directly to incorporate new trading strategies could introduce errors into previously stable execution strategies. This could lead to unexpected behaviors in parts of the system that depend on the unchanged behavior of the original engine.

Liskov Substitution Principle (LSP)

Definition: The Liskov Substitution Principle (LSP) stipulates that objects of a subclass should be able to replace objects of the superclass without affecting the correctness of the program. This principle ensures that a subclass remains compatible with the behavior of its superclass.

Purpose: LSP is crucial for maintaining robustness and reliability in code, particularly in scenarios involving libraries and frameworks used by external parties. It helps prevent disruptions in the client code when substituting superclass objects with subclass objects.

Key Requirements:

  • Parameter Types: The types of parameters in subclass methods should be the same or more generalized compared to those in superclass methods.
  • Return Types: Return types in subclass methods should be the same or more specialized than those in superclass methods.
  • Exception Handling: Subclass methods should not introduce new exceptions that are not expected by the superclass method, adhering to existing exception handling expectations.
  • Pre-conditions and Post-conditions: Subclasses should not strengthen pre-conditions or weaken post-conditions, as this could lead to unexpected behaviors in the client code.
  • Invariants: Subclasses must uphold the invariants established by the superclass to avoid logical discrepancies.
  • Private Field Access: Direct manipulation of superclass’s private fields should be avoided to maintain encapsulation integrity.

Example

Consider a superclass OrderExecutionEngine in a trading system:

  • Base Functionality: Manages the execution of trades based on current market conditions and predefined strategies.
  • Subclass Example: TimedOrderExecutionEngine extends OrderExecutionEngine by adding functionality to execute trades at specific times without altering the fundamental trade execution strategy.

LSP Compliance:

  • Parameter and Return Types: If OrderExecutionEngine has a method executeTrade(Order order), the TimedOrderExecutionEngine might have executeTradeAtTime(Order order, Time time). The original executeTrade can be overridden to call executeTradeAtTime(order, currentTime) ensuring type compatibility.
  • Exception Handling: If the original method throws TradeExecutionException, the subclass method should not introduce unrelated exceptions like IOException.
  • Preserving Behavior: The TimedOrderExecutionEngine should ensure that all trade executions, whether timed or immediate, maintain the superclass’s behavior such as confirming trade validity and updating system state consistently.

Contrast with a Non-LSP Approach:

  • Suppose a subclass modifies the trade execution logic significantly, such as by executing trades based on different financial models without maintaining the integrity of the superclass’s validation processes. This would breach LSP as the new behavior might not be substitutable in contexts where the original class was expected to operate under specific trading constraints.

Interface Segregation Principle (ISP)

Definition: The Interface Segregation Principle (ISP) suggests that clients should not be forced to depend on interfaces they do not use. This principle advises for designing narrower interfaces, focusing on client-specific functionalities rather than general-purpose interfaces.

Purpose: ISP aims to reduce the side effects of changes in interfaces by ensuring that classes only implement the methods that are essential for their functionality. This leads to more maintainable and adaptable code, as changes to an interface do not impact classes that do not use the modified methods.

Implementation Strategy:

  • Granular Interfaces: Instead of a single “fat” interface, functionalities should be split into smaller, more focused interfaces.
  • Flexible Implementation: Classes can implement multiple interfaces based on their specific needs without being burdened by irrelevant methods.
  • Reduced Impact of Changes: By segregating interfaces, changes to one part of the system have minimal impact on classes that interact with other parts.

Example

Consider a trading system with multiple components like market data handling, order execution, and risk management:

  • Fat Interface Issue: A single interface TradingOperations that includes methods for handling market data, executing trades, and managing risk is problematic. Not all components need all these methods.
  • ISP Application: Break down TradingOperations into MarketDataHandler, OrderExecutor, and RiskManager interfaces.
    • MarketDataHandler might have methods like updateMarketData() and getLatestPrices().
    • OrderExecutor could include methods like executeOrder() and cancelOrder().
    • RiskManager might handle methods like assessRisk() and applyRiskControls().

Scenario Implementation:

  • A class BasicTrader implements OrderExecutor as it only needs to execute trades.
  • Another class AlgorithmicTrader implements both OrderExecutor and RiskManager because it executes trades and also manages risk based on algorithms.
  • MarketDataServer implements MarketDataHandler, handling only market data updates and retrieval.

Benefits of ISP in this Context:

  • Flexibility and Relevance: Each class implements only the interfaces relevant to its operations, avoiding unnecessary dependencies on unused methods.
  • Ease of Maintenance: Changes to the RiskManager interface, for example, do not affect the MarketDataServer or any other class that does not manage risk, minimizing disruption across the system.
  • Scalability: As new functionalities become necessary, additional interfaces can be created and implemented without affecting existing components.

Dependency Inversion Principle (DIP)

Definition: The Dependency Inversion Principle (DIP) dictates that high-level modules should not depend on low-level modules; both should depend on abstractions. Furthermore, these abstractions should not depend on details; rather, details should depend on abstractions.

Purpose: DIP aims to reduce dependencies between the concrete implementations of classes and increase the modularity of the system. This facilitates easier maintenance and scalability, as well as enhancing the ability to test components in isolation from others.

Implementation Strategy:

  • Define Abstractions: Create interfaces or abstract classes that encapsulate the operations required by high-level classes.
  • Depend on Abstractions: Ensure that high-level classes interact with these abstractions rather than concrete implementations of lower-level classes.
  • Implement Abstractions: Lower-level classes should implement these interfaces or inherit from these abstract classes.

Example from High Frequency Trading:

Consider a trading system composed of components for data retrieval, trade execution, and logging:

  • High-Level Class: TradingStrategy which contains complex logic to determine when to buy or sell based on various signals.
  • Low-Level Classes: MarketDataFetcher retrieves market data; TradeExecutor handles the mechanics of placing trades; SystemLogger handles logging of system operations.

Application of DIP:

  1. Interfaces as Abstractions:

    • IMarketDataFetcher with methods like fetchData().
    • ITradeExecutor with methods such as executeTrade().
    • ILogger for logging, with methods like logMessage().
  2. High-Level Dependency:

    • The TradingStrategy class depends only on IMarketDataFetcher, ITradeExecutor, and ILogger, not on the concrete implementations. This allows the strategy to remain unchanged even if the details of data fetching or trade execution change.
  3. Low-Level Implementations:

    • MarketDataFetcher implements IMarketDataFetcher.
    • TradeExecutor implements ITradeExecutor.
    • SystemLogger implements ILogger.

    These classes define how the operations are carried out, but the high-level strategy only interacts with the interfaces.

Benefits of DIP in this Context:

  • Flexibility: Changes to how market data is fetched or how trades are executed can be made without modifying the trading strategy logic.
  • Decoupling: The trading strategy is decoupled from the data fetching and trade execution details, which simplifies testing and maintenance.
  • Scalability: New functionalities or improvements to low-level operations can be implemented by creating new classes that adhere to the established interfaces.

Reference

Last updated on