Learn the design pattern "Decorator" in Python

As a material for learning GoF design patterns, the book "Introduction to Design Patterns Learned in the Augmented and Revised Java Language" seems to be helpful. However, since the examples taken up are based on JAVA, I tried the same practice in Python to deepen my understanding.

■ Decorator (decorator pattern)

The Decorator pattern is one of the design patterns defined by GoF (Gang of Four; 4 gangs). This pattern allows you to dynamically add new features and behaviors to existing objects.

UML class and sequence diagram W3sDesign_Decorator_Design_Pattern_UML.jpg UML class diagram decorator.png (Quoted from Wikipedia)

□ Memorandum

It is a design pattern that makes objects more and more decorated. If you apply cream to a sponge cake and put strawberries on it, it will be like a strawberry shortcake. You often see it when you're involved in Python programming.

■ "Decorator" sample program

I'd like to actually run a sample program that utilizes the Decorator pattern and see how it works:

--Display the string Hello, world. as it is --Insert the # character before and after the character string Hello, world. to display it. --Insert the # character before and after the character string Hello, world., and display it by surrounding it with a frame. --Enclose the character string HELLO with a frame, insert*characters before and after it, and then enclose it twice with a frame ... (Hereafter, omitted)

$ python Main.py 
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

/+-----------+/
/|+---------+|/
/||*+-----+*||/
/||*|HELLO|*||/
/||*+-----+*||/
/|+---------+|/
/+-----------+/

It looks like a tower of Hanoi.

■ Details of sample program

Similar code has been uploaded to the Git repository. https://github.com/ttsubo/study_of_design_pattern/tree/master/Decorator/step1

--Directory structure

.
├── Main.py
└── decorator
    ├── __init__.py
    ├── border.py
    └── display.py

(1) Role of Component

It is a core when adding functions. What is the role of Component? Only define the interface of the sponge cake. In the sample program, the Display class serves this role.

decorator/display.py


from abc import ABCMeta, abstractmethod

class Display(metaclass=ABCMeta):
    @abstractmethod
    def getColumns(self):
        pass

    @abstractmethod
    def getRows(self):
        pass

    @abstractmethod
    def getRowText(self, row):
        pass

    def show(self):
        for i in range(self.getRows()):
            print(self.getRowText(i))

... (snip)

(2) Role of Concrete Component

A concrete sponge cake that implements the interface for the role of Component. In the sample program, the StringDisplay class serves this role.

decorator/display.py


class StringDisplay(Display):
    def __init__(self, string):
        self.string = string

    def getColumns(self):
        return len(self.string)

    def getRows(self):
        return 1

    def getRowText(self, row):
        if row == 0:
            return self.string
        else:
            return None

(3) The role of Decorator

It has the same interface as the Component role. And, in addition, it has the Component role that this Decorator role decorates. This role "knows" what you are decorating. In the sample program, the Border class serves this role.

decorator/border.py


from decorator.display import Display

class Border(Display):
    def __init__(self, display):
        self.display = display

(4) The role of Concrete Decorator

It's a specific role of Decorator. In the sample program, the SideBorder and FullBorder classes serve this role.

decorator/border.py


class SideBorder(Border):
    def __init__(self, display, ch):
        super(SideBorder, self).__init__(display)
        self.__borderChar = ch

    def getColumns(self):
        return 1 + self.display.getColumns() + 1

    def getRows(self):
        return self.display.getRows()

    def getRowText(self, row):
        return self.__borderChar + self.display.getRowText(row) + self.__borderChar

decorator/border.py


class FullBorder(Border):
    def __init__(self, display):
        super(FullBorder, self).__init__(display)

    def getColumns(self):
        return 1 + self.display.getColumns() + 1

    def getRows(self):
        return 1 + self.display.getRows() + 1

    def getRowText(self, row):
        if row == 0:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        elif row == self.display.getRows() + 1:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        else:
            return '|' + self.display.getRowText(row - 1) + '|'

    def __make_line(self, char, count):
        buf = ''
        for _ in range(count):
            buf += char
        return buf

(5) The role of Client

In the sample program, the startMain method serves this role.

Main.py


from decorator.display import StringDisplay
from decorator.border import SideBorder, FullBorder

def startMain():
    b1 = StringDisplay('Hello, world.')
    b2 = SideBorder(b1, '#')
    b3 = FullBorder(b2)
    b1.show()
    b2.show()
    b3.show()
    print("")
    b4 = SideBorder(
        FullBorder(
            FullBorder(
                SideBorder(
                    FullBorder(
                        StringDisplay('HELLO')
                    ),
                    '*'
                )
            )
        ),
        '/'
    )
    b4.show()

