In software development, adhering to design principles is crucial for creating robust, maintainable, and scalable applications. Here, we'll explore five fundamental design principles: Dependency Inversion, Separation of Concerns, Single Responsibility, DRY (Don't Repeat Yourself), and Persistence Ignorance. Each principle helps to ensure that your codebase remains clean and efficient.
1. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle is the last of the SOLID principles. It states that high-level modules should not depend on low-level modules; both should depend on abstractions. Additionally, abstractions should not depend on details. Details should depend on abstractions.
Example:
public interface IMessageService
{
void SendMessage(string message);
}
public class EmailService : IMessageService
{
public void SendMessage(string message)
{
// Send email
}
}
public class Notification
{
private readonly IMessageService _messageService;
public Notification(IMessageService messageService)
{
_messageService = messageService;
}
public void Notify(string message)
{
_messageService.SendMessage(message);
}
}
In this example, the Notification
class depends on the abstraction IMessageService
rather than a concrete implementation like EmailService
. This allows for easy swapping of different message services without modifying the Notification
class.
2. Separation of Concerns (SoC)
Separation of Concerns involves organizing code into distinct sections, each responsible for a specific functionality. This principle helps to reduce code complexity and improve maintainability.
Example:
In a typical web application, you might separate concerns into layers such as:
- Presentation Layer: Handles the UI and user interaction.
- Business Logic Layer: Contains the core application logic.
- Data Access Layer: Manages data retrieval and storage.
// Presentation Layer
public class UserController
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
public IActionResult GetUser(int id)
{
var user = _userService.GetUserById(id);
return Ok(user);
}
}
// Business Logic Layer
public interface IUserService
{
User GetUserById(int id);
}
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public User GetUserById(int id)
{
return _userRepository.GetById(id);
}
}
// Data Access Layer
public interface IUserRepository
{
User GetById(int id);
}
public class UserRepository : IUserRepository
{
public User GetById(int id)
{
// Retrieve user from database
}
}
Each layer has a specific responsibility, making the codebase easier to manage and extend.
3. Single Responsibility Principle (SRP)
The Single Responsibility Principle asserts that a class should have only one reason to change, meaning it should have only one job or responsibility.
Example:
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
public class UserRepository
{
public void Save(User user)
{
// Save user to database
}
}
public class UserValidator
{
public bool Validate(User user)
{
// Validate user data
}
}
Here, User
, UserRepository
, and UserValidator
each have a single responsibility, adhering to the SRP.
4. DRY (Don't Repeat Yourself)
The DRY principle emphasizes reducing the repetition of code by abstracting out common functionality into reusable components.
Example:
public class EmailService
{
public void SendEmail(string to, string subject, string body)
{
// Send email
}
}
public class NotificationService
{
private readonly EmailService _emailService;
public NotificationService(EmailService emailService)
{
_emailService = emailService;
}
public void NotifyUser(string email, string message)
{
_emailService.SendEmail(email, "Notification", message);
}
}
By using the EmailService
in NotificationService
, we avoid duplicating email sending logic.
5. Persistence Ignorance
Persistence Ignorance means that the business logic of an application should not be aware of how data is persisted. This principle ensures that the core logic remains independent of the data access technology used.
Example:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public interface IProductRepository
{
void Save(Product product);
}
public class ProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public void AddProduct(Product product)
{
_productRepository.Save(product);
}
}
The ProductService
class does not know or care about how the ProductRepository
class persists data, adhering to the principle of persistence ignorance.
Conclusion
Understanding and applying these design principles can significantly enhance the quality of your software. By following Dependency Inversion, Separation of Concerns, Single Responsibility, DRY, and Persistence Ignorance, you can create applications that are easier to understand, maintain, and extend.
Top comments (4)
Great write-up! Also wrote some thoughts about it but in context of Go. Although Golang is not a purely object-oriented language, we can still apply SOLID principles to improve our Go code - packagemain.tech/p/mastering-solid...
Excellent.
Thanks
very well explained
Another 10 Principles