Apprenez lentement avec Python "Principe de l'inversion des dépendances"

Il existe de nombreux textes sur le "principe du renversement des dépendances",

Pour ceux qui disent, j'écrirai mon propre article: "C'était facile à comprendre si vous expliquiez cela."

github

https://github.com/koboriakira/koboridip

Quoi faire

Un outil pour vérifier les quatre règles. Le résultat est envoyé à la CLI comme suit.

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

Faites comme on vous l'a dit. Version 1

Structure du projet

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

La 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__':
    #Obtenir l'argument
    a = sys.argv[1]
    b = sys.argv[2]

    #Créer une instance de calculatrice
    calculator = Calculator(int(a), int(b))

    #Sortir les résultats de chacune des quatre règles
    calculator.print()

La description

C'est un programme simple. Après avoir donné un nombre à la classe Calculator, laissez l'instance faire le" calcul (traitement) "et la" sortie ".

Changement soudain des spécifications. Version 2

Concernant ce produit, il y avait une demande que "je veux enregistrer le résultat de sortie au format json". Par conséquent, nous modifierons la source.

La sortie est écrite dans la classe Calculator, donc corrigeons-la.

La 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))

Résultat d'exécution

Juste au cas où, lorsque vous l'exécutez, le texte suivant sera affiché dans result.json (formaté).

result.json


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

Refactoring

La classe Calculator effectue ** le traitement ** des quatre règles de fonctionnement et ** la sortie ** du résultat.

J'ai décidé qu'il serait préférable de les séparer, j'ai donc décidé de créer une classe Imprimante chargée du traitement de la sortie.

.
└── 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)

Prémonition désagréable. Version 3

Dans le changement de politique qui a suivi, il a été décidé que «je veux utiliser à la fois la sortie des résultats vers CLI et le stockage au format json». Changez de mode comme suit.

$ python -m koboridip.main 8 2 simple
>(Sortie vers CLI)

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

Par conséquent, la classe Imprimante a été divisée en deux types afin de pouvoir être commutée.

Structure du projet

.
└── koboridip
    ├── calculator.py
    ├── json_printer.py ->Sortie au format json
    ├── main.py
    ├── simple_printer.py ->Sortie vers CLI

La 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))

C'est à calculator.py de décider lequel afficher.

La chaîne spécifiée "simple" ou "json" peut être commutée en la stockant dans la variable mode.

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

        #Changer la méthode de sortie
        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)

Modifions également main.py pour que nous puissions obtenir les arguments.

main.py


import sys
from koboridip.calculator import Calculator

if __name__ == '__main__':
    #Obtenir l'argument
    a = sys.argv[1]
    b = sys.argv[2]
    #Méthode de sortie
    mode = sys.argv[3]

    #Créer une instance de calculatrice
    calculator = Calculator(int(a), int(b), mode)

    #Sortir les résultats de chacune des quatre règles
    calculator.print()

[Important] Problèmes de produit

Que se passe-t-il maintenant

Actuellement, la classe "Calculatrice" des quatre règles ** "traitement" ** importe la classe "Imprimante" du résultat ** "sortie".

Cet état,

** " Calculatrice (traitement) dépend de Imprimante (sortie)" **

Il est exprimé comme.

Qu'est-ce que "dépendant"

Dépendance (importation) signifie que ** une modification de la dépendance nécessite une modification de la source de dépendance **.

Comme nous l'avons vu dans la version 3, ce projet a également modifié la classe Calculator pour ajouter (changer) la méthode de sortie.

** Je voulais juste changer la sortie, mais je devais aussi changer le traitement. ** **

Supposons qu'il y ait plus de demandes telles que «Je veux sortir au format csv» et «Je veux envoyer le résultat à un serveur» à l'avenir.

A chaque fois, non seulement la classe Printer mais aussi la classe Calculator sont obligées d'apporter des modifications.

Encore une fois, même s'il n'y a pas de changement dans les spécifications de «traitement (quatre règles de fonctionnement)», il est nécessaire de modifier la fonction de traitement.

Il est important de se sentir «mal à l'aise» ici.

Créer des dépendances appropriées

À ce stade, vous pouvez arriver à la conclusion: "Alors, devrions-nous réduire la dépendance afin qu'elle ne soit pas affectée par le changement de dépendance?"

Cependant, il existe toujours une dépendance car les importations ne peuvent pas être utilisées dans les projets Python.

En d'autres termes, l'ingéniosité dont nous avons besoin est de créer des «dépendances appropriées».

Cela signifie ** "en fonction de celui avec le moins de changements" **.

Supplément (OK pour sauter)

Un autre problème avec ce projet est que Calculator connaît les ** détails ** de la sortie.

Le but de Calculator est de pouvoir" sortir le résultat ", et que ce soit au format CLI ou json, je veux éviter de m'inquiéter à ce sujet.

Dépendance et renversement. Version 4

Maintenant, inversons les dépendances.

Placez la classe Printer, qui est une classe abstraite, dans calculator.py, et importez les ʻABCMeta et ʻabstract method requises.

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

        #Changer la méthode de sortie
        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)

Puis changez chacun de SimplePrinter et JsonPrinter pour hériter de la classe Printer.

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))

L'important ici est que les SimplePrinters dépendent de calculator.py.

** Ici, les dépendances ont été inversées. ** "Sortie" dépend du "Traitement".

Bien sûr, ce n'est pas encore parfait, nous allons donc supprimer l'état où la classe Calculator dépend de la classe SimplePrinter.

Par conséquent, laissez le constructeur décider quelle imprimante utiliser.

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)

