Chapter 22 - Design Patterns and clean code principles Flashcards
Difference between design principles and patterns
design principles guide the overall structure and organization of your code, promoting qualities like maintainability and scalability, while design patterns offer specific solutions to recurring design problems in alignment with these principles. Principles are more abstract and foundational, while patterns are more concrete and implementation-focused. However, both are essential for writing clean, maintainable, and scalable code in Java (or any programming language).
Design Principles:
Design principles are general guidelines or rules that help in designing software that is maintainable, scalable, and understandable. They are high-level concepts that guide the overall structure and organization of your code.
Examples of design principles include SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion), DRY (Don’t Repeat Yourself), KISS (Keep It Simple, Stupid), and others.
These principles are more about the mindset and approach to software design rather than specific implementations.
Design Patterns:
Design patterns are specific solutions to common problems encountered during software development. They are tried and tested solutions to recurring design problems.
Design patterns provide concrete implementations to achieve goals aligned with design principles.
Examples of design patterns include Singleton, Factory Method, Observer, Strategy, Adapter, and many others.
Each design pattern has its specific context, structure, and participants.
Encapsulate What Varies Design principle
Identify the aspects of your application that vary and separate them from what stays the same
Encapsulating what varies means identifying the aspects of your codebase that are likely to change in the future and encapsulating them behind a stable interface. By doing this, you reduce the ripple effect of changes. When a change is needed, you only need to modify the encapsulated part, without impacting the rest of the codebase. This promotes maintainability, as it minimizes the risk of unintended side effects when making changes.
example from the spring boot when we have UserService interface and can have different implementations for example with MongoDB or JPA.
Program to an Interface, not an
Implementation
“Program to an Interface, not an Implementation” is a design principle that emphasizes the importance of using interfaces (or abstract classes) to define the behavior of components in your code rather than relying on concrete implementations. This principle promotes flexibility, extensibility, and maintainability in software design.
Favor Composition Over
Inheritance
“Favor Composition Over Inheritance” is a design principle that suggests preferring composition (has-a relationship) over inheritance (is-a relationship) when designing the structure of classes and their relationships. This principle encourages building systems by composing smaller, more cohesive components rather than relying heavily on inheritance hierarchies.
When favoring composition over inheritance, it’s preferable to use interface implementation (implements) rather than class inheritance (extends) to achieve the desired functionality.
SOLID
Single Responsibility Principle (SRP)
Open/Closed Principle (OCP)
Liskov Substitution Principle (LSP)
Interface Segregation Principle (ISP)
Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change, meaning that it should have only one responsibility or job. In other words, a class should be responsible for only one aspect of the functionality within a system.
Consider a class Employee that is responsible for storing employee information and also for formatting employee data for display. This violates SRP, as the class has two reasons to change: if the employee information structure changes or if the formatting requirements change.
Emagine spring boot, UserService class is responsible for both user management (e.g., saving users to the database) and sending welcome emails. This violates the Single Responsibility Principle because the class has multiple reasons to change: if the user management logic changes or if the email sending logic changes.
Open/Closed Principle (OCP)
The Open/Closed Principle (OCP) is one of the five SOLID principles of object-oriented design. It states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In simpler terms, this means that once a class is written and tested, it should not be modified to add new functionality; instead, it should be extended through inheritance or composition.
OrderService class is directly responsible for calculating the total cost of an order and applying a discount for premium customers. If a new type of customer or a different discount strategy is introduced, the OrderService class needs to be modified, violating the Open/Closed Principle.
Liskov Substitution Principle (LSP)
It states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In simpler terms, this means that subclasses should be substitutable for their base classes without altering the desired behavior of the program.
Imagine you have a class called Animal that represents all animals. Now, let’s say you have a subclass called Dog that represents dogs, and another subclass called Cat that represents cats.
According to the Liskov Substitution Principle (LSP):
You should be able to use a Dog object wherever you would use an Animal object, without anything going wrong.
Similarly, you should be able to use a Cat object wherever you would use an Animal object, without anything going wrong.
Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) is one of the SOLID principles of object-oriented design. It emphasizes that clients should not be forced to depend on interfaces they do not use. In simpler terms, ISP states that a class should not be forced to implement interfaces with methods it does not need.
Let’s consider a scenario where we have a large, monolithic interface called Machine that contains methods for various
machine operations such as start(), stop(), restart(), and shutdown(). However, not all machines need to support all these operations. Some machines may only need to start and stop.
ISP involves designing interfaces that are tailored to the specific needs of clients and avoiding “fat” interfaces.
Dependency Inversion Principle (DIP)
KISS
In Java, KISS can be applied by writing straightforward, understandable code without unnecessary complexity. This means avoiding overly clever solutions and keeping code concise yet clear.
Utilize built-in Java libraries and features instead of reinventing the wheel or creating overly intricate custom solutions.
Follow standard Java naming conventions and design patterns to make code more intuitive for other developers to read and understand.
Keep classes and methods focused on a single responsibility, adhering to the Single Responsibility Principle (SRP) from object-oriented design.
DRY (DOnt repeat yourself)
In Java, DRY can be implemented by identifying repeating code patterns and extracting them into reusable methods or classes.
Utilize inheritance, interfaces, and composition to avoid code duplication and promote code reuse.
Create utility classes or helper methods for common tasks rather than rewriting the same logic in multiple places.
Encapsulate repetitive operations into well-named functions or classes to improve readability and maintainability.
YAGNI (You Ain’t Gonna Need It):
In Java, YAGNI encourages developers to resist the temptation to add functionality prematurely. Instead, focus on delivering the required features without unnecessary additions.
Avoid over-engineering by only implementing the functionality that is currently needed to fulfill the requirements.
Refactor and extend code as new requirements arise, rather than preemptively adding complex features that may not be necessary.
Prioritize simplicity and clarity in code design, resisting the urge to add speculative features that may introduce unnecessary complexity or overhead.
Law of Demeter (LoD):
The Law of Demeter (LoD), also known as the principle of least knowledge or principle of least access, is a design guideline for object-oriented programming. LoD states that a module should only have knowledge of its immediate dependencies and should not reach out to access the internal details of other objects.
in spring boot case it meant that controller talks to service and not repository directly.
Single Level of Abstraction Principle (SLAP):
To adhere to SLAP, you could refactor the function to separate the low-level operations from the high-level operation, ensuring that each function focuses on a single level of abstraction. This improves code clarity, maintainability, and flexibility over time.
Dependency Injection (DI):
Dependency Injection (DI) is a design pattern used in software development to manage dependencies between components of an application. It is a technique where the dependencies of a component are provided from the outside rather than created internally. In other words, instead of a component creating its dependencies, they are “injected” into the component from an external source.
Fail Fast:
Fail Fast is a software development principle that promotes detecting and reporting errors as soon as they occur. Instead of allowing errors to propagate throughout the system, the application should fail immediately when an error is detected.
Rule of Three:
The Rule of Three suggests that if you find yourself writing similar code for the third time, you should consider refactoring it into a reusable abstraction. This principle helps in identifying common patterns and promoting code reuse and maintainability.