Applying the Open-Closed principle of software design using Strategy Design Pattern

Applying the Open-Closed principle of software design using Strategy Design Pattern

Open-Closed Principle in Software Design

The Open/Closed Principle is one of the SOLID principles of object-oriented design. SOLID is a set of five design principles mentioned below

  1. Single Responsibility Principle (SRP)
  2. Open-Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

These principles help software developers design robust, testable, extensible, and maintainable object-oriented software systems.

In this article, we will be discussing the Open-Closed principle using a Strategy Design Pattern. The Open-Closed Principle states:

"Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification."

In other words, once a class has been defined and implemented, its behavior should be extendable without modifying its source code. New functionality should be added through the introduction of new code (such as subclasses or modules) rather than by changing the existing code. This ensures that existing code remains stable and does not introduce unintended side effects.

Software Design Patterns

Software design patterns are proven solutions to recurring design problems encountered in software development. These patterns encapsulate years of collective experience and wisdom from seasoned developers, offering well-established templates for creating robust, scalable, and maintainable software. By providing a shared vocabulary and set of guidelines, design patterns facilitate effective communication and collaboration among software engineers. Design Patterns are generally classified into the following types.

  1. Creational Design Patterns: provide object creation mechanisms that increase flexibility and reuse of existing code. Examples: singleton, factory, abstract factory, builder, prototype, etc.
  2. Structural Design Patterns: explain how to assemble objects and classes into larger structures, while keeping these structures flexible and efficient. Examples: adapter, decorator, composite, proxy, bridge, etc.
  3. Behavioral Design Patterns: take care of effective communication and the assignment of responsibilities between objects. Examples: observer, strategy, command, state, chain of responsibility, etc.
  4. Architectural Patterns: offer high-level solutions for organizing and structuring entire applications. Examples: MVC, microservice, event-driven, layers, CQRS, SOA, serverless, etc.
  5. Concurrency Patterns: address challenges related to managing concurrent green threads(coroutine), threads, and processes. Examples: producer-consumer, reader-writer, thread-pool, semaphore, mutex, fork-join, etc.

Embracing design patterns promotes code flexibility, reusability, and adherence to best practices, ultimately leading to more efficient development processes and higher-quality software products.

In this article, we will only discuss the strategy design pattern.

Strategy Design Pattern

The Strategy Design Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each algorithm within a separate class, and makes them interchangeable. The pattern allows the client to choose the appropriate algorithm at runtime.

The strategy design pattern has 3 components.

  1. Context: defines the interface that the client uses to interact with different algorithms or strategies.
  2. Strategy Interface: declares an interface common to all supported algorithms or strategies.
  3. Strategy Implementations: implements the algorithm defined by the Strategy interface.

Let's consider a scenario that makes use of a strategy design pattern:

Problem Statement: In a software company, there are web developers who work on various frameworks based on various programming languages. When they finish developing web applications, they have to use a tool/software that helps them set up the application server environment based on their selected framework. After some time, the company hires new developers who work on a new framework. In this case, the feature addition to your software should be easy and hassle-free.

Now your job is to design and develop a tool/software that helps them do so easily. So how do you design and develop such software?

Now your job is to design and develop a tool/software that helps them do so easily. So how do you design and develop such software?

Problem Analysis and Planning Software Design:

  • Here developers use different frameworks so your software should be able to configure the server accordingly. But the general steps to do so are the same for all. These general steps can be represented by the Strategy Interface.
  • Although the general steps to configure the server can be represented by a common name, the actual implementations of the steps are different for different frameworks. So our software should have different strategy implementations based on different frameworks.
  • Since our software should have a feature to select a specific framework(strategy) at runtime, we have to make a provision to set the strategy and do the necessary steps present in the selected strategy to successfully configure the server environment for that framework. This responsibility is written in the Context Class component of the strategy design pattern.
  • Finally, we should be able to extend the feature instead of modifying the existing source code in addition to a new strategy(framework in our case). This principle in software engineering is called the Open-Closed principle: Open for extension but closed for modification, which was already discussed earlier. The strategy design pattern supports the Open-Closed principle by allowing the extension of features without modifying existing features.

Starting Software Development

Writing Code for each component mentioned above

Since we have already analyzed the problem and broken it down into the components of the Strategy Design Pattern, let's start writing codes for each component.

Starting Software Development

Writing Code for each component mentioned above

Since we have already analyzed the problem and broken it down into the components of the Strategy Design Pattern, let's start writing codes for each component.

Defining Strategy Interface

from abc import ABC, abstractmethod


