Solid Principles

Solid Principles

Solid Principles are the time tested principles for writing clean and high quality code in object oriented programming.

Why should you study SOLID Principles?

Studying the SOLID principles is essential for software developers because they provide a clear framework for designing software that's easy to maintain, scalable, and resilient against bugs. You will find a major difference in your code quality once you learn these principles as they are time tested.

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. Here's an example pseudocode to illustrate this principle:

```
// High-level module: PrinterManager

interface Printer {
print(document: Document): void
}

class LaserPrinter implements Printer {
print(document: Document): void {
// Code to print document on a laser printer
}
}

class InkjetPrinter implements Printer {
print(document: Document): void {
// Code to print document on an inkjet printer
}
}

class Document {
// Document properties and methods
}

class PrinterManager {
private printer: Printer

constructor(printer: Printer) {
this.printer = printer
}

printDocument(document: Document) {
this.printer.print(document)
}
}

// Low-level module: Main

document = new Document()
inkjetPrinter = new InkjetPrinter()

printerManager = new PrinterManager(inkjetPrinter)
printerManager.printDocument(document)
```

In this example, the `PrinterManager` is a high-level module responsible for managing the printing of documents. It depends on the `Printer` interface, which is an abstraction representing different types of printers. By depending on the interface, the `PrinterManager` is no longer tightly coupled to specific printer implementations.

The `LaserPrinter` and `InkjetPrinter` are low-level modules that implement the `Printer` interface. They provide the necessary functionality to print a document. By adhering to the DIP, the high-level module (`PrinterManager`) does not rely on concrete implementations of printers and can easily switch between different types of printers by injecting the appropriate implementation through its constructor.

By following the Dependency Inversion Principle, you can achieve more flexible and maintainable code, as it allows for easier testability, extensibility, and decoupling of modules.

Code Example

Without Dependency Inversion Principle Principle


# Concrete class of a light bulb
# There's no application of Dependency Inversion here; high-level modules would directly depend on this low-level module.
class LightBulb {
    function turnOn() {
        //... turn the bulb on ...
    }

    function turnOff() {
        //... turn the bulb off ...
    }
}

# High-level module
# This class violates the Dependency Inversion Principle by directly interacting with the 'LightBulb', a low-level module, rather than an abstraction.
class ElectricPowerSwitch {
    LightBulb bulb  # Direct dependency on a concrete class, which is against Dependency Inversion Principle.

    # This constructor is directly dependent on a concrete class 'LightBulb', illustrating a violation of the Dependency Inversion Principle.
    function constructor(LightBulb bulb) {
        this.bulb = bulb
    }

    function press() {
        //... check whether the light is on...
        //... and then turn it off or on accordingly ...
        # The direct use of methods from the 'LightBulb' class here underlines the high-level module's dependency on the low-level module, against the principle of Dependency Inversion.
    }
}

# Usage
# Here too, the creation and usage of classes are tightly coupled, not adhering to Dependency Inversion. Any change in 'LightBulb' would necessitate changes here as well.
bulb = new LightBulb()
switch = new ElectricPowerSwitch(bulb)
switch.press()

With Dependency Inversion Principle Principle


# Abstraction (defines a contract for low-level modules)
# Dependency Inversion Principle is applied here by creating an abstraction that the high-level module and low-level module can depend on.
interface SwitchableDevice {
    function turnOn()
    function turnOff()
}

# Lower-level module
# The LightBulb class is implementing the SwitchableDevice interface, adhering to the Dependency Inversion Principle by depending on abstraction rather than on details.
class LightBulb implements SwitchableDevice {
    function turnOn() {
        //... turn the bulb on ...
    }

    function turnOff() {
        //... turn the bulb off ...
    }
}

# Higher-level module
# Dependency Inversion is applied here since ElectricPowerSwitch relies on the abstraction of a SwitchableDevice, not on the lower-level detail like a specific LightBulb class.
class ElectricPowerSwitch {
    SwitchableDevice device  # This dependency on an interface is the core of the Dependency Inversion Principle.
    bool on

