Low level design and SOLID Principles
Srishti Prasad
Posted on October 1, 2024
Low-Level Design (LLD) is a critical phase in software development that bridges the gap between high-level design and actual implementation. While high-level design focuses on architectural blueprints, LLD deals with how each component, class, or function is implemented to fulfill the overall system's requirements.
In simpler terms, LLD involves designing classes, methods, interfaces, and interactions between them, ensuring that the code is efficient, maintainable, and scalable. It’s an essential skill for software engineers, especially when building systems that need to be robust, reusable, and easy to modify over time.
This blog will introduce you to the key concepts, principles, and techniques involved in low-level design and show how they can help you write better, more maintainable code.
The first question that comes in our mind is:
Why is Low-Level Design Important?
- Maintainability: A well-thought-out design makes it easier to maintain, extend, and debug code. Poor design leads to technical debt, making future changes costly.
- Scalability: Good LLD ensures that your code is scalable, both in terms of performance and in supporting new features as the system evolves.
- Reusability: Well-designed components can be reused across different parts of a system or in entirely different projects.
- Clarity: With a well-defined design, engineers can understand how various parts of the system fit together, making collaboration easier.
To bridge the gap between LLD concepts and real code, let's break down the process of designing a low-level diagram through the following steps:
Step 1:Object Oriented Principles
Step 2:SOLID Principles
Step 3:Design Patterns
Object oriented principles
Object-oriented programming concept 4 pillars are must-have to go start learning low-level designing. I have already covered this concept in brief checkout blog
SOLID Principles
S: Single Responsibility Principle (SRP)
- Each unit of code should have only one responsibility to change.
- A unit can be a class, module, function, or component.
- Keeps code modular and reduces tight coupling.
Example: Imagine a class that handles both user authentication and logging. If we need to change how logging works, we would end up modifying the authentication class as well. This violates SRP. Instead, we should have two separate classes: one for user authentication and another for logging, so each class has a single responsibility.
O: Open/Closed Principle (OCP)
- Units of code should be open for extension but closed for modification.
- Extend functionality by adding new code, not modifying existing code.
- Useful in component-based systems like a React frontend.
Example: Consider a payment processing system that handles payments via credit cards. If you need to add support for PayPal, rather than modifying the existing code, you should extend it by adding a new class for PayPal payments. This ensures the existing system remains stable while allowing new functionality to be added.
L: Liskov Substitution Principle (LSP)
- Subclasses should be substitutable for their base classes.
- Functionality in the base class should be usable by all subclasses.
- If a subclass can’t use the base class functionality, it shouldn’t be in the base class.
Example: If we have a Bird class that has a method fly(), and we create a subclass Penguin, which cannot fly, this violates LSP. The Penguin class should not inherit fly() since it changes the expected behavior. Instead, the Bird class should be refactored to handle birds that can and cannot fly differently.
I: Interface Segregation Principle (ISP)
- Provide multiple specific interfaces rather than a few general-purpose ones.
- Clients shouldn’t depend on methods they don’t use.
Example: Suppose we have an interface Animal with methods fly(), swim(), and walk(). A class Dog that implements Animal would be forced to define fly(), which it doesn't need. To comply with ISP, we should split the Animal interface into smaller interfaces like Flyable, Swimmable, and Walkable to avoid forcing irrelevant methods on classes
D: Dependency Inversion Principle (DIP)
- Depend on abstractions, not concrete classes.
- Use abstractions to decouple dependencies between parts of the system.
- Avoid direct calls between code units use interfaces or abstractions.
Example: In an e-commerce application, if the checkout process (high-level module) depends directly on a specific payment gateway like PayPal (low-level module), changing the payment gateway requires modifying the checkout process. By introducing an abstraction, such as a PaymentProcessor interface, the checkout process can work with any payment method without needing to know the specifics of PayPal or any other service.
Design Patterns
Design patterns are proven solutions to common problems that arise in software design. They are best practices that developers can follow to solve specific design issues efficiently and systematically. Instead of reinventing the wheel, design patterns provide a standard approach to solving recurring problems.
Design patterns can be categorized into three types:
-
Creational Patterns: Deal with object creation
- Factory design pattern
- Abstract factory design pattern
- Builder design pattern
- Prototype Design pattern
- Singleton design pattern
-
Structural Patterns: Deal with object composition and relationships
- Adapter Pattern
- Bridge Pattern
- Composite Pattern
- Decorator Pattern
- Facade Pattern
- Flyweight Pattern
- Proxy Pattern
-
Behavioral Patterns: Deal with object interaction and responsibility
- Chain of Responsibility Pattern
- Command Pattern
- Interpreter Pattern
- Mediator Patter
- Memento Pattern
- Observer Pattern
- State Pattern
- Strategy Pattern
- Template Method Pattern
- Visitor Pattern
When writing code, you typically face three major types of problems:
Object Creation:
The first challenge is figuring out how to create objects.
This is where creational design patterns come into play. Patterns like:
Factory, Abstract Factory, Singleton, Builder .These patterns deal with the creation of objects.
Object Relationships:
Once you’ve created the objects, the next question is: How will they be related to each other?
You need to decide how to structure these objects into a larger system or module. This is where structural design patterns are useful. Patterns like: Facade, Composite, Adapter
These help define the relationships and structure between objects.
Object Communication:
Finally, you need to determine how the objects will communicate with each other and what responsibilities each object will have.
Do you want different time behaviors, or chaining of objects?
Behavioral design patterns solve this. Patterns like: Strategy, Command, Observer
These patterns manage object communication and behavior delegation.
Now that we've laid the foundation by exploring the SOLID principles and introduced the vast landscape of design patterns, we're ready to dive deeper! In the upcoming series, I'll break down each design pattern with practical examples and real-world scenarios. Whether you're just starting your design journey or looking to sharpen your skills, these patterns will help you write cleaner, more scalable code. Stay tuned for the next blog, where we unravel the first design pattern—step by step!
If you've made it this far, don't forget to hit like ❤️ and drop a comment below with any questions or thoughts. Your feedback means the world to me, and I'd love to hear from you!
Posted on October 1, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.