From Object-Oriented Programming to Design

From Object-Oriented Programming to Design

This paper describes the basic paradigm of Object Oriented Programming and from which its extension Object Oriented Software design patterns. It illustrate basic concept to take note of and act as a starting point for those interested in the subject.

What are design pattern ?

Design patterns are a bag of reusable experience from many generations of coders and software engineers that we can learn from,

Why are we using them?

As developers we are prone to mistakes and sometimes inexperience decisions. To benefit from usage of design pattern, it is always good for developer to understand the fundamental of OOP.

Organized Code vs Unorganized Code

Visualization of unorganized code
Visualization of organized code

What are hallmarks of good architecture?

  1. Loose Coupling (LC) [Focus on Compoenents]:
    • Changes to one component least affect existence or performance of other component
  2. Separation of Concerns(SoC)[Focus on Software Systems]
    • Breaking system into distinct tiers with each responsible for a specifc aspect or functionality
  3. Law of Demeter (LoD) [Focus on Object]
    • Unit should only talk to its immediate friend
  4. Common benefits of adhering to the 3 points above:
    • minimize dependency
    • improved resuability
    • Improved maintainability
    • easier testing
    • scalability
    • Increased encapsulation
    • improved collaboration

What’re the difference between Object and Component?

An object is an instance of a class in object-oriented programming, representing a single unit that encapsulates data and behavior. In contrast, a component is a higher-level, self-contained unit of functionality in software architecture that can be composed of multiple objects or classes and is designed to be reusable, modular, and loosely coupled with other components.

Object-Oriented Programming : SOLID Principles

  1. Single Responsibility Principle (SRP). Class should specialize in terms of functionality
  2. Open/Closed Principle (OCP):This means that new functionality should be added through inheritance or composition, rather than by directly modifying existing code.
  3. Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering the correctness of the program.
  4. Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use.
  5. Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules, but both should depend on abstractions.

Code Example:

Single Responsibility Principle (SRP):

Suppose you are building an application that deals with user management, including adding users and sending welcome emails. Without considering the SRP, your classes might look like this:

//
//UserManager class has two responsibilities: 
//adding users to the system and sending welcome emails.
//
public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class UserManager
{
    public void AddUser(User user)
    {
        // Logic to add a user to the system

        SendWelcomeEmail(user);
    }

    private void SendWelcomeEmail(User user)
    {
        // Logic to send a welcome email to the user
    }
}
public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class UserService
{
    public void AddUser(User user)
    {
        // Logic to add a user to the system
    }
}

public class EmailService
{
    public void SendWelcomeEmail(User user)
    {
        // Logic to send a welcome email to the user
    }
}


//
//Now, the responsibilities of adding users and sending welcome emails are 
//separated into the UserService and EmailService classes, respectively. 
//
//The UserManager class is responsible for coordinating the user management 
//process, and it delegates the specific tasks to the appropriate services.
//
public class UserManager
{
    private readonly UserService _userService;
    private readonly EmailService _emailService;

    public UserManager(UserService userService, EmailService emailService)
    {
        _userService = userService;
        _emailService = emailService;
    }

    public void AddUser(User user)
    {
        _userService.AddUser(user);
        _emailService.SendWelcomeEmail(user);
    }
}

Open/Closed Principle (OCP):

Suppose you are building an application that calculates the area of different shapes. If you do not use OCP, you might code like the following:

public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }
}

public class Circle
{
    public double Radius { get; set; }
}

public class AreaCalculator
{

    //
    // We will need to modify AreaCalculator if we want to
    // support new shapes, which violates the 
    // Open/Closed Principle.
    //
    public double CalculateArea(object shape)
    {
        if (shape is Rectangle rectangle)
        {
            return rectangle.Width * rectangle.Height;
        }
        else if (shape is Circle circle)
        {
            return Math.PI * Math.Pow(circle.Radius, 2);
        }

        throw new InvalidOperationException("Unsupported shape");
    }
}

//
//Now, the AreaCalculator class depends on the IShape 
//abstraction, and each shape is responsible for 
//implementing its area calculation logic. 
//If you need to add support for a new shape
//you can simply create a new class that implements 
//the IShape interface without modifying the 
//AreaCalculator class.
//
public interface IShape
{
    double CalculateArea();
}



//
// Rectangle will know how to calculate its own area
//
public class Rectangle : IShape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double CalculateArea()
    {
        return Width * Height;
    }
}



//
// Circle will know how to calculate its own area
//
public class Circle : IShape
{
    public double Radius { get; set; }

    public double CalculateArea()
    {
        return Math.PI * Math.Pow(Radius, 2);
    }
}



public class AreaCalculator
{
    public double CalculateArea(IShape shape)
    {
        return shape.CalculateArea();
    }
}

Liskov Substitution Principle (LSP)

Suppose you are building an application that deals with different types of vehicles and their operations. Without considering the LSP, your classes might look like this:

public class Vehicle
{
    public virtual void StartEngine()
    {
        Console.WriteLine("Starting the engine...");
    }

    public virtual void StopEngine()
    {
        Console.WriteLine("Stopping the engine...");
    }
}

public class Car : Vehicle
{
    public override void StartEngine()
    {
        Console.WriteLine("Starting the car engine...");
    }

    public override void StopEngine()
    {
        Console.WriteLine("Stopping the car engine...");
    }
}



//
//While the Car class has a valid inheritance relationship with the Vehicle class,
//the ElectricCar class violates the Liskov Substitution Principle.
//
public class ElectricCar : Vehicle
{
    public override void StartEngine()
    {
        throw new NotSupportedException("Electric cars do not have engines");
    }

    public override void StopEngine()
    {
        throw new NotSupportedException("Electric cars do not have engines");
    }
}
public interface IVehicle
{
    void Start();
    void Stop();
}

public class Vehicle : IVehicle
{
    public virtual void Start()
    {
        Console.WriteLine("Starting the engine...");
    }

    public virtual void Stop()
    {
        Console.WriteLine("Stopping the engine...");
    }
}

public class Car : Vehicle
{
    public override void Start()
    {
        Console.WriteLine("Starting the car engine...");
    }

    public override void Stop()
    {
        Console.WriteLine("Stopping the car engine...");
    }
}




public class ElectricCar : IVehicle
{
    public void Start()
    {
        Console.WriteLine("Starting the electric car...");
    }

    public void Stop()
    {
        Console.WriteLine("Stopping the electric car...");
    }
}

We’ve removed the problematic inheritance relationship between Vehicle and ElectricCar and changed the behavior of the ElectricCarclass to conform to the LSP. To apply LSP, one need to work at the right abstration level.

Interface Segregation Principle (ISP):

Suppose you are building an application that interacts with a multi-function printer that can print, scan, and fax documents. Without following the ISP, your interface might look like this:

public interface IMultiFunctionPrinter
{
    void Print(Document document);
    void Scan(Document document);
    void Fax(Document document);
}



public class MultiFunctionPrinter : IMultiFunctionPrinter
{
    public void Print(Document document)
    {
        // Print the document
    }

    public void Scan(Document document)
    {
        // Scan the document
    }

    public void Fax(Document document)
    {
        // Fax the document
    }
}

If you need to support a printer that can only print, you are still forced to implement the Scan and Fax methods, which is unnecessary and violates the Interface Segregation Principle.

public interface IPrinter
{
    void Print(Document document);
}

public interface IScanner
{
    void Scan(Document document);
}

public interface IFaxMachine
{
    void Fax(Document document);
}

public class Printer : IPrinter
{
    public void Print(Document document)
    {
        // Print the document
    }
}

public class MultiFunctionPrinter : IPrinter, IScanner, IFaxMachine
{
    public void Print(Document document)
    {
        // Print the document
    }

    public void Scan(Document document)
    {
        // Scan the document
    }

    public void Fax(Document document)
    {
        // Fax the document
    }
}

Dependency Inversion Principle (DIP):

Example of an application that sends notifications to users through different channels, such as email and SMS. If you do not know DIP, you might code like the following:

public class EmailSender
{
    public void SendEmail(string message, string emailAddress)
    {
        // Send email
    }
}



public class NotificationService
{
    private EmailSender _emailSender;


		//
    // the high-level module NotificationService directly depends 
    //on the low-level module EmailSender
    //
    public NotificationService()
    {
        _emailSender = new EmailSender();
    }

    public void NotifyUser(string message, string emailAddress)
    {
        _emailSender.SendEmail(message, emailAddress);
    }
}
public interface INotificationSender
{
    void SendNotification(string message, string destination);
}



//
//Now, the NotificationService class depends on the INotificationSender 
//abstraction rather than the concrete EmailSender implementation. 
//This makes it easy to add support for new notification channels 
//(such as SMS) without modifying the NotificationService.
//
public class EmailSender : INotificationSender
{
    public void SendNotification(string message, string emailAddress)
    {
        // Send email
    }
}

public class SmsSender : INotificationSender
{
    public void SendNotification(string message, string phoneNumber)
    {
        // Send SMS
    }
}




public class NotificationService
{
    private INotificationSender _notificationSender;


    //
    // During the instantiation of the Notification, by passing in different
    // implementation of the INotificationSender, we can extend the 
    // the design
    //
    public NotificationService(INotificationSender notificationSender)
    {
        _notificationSender = notificationSender;
    }

    public void NotifyUser(string message, string destination)
    {
        _notificationSender.SendNotification(message, destination);
    }
}

Pattern Catalog