    # The constructor demonstrates Dependency Inversion Principle by accepting any 'SwitchableDevice' - it's not dependent on concrete classes.
    function constructor(SwitchableDevice device) {
        this.device = device
        this.on = false
    }

    function press() {
        if (this.on) {
            this.device.turnOff()
            this.on = false
        } else {
            this.device.turnOn()
            this.on = true
        }
    }
}

# Usage
# The client code here also follows Dependency Inversion Principle, as it can use any device implementing 'SwitchableDevice', not just a 'LightBulb'.
bulb = new LightBulb()
switch = new ElectricPowerSwitch(bulb)
switch.press()
The Dependency Inversion Principle (DIP) is a design principle that suggests high-level modules should not depend on low-level modules, but both should depend on abstractions. Here is a summary of implementing DIP using pseudocode:

1. Define the high-level module:
- Create a class or module that represents the high-level functionality.
- Declare a constructor or method that requires a dependency interface instead of a concrete implementation.

2. Define the low-level module:
- Create a class or module that represents the low-level functionality.
- Implement the dependency interface required by the high-level module.

3. Create the dependency interface:
- Define an interface or abstract class that represents the contract for the dependencies.
- Include the required methods that the high-level module needs to interact with.

4. Implement the dependency interface in the low-level module:
- Modify the low-level module to implement the defined dependency interface.
- Implement the required methods according to the interface contract.

5. Inject the dependency into the high-level module:
- In the high-level module, introduce a way to provide the dependency implementation.
- Modify the constructor or method to accept an instance of the dependency interface.

6. Use the dependency in the high-level module:
- Inside the high-level module, use the provided dependency interface to call the required methods.
- Avoid coupling to the concrete implementation by interacting only with the dependency interface.

By following these steps, you can achieve the principle of Dependency Inversion, allowing for flexibility, modularity, and easier testing and maintenance of your code.
Common mistakes while implementing the Dependency Inversion Principle in pseudocode:

1. Directly passing concrete implementations: One common mistake is to directly pass concrete implementations of dependencies in the code, instead of depending on abstractions. This breaks the Dependency Inversion Principle as it introduces tight coupling between the classes.

2. Violating the Liskov Substitution Principle: The Liskov Substitution Principle states that any implementation of an interface should be substitutable for its base interface. If the implementation violates this principle, it can lead to issues when applying the Dependency Inversion Principle.

3. Creating unnecessary abstractions: Another mistake is creating unnecessary abstractions or interfaces that do not add any value. This can make the code more complex and difficult to maintain.

Best practices while implementing the Dependency Inversion Principle in pseudocode:

1. Depend on abstractions, not concretions: The key to following the Dependency Inversion Principle is to depend on abstractions rather than concrete implementations. This can be achieved by programming to interfaces or using dependency injection.

2. Use inversion of control containers: Inversion of Control (IoC) containers can help in managing dependencies and provide automatic dependency injection. They handle the creation and wiring up of objects, allowing you to focus on defining abstractions and writing business logic.

3. Apply dependency injection correctly: When using dependency injection, make sure to inject dependencies explicitly, preferably through constructor injection. This helps in enforcing dependencies and makes it easier to test and maintain the code.

4. Design and apply SOLID principles: The Dependency Inversion Principle is one of the SOLID principles. It is important to understand and apply all the other SOLID principles (Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle) to have a well-designed, modular, and maintainable codebase.

5. Use clear and meaningful interface/class names: The names of the interfaces and classes should be clear, concise, and meaningful. This helps in understanding the responsibilities and purposes of different components and promotes good design practices.

Remember that pseudocode is a high-level description of code, so it's important to write clear and concise explanations of how the Dependency Inversion Principle is being applied and to avoid any ambiguity in your descriptions.

SolidPrinciples is a website dedicated to SOLID Principles. We want to ensure developers really the understand SOLID Principles and understand how to apply them in their code. It is both beginner and intermediate level friendly.

SolidPrinciples

SolidPrinciples © 2024 All rights reserved.

email
email
email