[CleanArchitecture with Python] Partie 2: Couche Frameworks & Drivers: Présentation du Web

Dans la première partie 1, aussi monolithique que possible,

  1. ** Recevoir la demande POST et enregistrer les notes **

  2. ** Recevez une demande GET et reportez-vous au mémo enregistré **

Nous avons préparé juste une API mémo.

Dans cet article, l'explication est basée sur le code ci-dessous créé dans le chapitre précédent.

Part1 : https://qiita.com/y_tom/items/ac6f6a08bdc374336dc4

1. Recevoir une demande de modification de spécification pour le livrable

J'ai reçu une demande de modification des spécifications de «l'API créée à l'aide du framework Flask» créée dans la partie 1.

** "Adoptons FastAPI au lieu de Flask pour le cadre d'application Web." **

Dans la partie 1, considérons une conception qui résiste aux changements de spécification, en supposant cette demande de changement de spécification.


Je n'ai pas rencontré beaucoup de cas où je souhaite remplacer le framework, mais j'ai pensé que ce serait un cas facile à comprendre en guise d'introduction, alors je l'ai adopté.

En passant, c'est mon expérience la plus récente, mais en raison des changements dans les conditions du marché, l'en-tête de réponse d'une certaine application Web a été soudainement ajouté. Il y avait une situation où je voulais donner un en-tête spécifique.

Cependant, comme l'attribut En-tête a été ajouté ces dernières années, le cadre d'application Web adopté à l'époque était Dans certains cas, il ne prenait pas en charge l'attribut Header et a été contraint de modifier le cadre d'application Web lui-même. (En fin de compte, j'ai écrit l'en-tête brut dans l'en-tête personnalisé et j'ai répondu, et je n'ai rien obtenu, mais ...)

2. Préoccupations lors de la réponse aux demandes de modification des spécifications avec la conception actuelle

Maintenant, revenons à l'histoire.

Actuellement, les processus suivants sont décrits collectivement dans «main.py».

  1. Accepter les demandes via le framework
  2. Exécutez le traitement initialement attendu de l'application (acquisition / sauvegarde du mémo)

main.py : https://github.com/y-tomimoto/CleanArchitecture/blob/master/part1/app/main.py

Codage lors de modifications de la conception actuelle

Que se passe-t-il lorsque vous modifiez le cadre que vous utilisez dans votre conception actuelle?

Si vous essayez de changer le framework de Flask en Fast API Vous apporterez probablement les modifications suivantes au fichier main.py existant.

  1. Réécrivez le routeur configuré par le framework
  2. Réécrivez le format de la réponse
  3. Réécrivez le gestionnaire d'erreurs
  4. Réécrivez comment démarrer l'application

Si vous conservez la conception actuelle et apportez des modifications réelles au main.py existant, vous obtiendrez ce qui suit.

main.py

from http import HTTPStatus
- from flask import Flask, request, jsonify, make_response
+ from fastapi import FastAPI, Form, Response
+ import uvicorn
from mysql import connector

- app = Flask(__name__) 
+ app = FastAPI()

#Paramètres de connexion à la base de données
config = {
    ...
}

def exist(memo_id: int) -> bool:
    ...


- @app.route('/memo/<int:memo_id>')
+ @app.get('/memo/{memo_id}') 
def get(memo_id: int) -> str:

    ...

    
-   return jsonify(
-       {
-           "message": f'memo : [{result[1]}]'
-       }
-   )