Design patterns are divided into three main categories, based on their purpose and the problems they address. These categories are:

  1. Creational Design Patterns: These patterns are concerned with the process of object creation. They help to abstract the instantiation logic and make object creation more flexible, efficient, and maintainable. Common creational design patterns include:
    • Singleton
    • Factory Method
    • Abstract Factory
    • Builder
    • Prototype
  2. Structural Design Patterns: These patterns focus on the composition of classes and objects, as well as the relationships between them. They simplify complex structures and improve code organization, reusability, and flexibility. Common structural design patterns include:
    • Adapter
    • Bridge
    • Composite
    • Decorator
    • Facade
    • Flyweight
    • Proxy
  3. Behavioral Design Patterns: These patterns deal with the interaction and communication between objects. They define the ways objects collaborate to fulfill specific tasks, making the communication between objects more efficient, organized, and flexible. Common behavioral design patterns include:
    • Chain of Responsibility
    • Command
    • Interpreter
    • Iterator
    • Mediator
    • Memento
    • Observer
    • State
    • Strategy
    • Template Method
    • Visitor

Each category of design patterns provides solutions to specific types of problems and promotes best practices in object-oriented software design. By understanding and applying these patterns, you can create code that is more maintainable, flexible, and scalable.

Creational Design Pattern:

Here is a list of common creational design patterns:

  • Singleton: Ensures that a class has only one instance and provides a global point of access to that instance. This pattern is useful when you need to control the number of instances or share resources within the system.
  • Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate. This pattern allows a class to defer instantiation to its subclasses.
  • Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is useful when you need to create a set of objects that work together.
  • Builder: Separates the construction of a complex object from its representation, allowing the same construction process to create different representations. This pattern is helpful when you need to create complex objects with many components or properties.
  • Prototype: Specifies the kind of objects to create using a prototypical instance and creates new objects by copying the prototype. This pattern is useful when object creation is expensive or complicated, and you want to avoid the overhead of creating a new instance each time.

Structural Design Pattern:

Here is a list of common structural design patterns:

  1. Adapter: Allows classes with incompatible interfaces to work together by converting the interface of one class into an interface expected by the clients. This pattern is useful when you need to make unrelated classes work together or when you want to introduce a new interface to an existing system without changing the original code.
  2. Bridge: Decouples an abstraction from its implementation so that the two can vary independently. This pattern is helpful when you want to avoid a permanent binding between an abstraction and its implementation, especially in cases where the implementation might need to be selected or switched at runtime.
  3. Composite: Composes objects into tree structures to represent part-whole hierarchies. This pattern allows clients to treat individual objects and compositions of objects uniformly, simplifying the handling of hierarchical structures.
  4. Decorator: Attaches additional responsibilities to an object dynamically without altering its structure. This pattern is a flexible alternative to subclassing for extending functionality and can be applied to objects at runtime.
  5. Facade: Provides a unified interface to a set of interfaces in a subsystem, making the subsystem easier to use. This pattern is useful when you want to simplify access to a complex subsystem or when you need to provide a high-level interface to a subsystem with multiple layers.
  6. Flyweight: Uses sharing to support a large number of fine-grained objects efficiently. This pattern is helpful when you have a large number of similar objects that consume a lot of memory and you want to reduce memory usage.
  7. Proxy: Provides a surrogate or placeholder for another object to control access to it. This pattern is useful when you need to add a level of indirection, such as providing access control, caching, or lazy initialization.

Behavioral Design Pattern:

Here is a list of common behavioral design patterns:

  1. Chain of Responsibility: Allows an object to pass a request along a chain of potential handlers until one of them handles the request. This pattern helps you decouple the sender of a request from its receiver by giving multiple objects a chance to handle the request.
  2. Command: Encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. This pattern is useful when you want to separate the object that invokes an operation from the object that performs the operation.
  3. Interpreter: Defines a representation for a grammar and provides an interpreter to process sentences in the language. This pattern is helpful when you need to parse and evaluate expressions that match a particular grammar or when you want to represent a complex structure as a language.
  4. Iterator: Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. This pattern is useful when you want to traverse a collection of objects without exposing the details of the underlying data structure.
  5. Mediator: Defines an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly. This pattern is helpful when you have a complex system of objects with many interdependencies and want to simplify communication and control between them.
  6. Memento: Captures and externalizes an object’s internal state so that the object can be restored to this state later. This pattern is useful when you need to implement undo/redo functionality or save the state of an object for later use.
  7. Observer: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This pattern is helpful when you have an object that needs to inform other objects about changes in its state.
  8. State: Allows an object to alter its behavior when its internal state changes. This pattern is useful when you want an object to change its behavior depending on its current state, without using large conditional statements or switch cases.
  9. Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows you to select an algorithm at runtime and is useful when you have multiple ways to perform an operation and want to choose the most appropriate one based on the context.
  10. Template Method: Defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. This pattern allows subclasses to redefine certain steps of an algorithm without changing the algorithm’s structure.
  11. Visitor: Represents an operation to be performed on the elements of an object structure without changing the classes on which it operates. This pattern is useful when you need to perform various operations on a set of objects with different types, and you want to avoid adding these operations to the classes themselves.

Leave a Reply

Back To Top