"Principle of dependency reversal" learned slowly with Python

There are many texts on the "principle of dependency reversal",

For those who say, I will write my own article, "It was easy to understand if you explained this."

github

https://github.com/koboriakira/koboridip

What to make

A tool for checking the four arithmetic operations. Output the result to the CLI as follows:

$ python -m koboridip.main 8 2
8 + 2 = 10
8 - 2 = 6
8 * 2 = 16
8 / 2 = 4.0

Make as you were told. Version 1

Project structure

.
└── koboridip
    ├── calculator.py
    └── main.py

Source

calculator.py


class Calculator():
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b

    def print(self) -> None:
        print(f'add: {self.a + self.b}')
        print(f'subtract: {self.a - self.b}')
        print(f'multiply: {self.a * self.b}')
        print(f'divide: {self.a / self.b}')

main.py


import sys
from koboridip.calculator import Calculator

if __name__ == '__main__':
    #Get arguments
    a = sys.argv[1]
    b = sys.argv[2]

    #Create a Calculator instance
    calculator = Calculator(int(a), int(b))

    #Output the results of each of the four arithmetic operations
    calculator.print()

Description

It's a simple program. After giving a number to the Calculator class, let the instance do the" calculation (processing) "and" output ".

Sudden specification change. Version 2

Regarding this product, there was a request that "I want to save the output result in json format". Therefore, the source will be modified.

The output is written in the Calculator class, so let's fix it.

Source

calculator.py


import json
from typing import Dict


class Calculator():
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b

    def print(self) -> None:
        # print(f'add: {self.a + self.b}')
        # print(f'subtract: {self.a - self.b}')
        # print(f'multiply: {self.a * self.b}')
        # print(f'divide: {self.a / self.b}')
        result: Dict[str, int] = {
            "add": self.a + self.b,
            "subtract": self.a - self.b,
            "multiply": self.a * self.b,
            "divide": self.a / self.b
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))

Execution result

Just in case, when I run it, I get the following text in result.json (formatted):

result.json


{
   "add":10,
   "subtract":6,
   "multiply":16,
   "divide":4.0
}

Refactoring

The Calculator class performs ** processing ** of four arithmetic operations and ** output ** of the results.

I decided that it would be better to separate these, so I decided to create a Printer class that was in charge of output processing.

.
└── koboridip
    ├── calculator.py
    ├── main.py
    └── printer.py

printer.py


import json
from typing import Dict


class Printer():
    def print(self, add, subtract, multiply, divide) -> None:
        result: Dict[str, int] = {
            "add": add,
            "subtract": subtract,
            "multiply": multiply,
            "divide": divide
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))

calculator.py


from koboridip.printer import Printer


class Calculator():
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b

        printer = Printer()
        printer.print(add, subtract, multiply, divide)

Unpleasant premonition. Version 3

In the subsequent policy change, it was decided that "I want to use both the result output to the CLI and the storage in json format". Switch the mode as follows.

$ python -m koboridip.main 8 2 simple
>(Output to CLI)

$ python -m koboridip.main 8 2 json
> (result.output json)

Therefore, the Printer class has been divided into two types so that they can be switched.

Project structure

.
└── koboridip
    ├── calculator.py
    ├── json_printer.py ->Output in json format
    ├── main.py
    ├── simple_printer.py ->Output to CLI

Source

simple_printer.py


class SimplePrinter():
    def print(self, add, subtract, multiply, divide) -> None:
        print(f'add: {add}')
        print(f'subtract: {subtract}')
        print(f'multiply: {multiply}')
        print(f'divide: {divide}')

json_printer.py


import json
from typing import Dict


class JsonPrinter():
    def print(self, add, subtract, multiply, divide) -> None:
        result: Dict[str, int] = {
            "add": add,
            "subtract": subtract,
            "multiply": multiply,
            "divide": divide
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))

It is up to calculator.py to decide which one to output.

The specified string "simple" or "json" can be switched by storing it in the mode variable.

calculator.py


from koboridip.simple_printer import SimplePrinter
from koboridip.json_printer import JsonPrinter


class Calculator():
    def __init__(self, a: int, b: int, mode: str) -> None:
        self.a = a
        self.b = b
        self.mode = mode

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b

        #Switch the output method
        if self.mode == 'json':
            json_printer = JsonPrinter()
            json_printer.print(add, subtract, multiply, divide)
        elif self.mode == 'simple':
            simple_printer = SimplePrinter()
            simple_printer.print(add, subtract, multiply, divide)

Let's also change main.py so that we can get the arguments.

main.py


import sys
from koboridip.calculator import Calculator

if __name__ == '__main__':
    #Get arguments
    a = sys.argv[1]
    b = sys.argv[2]
    #Output method
    mode = sys.argv[3]

    #Create a Calculator instance
    calculator = Calculator(int(a), int(b), mode)

    #Output the results of each of the four arithmetic operations
    calculator.print()

[Important] Product problems

What's going on now