if __name__ == '__main__':
    startMain()

□ Memorandum (Try using the Python decorator!) -Part 1-

In Python programming, I often see decorators, so I'd like to rewrite the startMain method part with a ** Python decorator **.

Similar code has been uploaded to the Git repository. https://github.com/ttsubo/study_of_design_pattern/tree/master/Decorator/step2

decorator/display.py


from abc import ABCMeta, abstractmethod

className = []

class Display(metaclass=ABCMeta):
    @abstractmethod
    def getColumns(self):
        pass

    @abstractmethod
    def getRows(self):
        pass

    @abstractmethod
    def getRowText(self, row):
        pass

    def show(self):
        for i in range(self.getRows()):
            print(self.getRowText(i))

class StringDisplay(Display):
    def __init__(self, func):
        self.string = "Hello, world."
        self.func = func
        super(StringDisplay, self).__init__()

    def getColumns(self):
        return len(self.string)

    def getRows(self):
        return 1

    def getRowText(self, row):
        if row == 0:
            return self.string
        else:
            return None
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()

decorator/border.py


from decorator.display import Display, className

class Border(Display):
    def __init__(self, display):
        self.display = display
        super(Border, self).__init__()

class SideBorder(Border):
    def __init__(self, func):
        self.func = func
        self.__borderChar = '#'
        super(SideBorder, self).__init__(func)

    def getColumns(self):
        return 1 + self.display.getColumns() + 1

    def getRows(self):
        return self.display.getRows()

    def getRowText(self, row):
        return self.__borderChar + self.display.getRowText(row) + self.__borderChar
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()

class FullBorder(Border):
    def __init__(self, func):
        self.func = func
        super(FullBorder, self).__init__(func)

    def getColumns(self):
        return 1 + self.display.getColumns() + 1

    def getRows(self):
        return 1 + self.display.getRows() + 1

    def getRowText(self, row):
        if row == 0:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        elif row == self.display.getRows() + 1:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        else:
            return '|' + self.display.getRowText(row - 1) + '|'

    def __make_line(self, char, count):
        buf = ''
        for _ in range(count):
            buf += char
        return buf

    def __call__(self):
        global className
        className.append(self)
        return self.func()

Main.py


from decorator.display import StringDisplay, className
from decorator.border import FullBorder, SideBorder

def startMain():
    @FullBorder
    @SideBorder
    @StringDisplay
    def dummy1():
        className[2].show()
        className[1].show()
        className[0].show()
    dummy1()
    print("")
    className.clear()

    @SideBorder
    @FullBorder
    @FullBorder
    @SideBorder
    @FullBorder
    @StringDisplay
    def dummy2():
        className[0].show()
    dummy2()

if __name__ == '__main__':
    startMain()

I will move it.

$ python Main.py 
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

#+-------------------+#
#|+-----------------+|#
#||#+-------------+#||#
#||#|Hello, world.|#||#
#||#+-------------+#||#
#|+-----------------+|#
#+-------------------+#

In this method, ** the border character is embedded with # and the character string Hello, world. is embedded, which results in a loss of programming flexibility. .. ** **

□ Memorandum (Try using the Python decorator!) -Part 2-

Let's improve the way the Python decorator works so that you can specify border characters and strings in a timely manner. I refer to this [Qiita article "Understanding how WSGI applications work with OpenStack" (https://qiita.com/ttsubo/items/0a1abecce272559edc94).

Similar code has been uploaded to the Git repository. https://github.com/ttsubo/study_of_design_pattern/tree/master/Decorator/step3

decorator/display.py


from abc import ABCMeta, abstractmethod

className = []

class Display(metaclass=ABCMeta):
    @abstractmethod
    def getColumns(self):
        pass

    @abstractmethod
    def getRows(self):
        pass

    @abstractmethod
    def getRowText(self, row):
        pass

    def show(self):
        for i in range(self.getRows()):
            print(self.getRowText(i))

class StringDisplay(Display):
    def __init__(self, func, string):
        self.string = string
        self.func = func
        super(StringDisplay, self).__init__()

    def getColumns(self):
        return len(self.string)

    def getRows(self):
        return 1

    def getRowText(self, row):
        if row == 0:
            return self.string
        else:
            return None
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()

def StringDisplay_filter(string):
    def filter(func):
        return StringDisplay(func, string)
    return filter

decorator/border.py


from decorator.display import Display, className

class Border(Display):
    def __init__(self, display):
        self.display = display
        super(Border, self).__init__()

