Design mode - opening and closing principle

Opening and closing principle

summary

In the software field, we sometimes encounter some problems in the process of writing code, which have been summarized by predecessors. Help us how to write more elegant code, more concise code and more non repetitive code when we encounter problems. So there are many design principles, which can help us write more elegant code and deal with possible changes in the code in the future. Realize the characteristics of low coupling, high cohesion and code reuse. Today, we will learn one of the principles of software design. The principle of opening and closing seems simple, but sometimes it will also produce the smell of over design.

No more nonsense, get to the point.

Opening and closing principle, English abbreviation OCP, full name open closed principle

Original definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.

If there is a change in the program, there will be a chain reaction, resulting in a series of related module changes, then the design has a rigid smell. OCP suggests that we should reconstruct the system, so that when we make such changes to the system in the future, it will not lead to more modifications. If OCP can be applied correctly, each time the system is modified, it only expands the code, not modifies the original source code. In this way, the code that has run well does not need to be changed. This can avoid a lot of testing, as long as testing new classes, modules and functions.

Of course, the ideal is beautiful and the reality is cruel.
##OCP has two main features

Open for extension

The behavior of the module can be extended. When the requirements of the application change, we can extend the module to meet the new requirements. In other words, I can change the function of the module.

Closed for modification

For module expansion, there is no need to change the source code or binary code of the module. The binary executable version of the module, whether it is a linkable library, DLL or Java jar file, does not need to be modified.

In fact, how to ensure the above two principles? A seemingly contradictory principle, in fact, it is not. The key point is that if we can know the future changes, we can abstract the possible changes in the future in advance, so as to isolate the changes. In this way, when future requirements change, you only need to generate subclasses from the abstraction layer. The original code does not need to be changed, and new functions can be added to meet the requirements.

give an example

There are different shapes, and then each shape has its own drawing method, and there is a draw_all_shapes this function is used to draw all shapes

# -*- coding: utf-8 -*-
"""
@Time    : 2022/1/22 11:20
@Author  : Frank
@File    : ocp.py
"""
from enum import Enum

from typing import List, Union


class ShapeType(Enum):
    Circle = 0
    Square = 1


class Circle:
    """
    Circular class
    """

    def __init__(self, its_type):
        self.its_type = its_type

    def draw(self):
        print("draw circle ...")
        pass


class Square:
    """
    square
    """

    def __init__(self, its_type):
        self.its_type = its_type

    def draw(self):
        print("draw square ...")


def draw_square(square):
    print("==draw square begin==")
    square.draw()
    print("==draw square done==")


def draw_cirle(circle):
    print("==draw circle begin==")
    circle.draw()
    print("==draw circle done==")


def draw_all_shapes(shapes: List[Union[Circle, Square]], n: int):
    """

    :param shapes: Shape list
    :param n:  Length of shape
    :return:
    """
    for i in range(n):
        cur_type = shapes[i].its_type
        if cur_type == ShapeType.Circle:
            draw_cirle(shapes[i])
            pass
        elif cur_type == ShapeType.Square:
            draw_square(shapes[i])
        else:
            print('warning:unknown shape')


def main():
    s1 = Square(ShapeType.Square)
    s2 = Square(ShapeType.Square)
    c1 = Circle(ShapeType.Circle)
    c2 = Circle(ShapeType.Circle)

    shapes = [s1, s2, c1, c2]

    draw_all_shapes(shapes, len(shapes))

if __name__ == '__main__':
    main()

Let's see if there is any problem with this code?

This is a typical process oriented way of thinking. First, you need to modify the code draw to add a new shape_ all_ In the shapes function, judge the if statement, and then add a new type of ShapeType. In this way, we need to change the source code to expand the program. Suppose you have a triangle shape and write a function draw for the triangle_ Triangle this obviously violates what we call the OCP principle.

How to modify it? How to make the program easier to expand the program when adding new shapes? If this change is what we expect, and there may be many shapes to be added in the future, such as fan-shaped, Pentagon, hexagon and so on.. Then we need to redesign a better way to solve the problem of change.

First, think about the direction of this change?

The direction of change is that new shapes will be added in the future and will be drawn.

Improved code

Our main method is to create an abstraction layer. What is this abstraction layer? If the shape needs to be added and drawn, I can abstract a class and define the drawing method.

Here, you can abstract a Shape base class, then have an interface draw, and then derive different subclasses. In this way, each time we add a new Shape, we can create a new subclass without the need for code before making changes.

from abc import abstractmethod
from enum import Enum

from typing import List


class ShapeType(Enum):
    Circle = 0
    Square = 1


class Shape:
    """
    Shape class

    """

    def __init__(self, its_type):
        self.its_type = its_type

    @abstractmethod
    def draw(self):
        pass

Derived classes. Here are different derived classes, circle and square

class Circle(Shape):
    """
    Circular class
    """
    def __init__(self, its_type):
        super().__init__(its_type)

    def draw(self):
        print("begin draw circle")
        pass


class Square(Shape):
    """
    square
    """

    def __init__(self, its_type):
        super().__init__(its_type)

    def draw(self):
        print("begin draw square")
        pass

There is also a draw_all_shapes method

def draw_all_shapes(shapes: List[Shape], n: int):
    for i in range(n):
        shapes[i].draw()

        
        
def main():
    s1 = Square(ShapeType.Square)
    s2 = Square(ShapeType.Square)
    c1 = Circle(ShapeType.Circle)
    c2 = Circle(ShapeType.Circle)

    shapes = [s1, s2, c1, c2]

    draw_all_shapes(shapes, len(shapes))
    