+   return JSONResponse(
+       content={"message": f'memo : [{result[1]}]'
+   )


- @app.route('/memo/<int:memo_id>', methods=['POST'])
+ @app.post('/memo/{memo_id}')
- def post(memo_id: int) -> str:
+ async def post(memo_id: int, memo: str = Form(...)) -> str:


    ...

    
-   return jsonify(
-       {
-            "message": "saved."
-       }
-   )

+   return JSONResponse(
+      content={"message": "saved."}
+   )

- @app.errorhandler(NotFound)
- def handle_404(err):
-     json = jsonify(
-         {
-             "message": err.description
-         }
-     )
-     return make_response(json, HTTPStatus.NOT_FOUND)


+ @app.exception_handler(NotFound)
+ async def handle_404(request: Request, exc: NotFound):
+   return JSONResponse(
+       status_code=HTTPStatus.NOT_FOUND,
+       content={"message": exc.description},
+   )

- @app.errorhandler(Conflict)
- def handle_409(err):
-     json = jsonify(
-         {
-             "message": err.description
-         }
-     )
-     return make_response(json, HTTPStatus.CONFLICT)


+ @app.exception_handler(Conflict)
+ async def handle_409(request: Request, exc: Conflict):
+   return JSONResponse(
+       status_code=HTTPStatus.CONFLICT,
+       content={"message": exc.description},
+   )



if __name__ == '__main__':
-   app.run(debug=True, host='0.0.0.0') # DELETE
+   uvicorn.run(app=fastapi_app, host="0.0.0.0", port=5000) # NEW

Bien qu'il soit possible de modifier les spécifications par la force de cette manière, il y a quelques inquiétudes.

Problèmes de codage lors de modifications de la conception actuelle

Ce correctif modifie le code du framework dans main.py.

Cependant, dans main.py, non seulement le code lié au framework, mais aussi le ** processus de récupération et d'enregistrement des notes **, qui est à l'origine attendu de l'application, est décrit.

Principe de responsabilité unique: principe de responsabilité unique: https://note.com/erukiti/n/n67b323d1f7c5

À ce stade, vous pouvez accidentellement apporter des modifications inutiles au «processus d'acquisition et d'enregistrement des mémos» que vous attendiez initialement de l'application **.

Je voudrais éviter la situation où je fais des corrections en pensant que cela peut accidentellement provoquer un bogue dans le code qui fonctionne déjà.

Dans cet exemple, il n'y a que deux points de terminaison, mais s'il s'agit d'un grand service et que vous avez plusieurs points de terminaison, ce problème sera encore plus grand.

Principe ouvert / fermé: principe ouvert / fermé: https://medium.com/eureka-engineering/go-open-closed-principle-977f1b5d3db0

3. En réponse à la demande, réfléchissez au type de conception qui a pu modifier les spécifications en douceur sur la base d'une architecture propre.

i. Réorganiser les problèmes de conception

Préoccupations: ** Peut apporter des modifications inutiles au code existant qui fonctionne correctement **

Ii. Quel type de conception pourrait éviter les problèmes et modifier les spécifications?

Cette préoccupation est due au fait que main.py contient non seulement le framework, mais aussi le ** processus de récupération et d'enregistrement des ** notes ** qui est à l'origine attendu de l'application.

Par conséquent, la préoccupation cette fois est main.py,

Il semble être résolu en le divisant en ** framework ** et ** traitement initialement attendu de l'application **.

Si le code est conçu pour être divisé en rôles, il semble que la portée de la modification puisse être limitée à ce rôle.

III. Quand le design idéal est interprété par CleanArchitecture

Dans main.py,

  1. Recevoir des demandes avec le framework flask
  2. Enregistrez le mémo ou obtenez le mémo

Il existe deux processus.

En d'autres termes, en termes de CleanArchitecture,

  1. Cadre d'application Web
  2. Fonctions initialement attendues de l'application

est.

En interprétant avec CleanArchitecture, dans la figure ci-dessous,

  1. Il semble que «1» puisse être décrit comme Web (faisant partie de la couche Frameworks & Drivers).

  2. Concernant «2», puisqu'il s'agit d'une fonction initialement attendue de l'application, il semble qu'elle corresponde soit à la couche Application Business Rules, soit à la couche Enterprise Business Rules, mais ici, enregistrez le mémo ou récupérez le mémo Décrivons la fonction comme MemoHandler.

Il semble être exprimé comme.

https___qiita-image-store.s3.amazonaws.com_0_293368_7ce1fb10-504e-16e0-8930-278b8a7f942d.jpeg

Divisons maintenant main.py dans le niveau Frameworks & Drivers: Web et MemoHandler.

Iv. Codage réel

Depuis main.py, appelez la couche Frameworks & Drivers: Web router, Conception pour appeler memo_handler.py depuis chaque routeur.

Avec cette conception, si vous voulez changer le framework, changez simplement le framework appelé dans main.py. Il ne modifie pas le processus existant memo_handler.py lui-même, donc le processus existant n'est pas accidentellement modifié.

.
├── memo_handler.py 
└── frameworks_and_drivers
    └── web
        ├── fastapi_router.py
        └── flask_router.py

Couche Frameworks & Drivers

frameworks_and_drivers/web/fastapi_router.py

from fastapi import FastAPI, Form, Request
from fastapi.responses import JSONResponse
from werkzeug.exceptions import Conflict, NotFound
from memo_handler import MemoHandler
from http import HTTPStatus

app = FastAPI()


@app.get('/memo/{memo_id}')
def get(memo_id: int) -> str:
    return JSONResponse(
        content={"message": MemoHandler().get(memo_id)}
    )


@app.post('/memo/{memo_id}')
async def post(memo_id: int, memo: str = Form(...)) -> str:
    return JSONResponse(
        content={"message": MemoHandler().save(memo_id, memo)}
    )


@app.exception_handler(NotFound)
async def handle_404(request: Request, exc: NotFound):
    return JSONResponse(
        status_code=HTTPStatus.NOT_FOUND,
        content={"message": exc.description},
    )


@app.exception_handler(Conflict)
async def handle_409(request: Request, exc: Conflict):
    return JSONResponse(
        status_code=HTTPStatus.CONFLICT,
        content={"message": exc.description},
    )


frameworks_and_drivers/web/flask_router.py


from flask import Flask, request , jsonify , make_response
from werkzeug.exceptions import Conflict,NotFound
from http import HTTPStatus
from memo_handler import MemoHandler
app = Flask(__name__)


@app.route('/memo/<int:memo_id>')
def get(memo_id: int) -> str:
    return jsonify(
        {
            "message": MemoHandler().get(memo_id)
        }
    )


@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
    memo: str = request.form["memo"]
    return jsonify(
        {
            "message": MemoHandler().save(memo_id, memo)
        }
    )


@app.errorhandler(NotFound)
def handle_404(err):
    json = jsonify(
        {
            "message": err.description
        }
    )
    return make_response(json,HTTPStatus.NOT_FOUND)


@app.errorhandler(Conflict)
def handle_409(err):
    json = jsonify(
        {
            "message": err.description
        }
    )
    return make_response(json, HTTPStatus.CONFLICT)


MemoHandler

memo_handler.py

from mysql import connector
from werkzeug.exceptions import Conflict, NotFound

#config pour le client SQL
config = {
    'user': 'root',
    'password': 'password',
    'host': 'mysql',
    'database': 'test_database',
    'autocommit': True
}


class MemoHandler:

    def exist(self, memo_id: int):
        #Créer un client DB
        conn = connector.connect(**config)
        cursor = conn.cursor()

        # memo_Vérifiez s'il y a un identifiant
        query = "SELECT EXISTS(SELECT * FROM test_table WHERE memo_id = %s)"
        cursor.execute(query, [memo_id])
        result: tuple = cursor.fetchone()

        #Fermez le client DB
        cursor.close()
        conn.close()

        #Vérifier l'existence en vérifiant s'il y a un résultat de recherche
        if result[0] == 1:
            return True
        else:
            return False

    def get(self, memo_id: int):

        #Vérifiez s'il y a un identifiant spécifié
        is_exist: bool = self.exist(memo_id)

        if not is_exist:
            raise NotFound(f'memo_id [{memo_id}] is not registered yet.')

        #Créer un client DB
        conn = connector.connect(**config)
        cursor = conn.cursor()
        # memo_Effectuer une recherche par identifiant
        query = "SELECT * FROM test_table WHERE memo_id = %s"
        cursor.execute(query, [memo_id])
        result: tuple = cursor.fetchone()

        #Fermez le client DB
        cursor.close()
        conn.close()

        return f'memo : [{result[1]}]'

    def save(self, memo_id: int, memo: str):

        #Vérifiez s'il y a un identifiant spécifié
        is_exist: bool = self.exist(memo_id)

        if is_exist:
            raise Conflict(f'memo_id [{memo_id}] is already registered.')

        #Créer un client DB
        conn = connector.connect(**config)
        cursor = conn.cursor()

        #Enregistrer le mémo
        query = "INSERT INTO test_table (memo_id, memo) VALUES (%s, %s)"
        cursor.execute(query, (memo_id, memo))

        #Fermez le client DB
        cursor.close()
        conn.close()

        return "saved."



main.py

Basculez le framework adopté sur main.py.

main.py


import uvicorn
from frameworks_and_drivers.flask_router import app as fastapi_app
from frameworks_and_drivers.flask_router import app as flask_app

---

#Lors de l'adoption de flask comme cadre
flask_app.run(debug=True, host='0.0.0.0')

---

#Rapide comme un cadre_Lors de l'adoption de l'API
uvicorn.run(app=fastapi_app, host="0.0.0.0",port=5000)

4. À quels types de modifications de spécifications les modifications de conception ont-elles permis de résister?

Le code final est ici. : https://github.com/y-tomimoto/CleanArchitecture/blob/master/part2

En découpant chaque framework dans la couche Frameworks & Drivers: Web, et en supprimant le traitement initialement attendu de l'application vers MemoHandler, En appelant simplement le routeur que vous souhaitez adopter avec main.py, vous pouvez modifier de manière flexible le framework ** sans modifier memo_handler.py, qui est le processus que vous attendiez à l'origine de l'application. J'ai fait.

https___qiita-image-store.s3.amazonaws.com_0_293368_7ce1fb10-504e-16e0-8930-278b8a7f942d.jpeg

Cette conception implémente l'une des règles CleanArchitecture, ** Framework Independence **.

Clean Architecture (traduit par The Clean Architecture): https://blog.tai2.net/the_clean_architecture.html

Indépendance du framework: L'architecture ne repose pas sur la disponibilité d'une bibliothèque logicielle riche en fonctionnalités. Cela permet à de tels cadres d'être utilisés comme des outils et ne force pas le système à être forcé dans les contraintes limitées du cadre.

Recommended Posts

[CleanArchitecture with Python] Partie 2: Couche Frameworks & Drivers: Présentation du Web
[Part.2] Exploration avec Python! Cliquez sur la page Web pour vous déplacer!
Télécharger des fichiers sur le Web avec Python
Essayez d'utiliser le framework Web Python Tornado Partie 1
Essayez d'utiliser le framework Web Python Tornado Partie 2
Faire un point d'arrêt sur la couche c avec python
Application Web réalisée avec Python3.4 + Django (Construction de l'environnement Part.1)
Traitement d'image avec Python (partie 2)
Etudier Python avec freeCodeCamp part1
Images en bordure avec python Partie 1
Web scraping avec python + JupyterLab
Grattage avec Selenium + Python Partie 1
Etudier Python avec freeCodeCamp part2
Traitement d'image avec Python (partie 1)
API Web avec Python + Falcon
Comparaison de 4 types de frameworks Web Python
Résolution de Nampre avec Python (partie 2)
Traitement d'image avec Python (3)
Grattage avec Selenium + Python Partie 2
Appelez l'API avec python3.
Application Web avec Python + Flask ② ③
Enregistrer des images sur le Web sur un lecteur avec Python (Colab)
Web scraping débutant avec python
Rationalisez la recherche Web avec Python
Application Web avec Python + Flask ④
La première intelligence artificielle. Testez la sortie Web avec python. ~ Introduction du flacon
[python, ruby] sélénium-Obtenez le contenu d'une page Web avec le pilote Web
Visualisez vos fichiers d'argent de poche avec Dash, le framework Web Python
Jouez des nombres manuscrits avec Python Partie 1
Extraire le fichier xz avec python
[Automatisé avec python! ] Partie 1: fichier de configuration
Premiers pas avec les applications Web Python
Web scraping avec Python Première étape
J'ai essayé webScraping avec python.
Surveillez les applications Web Python avec Prometheus
Obtenez la météo avec les requêtes Python
Obtenez une capture d'écran Web avec python
Trouvez la distance d'édition (distance de Levenshtein) avec python
Accédez à l'API Etherpad-lite avec Python
Installer le plug-in Python avec Netbeans 8.0.2
Accédez à l'API Web en Python
J'ai aimé le tweet avec python. ..
Maîtriser le type avec Python [compatible Python 3.9]
Automatisez des tâches simples avec Python Part0
[Automatisé avec python! ] Partie 2: Fonctionnement des fichiers
Comment couper la partie inférieure droite de l'image avec Python OpenCV
Hit une méthode d'une instance de classe avec l'API Web Python Bottle
Rendre la console Python couverte d'UNKO
Présentation du framework BOT Minette pour Python
[Python] Définissez la plage du graphique avec matplotlib
Grattage WEB avec Python (pour mémo personnel)
Jouez des nombres manuscrits avec python, partie 2 (identifier)
Traiter les données Pubmed .xml avec python [Partie 2]
Derrière le flyer: utiliser Docker avec Python
Automatisez des tâches simples avec Python Part1 Scraping
Premiers pas avec Python Web Scraping Practice
Vérifier l'existence du fichier avec python
[Python] Récupère le nom de la variable avec str
[Python] Arrondissez avec juste l'opérateur