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
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
.
└── koboridip
├── calculator.py
└── main.py
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()
It's a simple program. After giving a number to the Calculator
class, let the instance do the" calculation (processing) "and" output ".
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.
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))
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
}
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)
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.
.
└── koboridip
├── calculator.py
├── json_printer.py ->Output in json format
├── main.py
├── simple_printer.py ->Output to CLI
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()
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.
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.
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" **.
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.
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.py
s.
This completes the dependency reversal.
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.
I hope it helps you understand the principle of dependency reversal. One last point supplement.
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."
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