Dans la première partie 1, aussi monolithique que possible,
** Recevoir la demande POST et enregistrer les notes **
** 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
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 ...)
Maintenant, revenons à l'histoire.
Actuellement, les processus suivants sont décrits collectivement dans «main.py».
main.py : https://github.com/y-tomimoto/CleanArchitecture/blob/master/part1/app/main.py
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.
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.
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
Préoccupations: ** Peut apporter des modifications inutiles au code existant qui fonctionne correctement **
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.
Dans main.py
,
Il existe deux processus.
En d'autres termes, en termes de CleanArchitecture,
est.
En interprétant avec CleanArchitecture, dans la figure ci-dessous,
Il semble que «1» puisse être décrit comme Web (faisant partie de la couche Frameworks & Drivers).
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.
Divisons maintenant main.py
dans le niveau Frameworks & Drivers: Web et MemoHandler.
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
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)
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.
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