I've been worried about design and architecture a lot these days, but I wrote my own opinion on whether Python, a dynamically typed language, can also implement the Dependency Inversion Principle. My opinion is not absolutely correct, but I hope it helps.
First, we need to understand ** polymorphism ** before we understand the Dependency Inversion Principle. The explanation is quoted from Wikipedia.
Polymorphism describes the nature of the type system of a programming language, and that each element of the programming language (constants, variables, expressions, objects, functions, methods, etc.) belongs to multiple types. Refers to the property of forgiving. Also called polymorphism, polymorphism, polymorphism, diversity.
Since the content is difficult to understand, I will explain it in detail. The essence is the part of "** refers to the property of allowing them to belong to multiple types **". In the case of dynamically typed languages such as Python, it is not so conscious, but in the case of statically typed languages, it is basically not allowed to belong to multiple types because the types of variables are limited.
For example, see the example below. (C ++ code, but I don't think the content is difficult)
main.cpp
#include <iostream>
using namespace std;
int main()
{
int a = 1;
string b = "hello";
a = b; // error
}
The variable a is defined as an int and the variable b is defined as a string. I am trying to "assign a string type variable b to an int type variable a" with "a = b". This operation will result in an error. This is because ** the variable a is defined as an int type and cannot belong to more than one type **.
In other words, variables etc. cannot belong to multiple types in principle. However, in the case of polymorphism, the content that allows it to belong to multiple types is explained in the quoted part of Wikipedia.
Let's take a look at an example of polymorphism. (C ++ code, but you don't need to understand the code itself)
main.cpp
#include <iostream>
using namespace std;
class Human{
public:
string name;
int age;
Human(const string _name, const int _age){
name = _name;
age = _age;
};
virtual void print_gender(){};
void print_info(){
cout << "name: " << name << ", age: " << age << endl;
};
};
class Man: public Human{
public:
Man(const string _name, const int _age): Human(_name,_age){};
void print_gender() override {
cout << "I am man." << endl;
};
};
class Woman: public Human{
public:
Woman(const string _name, const int _age): Human(_name,_age){};
void print_gender() override {
cout << "I am woman." << endl;
};
};
int main()
{
Man* taro = new Man("taro", 12);
Woman* hanako = new Woman("hanako", 23);
Human* human = taro;
human->print_gender();
human->print_info();
}
The important parts are as follows.
Man* taro = new Man("taro", 12);
Woman* hanako = new Woman("hanako", 23);
Human* human = taro;
The variable called taro of the Man class is assigned to the variable called human defined in the Human class. I mentioned in the above explanation that variables can only belong to one type (class), but the variable human seems to belong to both the class Human and the class Man. (Strictly speaking, it belongs only to Human)
The reason this is possible is that the Man class is a derivative of the Human class. Since the Man class and the Human class are related, the image is that you allow related things to be treated in the same way.
I think the merit of polymorphism is that you can write clean code because you can treat things with the same properties in the same way. (I will not explain specific examples. I'm sorry.)
As a prior knowledge, it is necessary to understand a little about "abstract" and "concrete". I will explain a little about the words.
Abstraction can be rephrased as an interface, but it's basically a type definition (which may not be exactly correct). For example, consider the following function.
main1.py
def add_number(a: int, b: int) -> int:
"""Returns the result of adding two arguments"""
return a + b
A function called add_number defines an interface that takes an int argument and changes the result of the int. From an abstract point of view, I don't think about the inside of the implementation, so the function name is add_number, but I don't care if multiplication is done inside instead of addition. We only consider inputs and outputs (interfaces).
Specifically, we think about the inside of the implementation. Therefore, we should consider whether the internal implementation of the add_number function is addition or multiplication.
The explanation of the prerequisite knowledge has become long. Moreover, almost no Python code comes out ... First of all, the meaning of the word "dependence" is confusing, so I will explain it from there.
For example, see the code below.
main2.py(Excerpt)
def main():
taro: Man = Man('taro', 12)
taro.print_gender()
taro.print_info()
Apparently I'm using a class called Man in the main function. This state can be said to be "the main function depends on the Man class". In other words, if the contents of the Man class are changed, the main function may also be affected.
Now, let's quote the explanation of the Dependency Inversion Principle from wikipedia.
In object-oriented design, the principle of dependency inversion or the principle of dependency inversion [1](dependency inversion principle) is a term that refers to a specific form for keeping software modules loosely coupled. Following this principle, the traditional dependencies from higher-level modules to lower-level modules that define software behavior are reversed so that the higher-level modules can be kept independent of the implementation details of the lower-level modules. Become A. Higher level modules should not depend on lower level modules. Both should rely on abstractions. B. Abstraction should not depend on details. The details should depend on abstraction.
Another esoteric explanation. .. .. First of all, the words "upper" and "lower", but in the previous example, the main function is the upper and the Man class is the lower. In other words, "upper level modules should not depend on lower level modules" specifically means "main function should not depend on Man class". Next, the part that "both should depend on abstractions", in other words, it means that it should depend on the interface (type) rather than on the internal implementation.
It's almost the same as the code above, but see the example below.
main3.py(Excerpt)
def main():
taro: Human = Man('taro', 12)
taro.print_gender()
taro.print_info()
The difference is that the ** main function has replaced its dependency on the Man class with a dependency on the Human class **. Now let's take a look at the implementations of the Human and Man classes. (The content is an excerpt, the full text is at the end)
main3.py(Excerpt)
@dataclass
class Human(metaclass=ABCMeta):
name: str
age: int
#Abstract method
@abstractmethod
def print_gender(self) -> None:
pass
#Common method
def print_info(self) -> None:
print(f'name: {self.name}, age: {self.age}')
class Man(Human):
def print_gender(self):
print('I am man.')
The Human class is the base class and does not contain any complex business logic. In other words, it's an abstract class. On the contrary, the Man class is a concrete class because it is a class that includes an internal implementation. The difference between the Human class and the Man class is that the Human class changes less frequently and the Man class changes more often. The reason is, of course, that the Human class only defines the interface (not exactly, but ...), and the Man class contains the internal implementation and business logic.
At this point, you can see the benefits of changing the main function from a Man class (concrete) to a Human class (abstract). If you depend on the Man class, the main function is affected by the changes in the Man class, so the frequency of changes in the Man class is high, so it is greatly affected. On the contrary, if it depends on the Human class, it will be affected if the Human class is changed, but it will be less affected because the change frequency is less than that of the Man class.
This means that you can write code that is more robust to changes by relying on abstractions.
You can, but it's not perfect. Specifically, it is possible by using the abstract class abc, lint, and type check tool together. However, in the case of Python, the program itself works even if the type information is incorrect, so it is not possible to achieve the same level as other statically typed languages. If you are aiming for a higher level, I think that it can be achieved by thoroughly preparing the development environment as a team and checking the type and lint by pre-commit. I think that it provides functions that are not a problem for use at a practical level.
I wrote it in a hurry, so I think it's very easy to understand. This area is a difficult field to understand, so I hope it will be helpful as much as possible. If you have any questions in the comments, we will answer as much as possible. It would be helpful if you could point out any mistakes or advice.
main3.py
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
@dataclass
class Human(metaclass=ABCMeta):
name: str
age: int
#Abstract method
@abstractmethod
def print_gender(self) -> None:
pass
#Common method
def print_info(self) -> None:
print(f'name: {self.name}, age: {self.age}')
class Man(Human):
def print_gender(self):
print('I am man.')
class Woman(Human):
def print_gender(self):
print('I am woman.')
def main():
taro: Human = Man('taro', 12)
taro.print_gender()
taro.print_info()
Recommended Posts