All right, that's it. After this change, every time a new Shape is added, we only need to inherit a Shape base class and implement the draw method. In the main function, add a Shape. After this modification, the function can be extended, and the modification only adds new subclasses, which is in line with OCP Changes to it are made by adding new code, not by changing existing code Therefore, it will not cause chain changes like the program The only change required is the addition of new modules

Is this the perfect code?

Great, very perfect, which solves the problem of adding different shapes in the future Has he really solved the problem?

Is this program 100% closed? However, I lied, not completely closed!

Suppose there is a requirement that the circle must be drawn in front of the square. To realize this requirement, we must modify the draw_all_shapes implementation, so that it first draws a circle and then draws a square If we anticipate this change, we can design an abstraction to isolate it For the above abstraction, the order of drawing graphics is suddenly required, which becomes an obstacle What else do you think is more appropriate to define a Shape class and derive circle and square classes?

This design is not so appropriate for a system where the order of shapes is more meaningful than the type of shape

This leads to a troublesome result. Generally speaking, no matter how closed the module design is, there will be some changes that cannot be closed, and there is no design appropriate to all changes Since we can't cope with all the changes, we must make a choice about which changes are closed Or predict the most likely change, and then construct an abstraction to isolate the change

So you may ask, how can I know this change in advance? How do I know the changing direction of users' needs?

My idea is to give the change to the market or users First, release the 0.1 beta version to see the response of the market or users Let users use the software as early as possible and demonstrate the software to customers and users as often as possible Users may put forward various requirements, which is the point of isolating changes in your design In fact, this is a bit similar to agile development Use the principle of running fast in small steps and seek more feedback from users Find the direction of software change in time

There is an old saying that if you fool me once, you should be ashamed. If you fool me again, I should be ashamed

Software design also abides by such principles Find out in advance and find out the changes of users as soon as possible You can build an abstraction layer in advance to isolate changes

Now the first bullet has hit us

Well, now users need to draw a square before drawing a circle So how do we isolate this change? To prevent being hit by a second bullet?

Now, assuming that I have predicted that users may have requirements for order, let's redesign the Shape class and add a weight variable in the Shape class to represent the priority of drawing in the future. The larger the number, the more priority it will be drawn

Then add magic methods to the Shape class__ gt__ And__ eq__ These two magic methods In this way, you can customize the sorting

After that, the sort operation in the list of shapes can be sorted according to our regulations. Then enter draw_all_shapes function

# -*- coding: utf-8 -*-
from abc import abstractmethod
from enum import Enum
from functools import total_ordering
from typing import List


class ShapeType(Enum):
    Circle = 0
    Square = 1


@total_ordering
class Shape:
    """
    Shape class

    """

    def __init__(self, its_type, weight=0):
        """

        :param its_type:
        :param weight: int type,The number size represents the order of painting in the future, 0,1,2,3.. The larger the number, the more priority is drawn.
        """
        self.its_type = its_type
        self.weight = weight

    @abstractmethod
    def draw(self):
        pass

    # Magic method to realize custom comparison
    def __gt__(self, other):
        return self.weight < other.weight

    def __eq__(self, other):
        return self.weight == other.weight


class Circle(Shape):
    """
    Circular class
    """

    def __init__(self, its_type, weight: int):
        super().__init__(its_type)
        self.weight = weight

    def draw(self):
        print(f"begin draw circle,{self.weight}")
        pass


class Square(Shape):
    """
    square
    """

    def __init__(self, its_type, weight: int):
        super().__init__(its_type)
        self.weight = weight

    def draw(self):
        print(f"begin draw square,{self.weight}")
        pass


def draw_all_shapes(shapes: List[Shape], n: int):
    for i in range(n):
        shapes[i].draw()


def main():
    s1 = Square(ShapeType.Square, 0)
    s2 = Square(ShapeType.Square, 0)
    s3 = Square(ShapeType.Square, 0)

    c1 = Circle(ShapeType.Circle, 1)
    c2 = Circle(ShapeType.Circle, 1)

    shapes: list = [s1, s2, c1, c2, s3]
    ordered_shapes = sorted(shapes)
    draw_all_shapes(ordered_shapes, len(shapes))


if __name__ == '__main__':
    main()

The results are as follows:

It can be seen that it is possible to draw a circle first and then a square And if new shapes are added in the future, we only need to inherit Shape and specify a weight attribute to realize the extension without modifying the original code, but only adding a class This is in line with OCP

Core idea of OCP

The core idea of OCP is that if the software changes or has new requirements in the future, we hope to expand the original module instead of modifying the source code to expand the function of the code It is open to extension and closed to change

summary

In many ways, OCP is the core of object-oriented design. Adhering to this principle can bring great benefits claimed by object-oriented technology (flexibility, reusability, and maintainability) however, it does not mean that as long as the object-oriented language is carried out, it is also a bad idea to arbitrarily abstract every part of the application It's a good idea to abstract when changes occur to prevent being hit by a second bullet In other words, only the parts that change frequently are abstracted We should reject immature abstraction or premature abstraction to prevent the code from becoming the smell of over design

Reference documents

Agile software development: principles, patterns, practices

Principle of small talk design mode: opening and closing Principle OCP

Share happiness and keep moving. " 2022-01-24 18:49:53' --frank

Keywords: Design Pattern

Added by Daisy Cutter on Mon, 24 Jan 2022 20:12:34 +0200