Puis laissez main.py spécifier quelle imprimante utiliser.

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__':
    #Obtenir l'argument
    a = sys.argv[1]
    b = sys.argv[2]
    #Méthode de sortie
    mode = sys.argv[3]

    #Spécifiez la classe d'imprimante ("simple"Puisqu'il est difficile de juger, je l'ai fait autrement)
    printer: Printer = JsonPrinter() if mode == 'json' else SimplePrinter()

    #Créer une instance de calculatrice
    calculator = Calculator(int(a), int(b), printer)

    #Sortir les résultats de chacune des quatre règles
    calculator.print()

Il n'y a pas d'importation dans Calculate.py, mais il y a une importation dans simple_printer.pys.

Ceci termine l'inversion de dépendance.

épilogue. Version 5

Comme prévu, une sortie au format csv a également été demandée.

Auparavant, la classe Calculator était également affectée chaque fois qu'il y avait un changement dans la méthode de sortie, mais voyons ce qui se passe.

.
└── 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__':
    #Obtenir l'argument
    a = sys.argv[1]
    b = sys.argv[2]
    #Méthode de sortie
    mode = sys.argv[3]

    #Spécifiez la classe d'imprimante
    printer: Printer = JsonPrinter() if mode == 'json' else CsvPrinter(
    ) if mode == 'csv' else SimplePrinter()

    #Créer une instance de calculatrice
    calculator = Calculator(int(a), int(b), printer)

    #Sortir les résultats de chacune des quatre règles
    calculator.print()

En faisant cela, le fichier csv était également sorti.

Vous pouvez imaginer que vous pouvez facilement changer la méthode de sortie après cela.

à la fin

J'espère que cela vous aidera à comprendre le principe du renversement de la dépendance. Un dernier supplément de point.

La version précédente était-elle erronée?

Ceux qui disent "Je comprends le principe de l'inversion des dépendances!" Essaieront immédiatement de corriger la conception et l'implémentation, en disant "C'est un problème!" Quand ils voient un projet qui ne semble pas avoir de dépendance appropriée. Je suis).

Par exemple, après le refactoring de la version 2, la classe Calculator dépend de la classe Printer, donc à ce stade, vous voudrez peut-être appliquer le principe d'inversion de dépendance.

Mais c'est prématuré. Bien sûr, si vous savez que "la méthode de sortie peut augmenter autant que vous le souhaitez" à ce moment, vous devez l'appliquer, mais d'un autre côté, si "la méthode de sortie est peu susceptible de changer", appliquez ** << maintenir 》 ** Je pense que cela peut être une bonne décision.

Personnellement, je voudrais trier les dépendances des «détails» tels que la sortie le plus tôt possible, mais je pense qu'il est important de penser au moins que «vous pouvez le changer à tout moment».

Injection de dépendance (injection)

Si j'ai le temps, j'aimerais écrire sur "DI = Dependency Injection" tel quel.

Si vous avez des suggestions ou des questions, n'hésitez pas à commenter.

Recommended Posts

Apprenez lentement avec Python "Principe de l'inversion des dépendances"
1. Statistiques apprises avec Python 1-3. Calcul de diverses statistiques (statistiques)
1. Statistiques apprises avec Python 1-2. Calcul de diverses statistiques (Numpy)
1. Statistiques apprises avec Python 2. Distribution des probabilités [Compréhension approfondie de scipy.stats]
[Python] Programmation orientée objet apprise avec Pokemon
Premiers pas avec Python Bases de Python
Expérience d'apprentissage Perceptron apprise avec Python
Structure de données Python apprise avec la chimioinfomatique
Jeu de vie avec Python! (Le jeu de la vie de Conway)
Ramassage efficace du réseau avec Python
1. Statistiques apprises avec Python 1-1. Statistiques de base (Pandas)
Implémentation de la méthode Dyxtra par python
Coexistence de Python2 et 3 avec CircleCI (1.0)
Etude de base d'OpenCV avec Python
[Python] Extensions réactives apprises avec RxPY (3.0.1) [Rx]
Bases du traitement d'images binarisées par Python
[Exemple d'amélioration de Python] Apprentissage de Python avec Codecademy
Exécuter le script Python avec TS-220 cron
Branchement conditionnel de Python appris avec la chimioinfomatique
Vérifier l'existence du fichier avec python
Obstrué par la mise à jour Python de la console GCP ①
Introduction facile de la reconnaissance vocale avec Python
Faisons la voix lentement avec Python
Code source pour la séparation des sources sonores (série de pratiques d'apprentissage automatique) appris avec Python
UnicodeEncodeError lutte avec la sortie standard de python3
Dessin avec Matrix-Reinventor of Python Image Processing-
Recommandation d'Altair! Visualisation des données avec Python
Deep Learning from scratch La théorie et la mise en œuvre de l'apprentissage profond appris avec Python Chapitre 3
[AtCoder] Résoudre un problème de ABC101 ~ 169 avec Python
J'ai essayé des centaines de millions de SQLite avec python
Préparer l'environnement d'exécution de Python3 avec Docker
Fonctionnement automatique de Chrome avec Python + Sélénium + pandas
Comparaison des performances du détecteur de visage avec Python + OpenCV
[Python] axe limite du graphe 3D avec Matplotlib
Mathématiques Todai 2016 résolues avec Python
[Note] Exportez le html du site avec python.
Obstruction de la mise à jour python de la console GCP ② (Solution)
Calculez le nombre total de combinaisons avec python
Utiliser plusieurs versions de l'environnement python avec pyenv
Vérifiez la date du devoir de drapeau avec Python
Résolvez A ~ D du codeur yuki 247 avec python
[Python] Débarrassez-vous de la datation avec des expressions régulières
Comment spécifier des attributs avec Mock of Python
Construction d'environnement Poetry-virtualenv avec python de centos-sclo-rh ~ Notes
Automatiser des tâches simples avec Python Table des matières