class FrameworkInterface(ABC):
    def __init__(self, framework_name: str, language: str) -> None:
        self.framework_name = framework_name
        self.language = language

    @abstractmethod
    def setup_server(self) -> None:
        """ installing language and dependencies """
        
    @abstractmethod
    def run_server(self, host: str, port: int) -> None:
        """ running {self.framework_name} server at https://{host}:{port} """

        

Implementing Framework Implementations (for now Django and Spring)

class DjangoFramework(FrameworkInterface):
    def setup_server(self) -> None:
        print("Python installed successfully...")
        print("Django installed successfully...\n")

    def run_server(self, host: str, port: int) -> None:
        print(f"Starting {self.framework_name} server at https://{host}:{port} ...\n")


class SpringFramework(FrameworkInterface):
    def setup_server(self) -> None:
        print("Java installed successfully...")
        print("Spring installed successfully...\n")

    def run_server(self, host: str, port: int) -> None:
        print(f"Starting {self.framework_name} server at https://{host}:{port} ...\n")        

Defining Context

class FrameworkContext:
    def __init__(self, framework: FrameworkInterface) -> None:
        self.framework = framework
        print(f"{self.framework.framework_name} choosen.\n")

    def setup_server(self) -> None:
        self.framework.setup_server()

    def run_server(self, host: str, port: int) -> None:
        self.framework.run_server(host, port)        

Implementing the above components in the main function

def main() -> None:
    print("\nStarting our software\n----------")

    framework = DjangoFramework(framework_name="Django Framework", language="Python")
    manager = FrameworkContext(framework=framework)
    manager.setup_server()
    manager.run_server("127.0.0.1", 8080)

    framework = SpringFramework(framework_name="Spring Framework", language="Java")
    manager = FrameworkContext(framework=framework)
    manager.setup_server()
    manager.run_server("127.0.0.1", 9090)


if __name__ == '__main__':
    main()        

Joining them altogether

from abc import ABC, abstractmethod


class FrameworkInterface(ABC):
    def __init__(self, framework_name: str, language: str) -> None:
        self.framework_name = framework_name
        self.language = language

    @abstractmethod
    def setup_server(self) -> None:
        """ installing language and dependencies """
        
    @abstractmethod
    def run_server(self, host: str, port: int) -> None:
        """ running {self.framework_name} server at https://{host}:{port} """


class DjangoFramework(FrameworkInterface):
    def setup_server(self) -> None:
        print("Python installed successfully...")
        print("Django installed successfully...\n")

    def run_server(self, host: str, port: int) -> None:
        print(f"Starting {self.framework_name} server at https://{host}:{port} ...\n")


class SpringFramework(FrameworkInterface):
    def setup_server(self) -> None:
        print("Java installed successfully...")
        print("Spring installed successfully...\n")

    def run_server(self, host: str, port: int) -> None:
        print(f"Starting {self.framework_name} server at https://{host}:{port} ...\n")


class FrameworkContext:
    def __init__(self, framework: FrameworkInterface) -> None:
        self.framework = framework
        print(f"\n{self.framework.framework_name} choosen.\n")

    def setup_server(self) -> None:
        self.framework.setup_server()

    def run_server(self, host: str, port: int) -> None:
        self.framework.run_server(host, port)



def main() -> None:
    print("\nStarting our software\n----------")

    framework = DjangoFramework(framework_name="Django Framework", language="Python")
    manager = FrameworkContext(framework=framework)
    manager.setup_server()
    manager.run_server("127.0.0.1", 8080)

    framework = SpringFramework(framework_name="Spring Framework", language="Java")
    manager = FrameworkContext(framework=framework)
    manager.setup_server()
    manager.run_server("127.0.0.1", 9090)


if __name__ == '__main__':
    main()        

After running the above program, the output will be as shown below

Starting our software
----------

Django Framework choosen.

Python installed successfully...
Django installed successfully...

Starting Django Framework server at https://127.0.0.1:8080 ...


Spring Framework choosen.

Java installed successfully...
Spring installed successfully...

Starting Spring Framework server at https://127.0.0.1:9090 ...        

Now in the final case, what if we have to add a new feature/strategy to it? Assume we have a new development team working in the Axum framework (One of the popular web frameworks written in Rust Programming Language). Let's incorporate the new feature in the above codebase.

from abc import ABC, abstractmethod


class FrameworkInterface(ABC):
    def __init__(self, framework_name: str, language: str) -> None:
        self.framework_name = framework_name
        self.language = language

    @abstractmethod
    def setup_server(self) -> None:
        """ installing language and dependencies """
        
    @abstractmethod
    def run_server(self, host: str, port: int) -> None:
        """ running {self.framework_name} server at https://{host}:{port} """