class SideBorder(Border):
    def __init__(self, func, ch):
        self.func = func
        self.__borderChar = ch
        super(SideBorder, self).__init__(func)

    def getColumns(self):
        return 1 + self.display.getColumns() + 1

    def getRows(self):
        return self.display.getRows()

    def getRowText(self, row):
        return self.__borderChar + self.display.getRowText(row) + self.__borderChar
    
    def __call__(self):
        global className
        className.append(self)
        return self.func()

def SideBorder_filter(ch):
    def filter(func):
        return SideBorder(func, ch)
    return filter

class FullBorder(Border):
    def __init__(self, func):
        self.func = func
        super(FullBorder, self).__init__(func)

    def getColumns(self):
        return 1 + self.display.getColumns() + 1

    def getRows(self):
        return 1 + self.display.getRows() + 1

    def getRowText(self, row):
        if row == 0:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        elif row == self.display.getRows() + 1:
            return '+' + self.__make_line('-', self.display.getColumns()) + '+'
        else:
            return '|' + self.display.getRowText(row - 1) + '|'

    def __make_line(self, char, count):
        buf = ''
        for _ in range(count):
            buf += char
        return buf

    def __call__(self):
        global className
        className.append(self)
        return self.func()

Main.py


from decorator.display import StringDisplay_filter, className
from decorator.border import SideBorder_filter, FullBorder

def startMain():
    @FullBorder
    @SideBorder_filter('#')
    @StringDisplay_filter("Hello, world.")
    def dummy1():
        className[2].show()
        className[1].show()
        className[0].show()
    dummy1()
    print("")
    className.clear()

    @SideBorder_filter('/')
    @FullBorder
    @FullBorder
    @SideBorder_filter('*')
    @FullBorder
    @StringDisplay_filter("HELLO")
    def dummy2():
        className[0].show()
    dummy2()

if __name__ == '__main__':
    startMain()

I will move it.

$ python Main.py 
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

/+-----------+/
/|+---------+|/
/||*+-----+*||/
/||*|HELLO|*||/
/||*+-----+*||/
/|+---------+|/
/+-----------+/

Since it is the same as the first operation result, it is completed for the time being.

■ Reference URL

-[Finishing "Introduction to Design Patterns Learned in Java Language" (Not)](https://medium.com/since-i-want-to-start-blog-that-looks-like-men-do/java Introduction to Design Patterns Learned in Language-Finishing-Not-2cc9b34a30b2) -Qiita article "Design Pattern #Facade

Recommended Posts

Learn the design pattern "Decorator" in Python
Learn the design pattern "Prototype" in Python
Learn the design pattern "Builder" in Python
Learn the design pattern "Flyweight" in Python
Learn the design pattern "Observer" in Python
Learn the design pattern "Memento" in Python
Learn the design pattern "Proxy" in Python
Learn the design pattern "Command" in Python
Learn the design pattern "Bridge" in Python
Learn the design pattern "Mediator" in Python
Learn the design pattern "Iterator" in Python
Learn the design pattern "Strategy" in Python
Learn the design pattern "Composite" in Python
Learn the design pattern "State" in Python
Learn the design pattern "Adapter" in Python
Learn the design pattern "Abstract Factory" in Python
Learn the design pattern "Template Method" in Python
Learn the design pattern "Chain of Responsibility" in Python
Learn the design pattern "Singleton" with Python
Learn the design pattern "Facade" with Python
Implement the Singleton pattern in Python
Singleton pattern in Python
Visitor pattern in Python
Decorator pattern in Java
Download the file in Python
Find the difference in Python
Learn cumulative sum in Python
Design Patterns in Python: Introduction
Learn exploration in Python # 1 Full exploration
Python Design Pattern --Template method
Python in the browser: Brython's recommendation
Save the binary file in Python
Hit the Sesami API in Python
Get the desktop path in Python
[Python] What is @? (About the decorator)
Get the script path in Python
In the python command python points to python3.8
Hit the web API in Python
I wrote the queue in Python
Calculate the previous month in Python
Examine the object's class in python
Get the desktop path in Python
Get the host name in Python
Access the Twitter API in Python
The first step in Python Matplotlib
I wrote the stack in Python
Master the weakref module in Python
Learn the basics of Python ① Beginners
Try using the Wunderlist API in Python
Check the behavior of destructor in Python
[Python Kivy] About changing the design theme
Try using the Kraken API in Python
Learn the basics while touching python Variables
Write the test in a python docstring
OR the List in Python (zip function)
GoF design pattern from the problem 2. Structure
Display Python 3 in the browser with MAMP
Tweet using the Twitter API in Python
Decorator to avoid UnicodeEncodeError in Python 3 print ()
Check if the URL exists in Python
Associate the table set in python models.py