Currently, the Calculator class of the four arithmetic operations " processing " imports the Printer class of the result ** "output".

This state,

** " Calculator (processing) depends on Printer (output)" **

It is expressed as.

What is "dependent"

Dependency (import) means that ** a change in the dependency requires a change in the dependency source **.

As we saw in version 3, this project has also modified the Calculator class to add (change) the output method.

** I just wanted to change the output, but I had to change the processing as well. ** **

Let's assume that there are more requests such as "I want to output in csv format" and "I want to send the result to some server" in the future.

Each time, not only the Printer class but also the Calculator class is forced to make some changes.

Again, even though there are no changes in the specifications of "processing (four arithmetic operations)", it is necessary to modify the processing function.

It is important to feel "uncomfortable" here.

Create appropriate dependencies

At this point, you may come to the conclusion, "Then, should we reduce the dependency so that it will not be affected by the change of the dependency?"

But you can't just use import in your Python project, so there's always a dependency.

In other words, the ingenuity we need is to create "appropriate dependencies."

It means ** "depending on the one with the fewest changes" **.

Supplement (OK to skip)

Another problem with this project is that the Calculator knows about the ** details ** of the output.

The purpose of Calculator is to be able to" output the result ", and whether it is CLI or json format, I don't want to worry about this.

Dependence, and reversal. Version 4

Now let's reverse the dependencies.

Place the Printer class, which is an abstract class, in calculator.py, and also import the required ʻABCMeta and ʻabstractmethod.

calculator.py


from abc import ABCMeta, abstractmethod
from koboridip.simple_printer import SimplePrinter
from koboridip.json_printer import JsonPrinter


class Printer(metaclass=ABCMeta):
    @abstractmethod
    def print(self, add, subtract, multiply, divide):
        pass


class Calculator():
    def __init__(self, a: int, b: int, mode: str) -> None:
        self.a = a
        self.b = b
        self.mode = mode

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b

        #Switch the output method
        if self.mode == 'json':
            json_printer = JsonPrinter()
            json_printer.print(add, subtract, multiply, divide)
        elif self.mode == 'simple':
            simple_printer = SimplePrinter()
            simple_printer.print(add, subtract, multiply, divide)

Then change each of SimplePrinter and JsonPrinter to inherit the Printer class.

simple_printer.py


from koboridip.calculator import Printer


class SimplePrinter(Printer):
    def print(self, add, subtract, multiply, divide) -> None:
        print(f'add: {add}')
        print(f'subtract: {subtract}')
        print(f'multiply: {multiply}')
        print(f'divide: {divide}')

json_printer.py


import json
from typing import Dict
from koboridip.calculator import Printer


class JsonPrinter(Printer):
    def print(self, add, subtract, multiply, divide) -> None:
        result: Dict[str, int] = {
            "add": add,
            "subtract": subtract,
            "multiply": multiply,
            "divide": divide
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))

The important thing here is that the SimplePrinters depend on calculator.py.

** Here the dependencies have been reversed. ** "Output" depends on "Processing".

Of course it's not perfect yet, so we'll remove the state where the Calculator class depends on the SimplePrinter class.

Therefore, let the constructor decide which Printer to use.

calculator.py


from abc import ABCMeta, abstractmethod


class Printer(metaclass=ABCMeta):
    @abstractmethod
    def print(self, add, subtract, multiply, divide):
        pass


class Calculator():
    def __init__(self, a: int, b: int, printer:Printer) -> None:
        self.a = a
        self.b = b
        self.printer = printer

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b
        self.printer.print(add, subtract, multiply, divide)

Then let main.py specify which Printer to use.

main.py


import sys
from koboridip.calculator import Calculator, Printer
from koboridip.json_printer import JsonPrinter
from koboridip.simple_printer import SimplePrinter

if __name__ == '__main__':
    #Get arguments
    a = sys.argv[1]
    b = sys.argv[2]
    #Output method
    mode = sys.argv[3]

    #Specify the Printer class ("simple"Since it is troublesome to judge, I made it else)
    printer: Printer = JsonPrinter() if mode == 'json' else SimplePrinter()

    #Create a Calculator instance
    calculator = Calculator(int(a), int(b), printer)

    #Output the results of each of the four arithmetic operations
    calculator.print()

There is no import in calculate.py, instead there is an import in simple_printer.pys.

This completes the dependency reversal.

epilogue. Version 5

As expected, output in csv format was also requested.

Previously, every time the output method changed, the Calculator class was also affected, but let's see what happens.

.
└── koboridip
    ├── calculator.py
    ├── csv_printer.py
    ├── json_printer.py
    ├── main.py
    └── simple_printer.py

csv_printer.py


import csv
from typing import List
from koboridip.calculator import Printer


class CsvPrinter(Printer):
    def print(self, add, subtract, multiply, divide) -> None:
        result: List[List] = []
        result.append(["add", add])
        result.append(["subtract", subtract])
        result.append(["multiply", multiply])
        result.append(["divide", divide])

        with open('result.csv', 'w') as f:
            writer = csv.writer(f)
            writer.writerows(result)

