,

Design Principles

General Guidelines that can guide your class structure and relations.

Definition of Design Principles in Programming

Design principles in programming refer to a set of guidelines and advice’s and best practices that help software developers create structured, maintainable, and efficient code.

These principles aim to improve code readability, facilitate collaboration among developers, and enhance the overall quality of software applications. Here are some key design principles in programming:

Difference Between Design Principles and Design Patterns

Design Principles:

Design Patterns:

In summary, design principles guide the overall approach to coding, while design patterns offer specific solutions that adhere to those principles.

Design Principles:

1. Single Responsibility Principle (SRP)

Each module or class should have one, and only one, reason to change. This means that a class should only have one job or responsibility, making it easier to understand, test, and maintain.

2. Open/Closed Principle (OCP)

Software entities (like classes, modules, and functions) should be open for extension but closed for modification. This principle encourages developers to build upon existing code without altering it, promoting better stability and reducing the risk of introducing bugs.

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of a subclass without affecting the correct functioning of the program. This principle ensures that subclasses can stand in for their parent classes, promoting code reusability and flexibility.

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use. This principle advocates for creating smaller, more specific interfaces rather than one large general-purpose interface, enhancing code flexibility and reducing dependency.

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions. This principle emphasizes the importance of relying on abstractions rather than concrete implementations, improving the modularity and testability of code.

6. Don’t Repeat Yourself (DRY)

This principle states that every piece of knowledge should have a single, unambiguous representation within a system. By avoiding redundancy, developers can reduce maintenance efforts and the risk of inconsistencies.

7. Keep It Simple, Stupid (KISS)

Designs should be as simple as possible, avoiding unnecessary complexity. Simple solutions are easier to understand, maintain, and improve, which is beneficial in the long run.

Few Other Principles

Encapsulate What Varies Principle

The “Encapsulate What Varies” principle is a fundamental concept in software design that suggests developers should identify the aspects of their code that are likely to change and encapsulate them in a way that isolates them from the rest of the system. By doing so, any alterations or modifications can be made with minimal impact on the overall codebase.

This principle promotes flexibility and maintainability, allowing developers to adapt to new requirements or changing conditions without risking the stability of other components. By wrapping the variable elements in dedicated classes or interfaces, developers can ensure that changes are localized, making it easier to manage and reducing the potential for introducing bugs. In practice, this can often be seen in the use of design patterns, where specific strategies or behaviors are encapsulated to accommodate future changes seamlessly.

Favor Composition Over Inheritance

In software design, the principle of “favor composition over inheritance” encourages developers to build systems by composing objects with reusable components rather than relying on a rigid inheritance structure. This approach leads to more flexible, maintainable, and reusable code.

Benefits of Composition:

How to Implement Composition

Here’s a C# example demonstrating composition:

// Define the Engine class
public class Engine
{
    public string Start()
    {
        return "Engine starting";
    }
}

// Define the Wheels class
public class Wheels
{
    public string Roll()
    {
        return "Wheels rolling";
    }
}

// Define the Car class that uses composition
public class Car
{
    private readonly Engine _engine;
    private readonly Wheels _wheels;

    public Car()
    {
        _engine = new Engine();
        _wheels = new Wheels();
    }

    public string Drive()
    {
        return $"{_engine.Start()}. {_wheels.Roll()}";
    }
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        Car myCar = new Car();
        Console.WriteLine(myCar.Drive());
    }
}

In this example:

By using composition, you gain advantages like easier maintenance and the ability to create more complex behaviors from simple components, reinforcing the principle of favoring composition over inheritance.

Other Use Case:
If need to add another type of CoffeWithSomeFlavour need another class, if any change in cost of milk need change in other class as well. instead we compose all the flavors with composition.

Program to Interface Principle

Description

The “Program to Interface” principle is a fundamental concept in software design that emphasizes the importance of coding against interfaces rather than concrete implementations. This principle allows developers to create flexible and decoupled systems where components can be easily replaced or extended without modifying existing code. By relying on abstractions, such as interfaces or abstract classes, instead of specific implementations, developers enhance the modularity and testability of their software.

Use Case

Consider a scenario where a developer is building a notification system that can send alerts through different mediums, such as email, SMS, or push notifications. Instead of directly coding the logic for each notification type, the developer can define an interface called INotificationService with a method SendNotification.

public interface INotificationService
{
    void SendNotification(string message);
}

Next, concrete implementations for each notification type can be created:

public class EmailNotification : INotificationService
{
    public void SendNotification(string message)
    {
        // Logic to send email notification
    }
}

public class SmsNotification : INotificationService
{
    public void SendNotification(string message)
    {
        // Logic to send SMS notification
    }
}

When utilizing the notification system, the developer interacts with the INotificationService interface rather than the specific implementations:

public class NotificationManager
{
    private readonly INotificationService _notificationService;

    public NotificationManager(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void Notify(string message)
    {
        _notificationService.SendNotification(message);
    }
}

In this example, the NotificationManager can work with any class that implements the INotificationService interface. This approach allows developers to easily introduce new notification methods (e.g., push notifications) without requiring changes to the existing NotificationManager logic, promoting scalability and maintainability in the codebase.

Single Responsibility Principle (SRP)

Definition

The Single Responsibility Principle (SRP) states that a class should have one, and only one, reason to change. This means that every module or class in a program should focus on a single responsibility or functionality, which makes the code easier to understand, maintain, and test. By adhering to this principle, developers can improve the cohesion of their code and minimize the impact of changes.

Use Case

Consider a simple application for managing user profiles. If you have a UserProfile class that handles user data, sends email notifications, and manages user settings, it violates the Single Responsibility Principle. This class would become too complex and difficult to maintain, as any change related to email notifications would affect other responsibilities.

Instead, you can refactor the code as follows:

public class UserProfile
{
    // Methods and properties related to user data
}

public class EmailService
{
    public void SendEmail(string emailAddress, string message)
    {
        // Logic to send email
    }
}

public class UserSettings
{
    // Methods and properties related to user settings
}

In this refactored example, each class has a clear responsibility: UserProfile manages user data, EmailService handles email notifications, and UserSettings deals with user preferences. This separation of concerns enhances maintainability and testability, as changes to one aspect (like user notifications) do not impact the others.

Open/Closed Principle (OCP)

Definition

The Open/Closed Principle states that software entities such as classes, modules, and functions should be open for extension but closed for modification. This principle encourages developers to design systems that allow for new functionality to be added without altering existing code, thereby promoting stability and reducing the risk of introducing bugs.

Use Case

Consider a payment processing system that initially supports credit card payments. As the business grows, the requirement arises to support additional payment methods, such as PayPal and bank transfers. Instead of modifying the existing payment processing class, which may lead to errors and regression, you can define a Payment interface and implement separate classes for each payment type:

public interface IPayment
{
    void ProcessPayment(decimal amount);
}

public class CreditCardPayment : IPayment
{
    public void ProcessPayment(decimal amount)
    {
        // Logic for processing credit card payment
    }
}

public class PayPalPayment : IPayment
{
    public void ProcessPayment(decimal amount)
    {
        // Logic for processing PayPal payment
    }
}

// Usage
public class PaymentProcessor
{
    private readonly IPayment _payment;

    public PaymentProcessor(IPayment payment)
    {
        _payment = payment;
    }

    public void Process(decimal amount)
    {
        _payment.ProcessPayment(amount);
    }
}

In this case, the existing system remains untouched while offering new payment options by simply introducing new classes that adhere to the IPayment interface. This approach aligns with the Open/Closed Principle, facilitating easier maintenance and scalability of the payment processing system.

Leave a comment