class DjangoFramework(FrameworkInterface):
    def setup_server(self) -> None:
        print("Python installed successfully...")
        print("Django installed successfully...\n")

    def run_server(self, host: str, port: int) -> None:
        print(f"Starting {self.framework_name} server at https://{host}:{port} ...\n")


class SpringFramework(FrameworkInterface):
    def setup_server(self) -> None:
        print("Java installed successfully...")
        print("Spring installed successfully...\n")

    def run_server(self, host: str, port: int) -> None:
        print(f"Starting {self.framework_name} server at https://{host}:{port} ...\n")


class AxumFramework(FrameworkInterface):
    def setup_server(self) -> None:
        print("Rust installed successfully...")
        print("Axum installed successfully...\n")

    def run_server(self, host: str, port: int) -> None:
        print(f"Starting {self.framework_name} server at https://{host}:{port} ...\n")


class FrameworkContext:
    def __init__(self, framework: FrameworkInterface) -> None:
        self.framework = framework
        print(f"\n{self.framework.framework_name} choosen.\n")

    def setup_server(self) -> None:
        self.framework.setup_server()

    def run_server(self, host: str, port: int) -> None:
        self.framework.run_server(host, port)



def main() -> None:
    print("\nStarting our software\n----------")

    framework = DjangoFramework(framework_name="Django Framework", language="Python")
    manager = FrameworkContext(framework=framework)
    manager.setup_server()
    manager.run_server("127.0.0.1", 8080)

    framework = SpringFramework(framework_name="Spring Framework", language="Java")
    manager = FrameworkContext(framework=framework)
    manager.setup_server()
    manager.run_server("127.0.0.1", 9090)

    framework = AxumFramework(framework_name="Axum Framework", language="Rust")
    manager = FrameworkContext(framework=framework)
    manager.setup_server()
    manager.run_server("127.0.0.1", 9000)


if __name__ == '__main__':
    main()        

If you observe the code above carefully, we have just extended the FrameworkInterface by writing a new AxumFramework class without modifying any of the existing framework implementations and the context class. Developers now can select the Axum framework in the main function(or their implementation block), inject them into the FrameworkContext instance, and set up and run the Axum-made webserver using the context.

The output of the above program would be

Starting our software
----------

Django Framework choosen.

Python installed successfully...
Django installed successfully...

Starting Django Framework server at https://127.0.0.1:8080 ...


Spring Framework choosen.

Java installed successfully...
Spring installed successfully...

Starting Spring Framework server at https://127.0.0.1:9090 ...


Axum Framework choosen.

Rust installed successfully...
Axum installed successfully...

Starting Axum Framework server at https://127.0.0.1:9000 ...        

So in this way Strategy design pattern supports the Open-Closed design principle.

Having some advantages of the Strategy Design Pattern and Open-Closed design principle, they also have some disadvantages. For example, Increased Number of Classes, Increased code coupling, object proliferation, etc. Software Engineering involves numerous trade-offs. So getting advantages from one way of implementation may introduce other disadvantages and we have to choose which better suits our use-case scenarios.

Thank you so much for reading this article. I am hoping this aids something in your software engineering journey. If you have any doubts or feedback related to this topic and example, you can have a comment below.

Namaste!!!






要查看或添加评论,请登录

Dipak Niroula的更多文章

  • Writing Asynchronous Code in Python

    Writing Asynchronous Code in Python

    Some terminologies and concepts to understand Concurrency and Parallelism: Concurrency is the ability of a program to…

  • Dependency Inversion Principle

    Dependency Inversion Principle

    Introduction Dependency Inversion is a principle in software design that is part of the SOLID principles, which aim to…

  • Generator in Python with examples

    Generator in Python with examples

    What is Generator? A generator in Python is a special type of function that is used to create an iterable object or…

    4 条评论
  • Improving Code Flexibility and Maintainability with Wrapper Classes in Python

    Improving Code Flexibility and Maintainability with Wrapper Classes in Python

    Background In the software development process, it is often necessary to use third-party modules/libraries to…

  • Lambda Function in Python

    Lambda Function in Python

    A lambda function is a small anonymous function that can be defined inline without a name. It takes arguments and has…

  • Decorators in Python by my examples and ChatGPT explanation

    Decorators in Python by my examples and ChatGPT explanation

    The Decorator is one of the most important, powerful, flexible, and useful feature of the Python programming language…

    2 条评论

社区洞察

其他会员也浏览了