main.py


import sys
from koboridip.calculator import Calculator, Printer
from koboridip.json_printer import JsonPrinter
from koboridip.simple_printer import SimplePrinter
from koboridip.csv_printer import CsvPrinter


if __name__ == '__main__':
    #Get arguments
    a = sys.argv[1]
    b = sys.argv[2]
    #Output method
    mode = sys.argv[3]

    #Specify Printer class
    printer: Printer = JsonPrinter() if mode == 'json' else CsvPrinter(
    ) if mode == 'csv' else SimplePrinter()

    #Create a Calculator instance
    calculator = Calculator(int(a), int(b), printer)

    #Output the results of each of the four arithmetic operations
    calculator.print()

By doing this, I was able to output a csv file as well.

You can imagine that you can easily change the output method after that.

at the end

I hope it helps you understand the principle of dependency reversal. One last point supplement.

Was the previous version wrong?

A person who says "I understand the principle of dependency reversal!" Will immediately try to fix the design and implementation, saying "This is a problem!" When I see a project that does not seem to have an appropriate dependency. I am).

For example, after the version 2 refactoring, the Calculator class depends on the Printer class, so at this point you may want to apply the dependency reversal principle.

But this is premature. Of course, if you know that "the output method can increase as much as you want" at this timing, you should apply it, but if "the output method is unlikely to change", apply ** << hold 》 ** I think it can be a good decision.

Personally, I would like to sort out the dependencies of "details" such as output as soon as possible, but I think it is important to think at least that "you can change it at any time."

Dependency injection (injection)

If I have time, I would like to write about "DI = Dependency Injection" as it is.

If you have any suggestions or questions, please feel free to comment.

Recommended Posts

"Principle of dependency reversal" learned slowly with Python
Algorithm learned with Python 8th: Evaluation of algorithm
1. Statistics learned with Python 1-3. Calculation of various statistics (statistics)
Algorithm learned with Python 13th: Tower of Hanoi
1. Statistics learned with Python 1-2. Calculation of various statistics (Numpy)
1. Statistics learned with Python 2. Probability distribution [Thorough understanding of scipy.stats]
[Python] Object-oriented programming learned with Pokemon
Getting Started with Python Basics of Python
Perceptron learning experiment learned with Python
Python data structures learned with chemoinformatics
Life game with Python! (Conway's Game of Life)
Efficient net pick-up learned with Python
1. Statistics learned with Python 1-1. Basic statistics (Pandas)
Implementation of Dijkstra's algorithm with python
Coexistence of Python2 and 3 with CircleCI (1.0)
Bookkeeping Learned with Python-The Flow of Bookkeeping-
Basic study of OpenCV with Python
[Python] Reactive Extensions learned with RxPY (3.0.1) [Rx]
Basics of binarized image processing with Python
[Examples of improving Python] Learning Python with Codecademy
Algorithm learned with Python 10th: Binary search
Algorithm learned with Python 5th: Fibonacci sequence
Execute Python script with cron of TS-220
Algorithm learned with Python 7th: Year conversion
Conditional branching of Python learned by chemoinformatics
Check the existence of the file with python
Clogged with python update of GCP console ①
Algorithm learned with Python 4th: Prime numbers
Easy introduction of speech recognition with Python
Algorithm learned with Python 2nd: Vending machine
Let's make a voice slowly with Python
Algorithm learned with Python 19th: Sorting (heapsort)
Source code of sound source separation (machine learning practice series) learned with Python
UnicodeEncodeError struggle with standard output of python3
Algorithm learned with Python 6th: Leap year
Drawing with Matrix-Reinventor of Python Image Processing-
Recommendation of Altair! Data visualization with Python
Algorithm learned with Python 3rd: Radix conversion
Algorithm learned with Python 12th: Maze search
Deep Learning from scratch The theory and implementation of deep learning learned with Python Chapter 3
[AtCoder] Solve A problem of ABC101 ~ 169 with Python
I tried hundreds of millions of SQLite with python
Prepare the execution environment of Python3 with Docker
Automatic operation of Chrome with Python + Selenium + pandas
Performance comparison of face detector with Python + OpenCV
[Python] limit axis of 3D graph with Matplotlib
2016 The University of Tokyo Mathematics Solved with Python
Color page judgment of scanned image with python
[Note] Export the html of the site with python.
Clogged with python update of GCP console ② (Solution)
Algorithm learned with Python 16th: Sorting (insertion sort)
Calculate the total number of combinations with python
Use multiple versions of python environment with pyenv
Check the date of the flag duty with Python
Algorithm learned with Python 14th: Tic-tac-toe (ox problem)
Solve A ~ D of yuki coder 247 with python
Algorithm learned with Python 15th: Sorting (selection sort)
[Python] Get rid of dating with regular expressions
How to specify attributes with Mock of python
Poetry-virtualenv environment construction with python of centos-sclo-rh ~ Notes
Automating simple tasks with Python Table of contents