Cliquez ici pour le dernier exemple de code d'architecture propre utilisant Python: https://github.com/y-tomimoto/CleanArchitecture/tree/master/part9
app
├── application_business_rules
│ ├── __init__.py
│ ├── boundary
│ │ ├── __init__.py
│ │ ├── input_port
│ │ │ ├── __init__.py
│ │ │ └── memo_input_port.py
│ │ └── output_port
│ │ ├── __init__.py
│ │ └── memo_output_port.py
│ └── memo_handle_interactor.py
├── enterprise_business_rules
│ ├── __init__.py
│ ├── dto
│ │ ├── __init__.py
│ │ ├── input_memo_dto.py
│ │ └── output_memo_dto.py
│ ├── entity
│ │ ├── __init__.py
│ │ └── memo.py
│ ├── memo_data.py
│ └── value_object
│ ├── __init__.py
│ └── memo_author.py
├── frameworks_and_drivers
│ ├── __init__.py
│ ├── db
│ │ ├── __init__.py
│ │ ├── mysql.py
│ │ └── postgres.py
│ └── web
│ ├── __init__.py
│ ├── fastapi_router.py
│ └── flask_router.py
├── interface_adapters
│ ├── __init__.py
│ ├── controller
│ │ ├── __init__.py
│ │ └── flask_controller.py
│ ├── gataways
│ │ ├── __init__.py
│ │ └── memo_repository_gateway.py
│ └── presenter
│ ├── __init__.py
│ ├── ad_presenter.py
│ └── default_presenter.py
└── main.py
En découpant chaque framework d'application Web que vous souhaitez adopter dans la couche Frameworks & Drivers: Web, et en découpant 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. ..
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.
Memo_handler.py
qui décrit le traitement que vous attendiez à l'origine de l'application
Divisée en.
Cela rendra memo_handler.py
,
En divisant en, lors de la modification des spécifications de l'application, il est conçu de sorte que les spécifications puissent être modifiées et étendues de manière flexible sans affecter le traitement de principe existant.
En exploitant le contrôleur dans la couche des adaptateurs d'interface La partie consistant à changer le "format de demande externe" fréquemment mis à jour en un format adapté au traitement réel, J'ai pu le couper du cadre.
Cela vous permet de modifier le format des demandes que votre application peut accepter. Il est conçu pour vous permettre de modifier votre code sans tenir compte des frameworks d'application Web ou des règles métier existants.
En adoptant DTO, l'accès aux données entre les couches est facilité et en même temps Il est conçu pour minimiser l'impact sur chaque couche lorsque la structure de données gérée par l'application change.
En plus d'implémenter Presenter, nous avons également implémenté OutputPort.
Par conséquent, lors de la modification de l'interface utilisateur, elle est conçue de sorte que seule l'interface utilisateur puisse être modifiée indépendamment sans tenir compte du cadre d'application Web ou des règles métier existants.
Avec l'introduction de ce Presenter, les règles CleanArchitecture et l'indépendance de l'interface utilisateur ont été atteintes.
Clean Architecture (traduit par The Clean Architecture): https://blog.tai2.net/the_clean_architecture.html
L'interface utilisateur peut être facilement modifiée. Pas besoin de changer le reste du système. Par exemple, l'interface utilisateur Web peut être remplacée par l'interface utilisateur de la console sans modifier les règles métier.
Nous avons implémenté DB dans la couche DB et adopté Gataways,
En conséquence, lors du changement de DB, il est conçu de sorte que le DB puisse être commuté sans considérer chaque couche.
En conséquence, les règles CleanArchitecture et l'indépendance de la base de données ont été atteintes.
Clean Architecture (traduit par The Clean Architecture): https://blog.tai2.net/the_clean_architecture.html
Indépendant de la base de données. Vous pouvez remplacer Oracle ou SQL Server par Mongo, BigTable, CoucheDB ou autre. Les règles métier ne sont pas liées à la base de données.
Communiquer avec DB en utilisant Entity, un objet avec la même structure que la base de données Afin de masquer les propriétés hautement confidentielles, nous l'avons conçu pour adopter le DTO dans chaque règle métier.
En conséquence, chaque règle métier est conçue de manière à pouvoir gérer les valeurs de la base de données sans connaître les propriétés confidentielles.
De plus, la validation et le traitement de chaque propriété sont rendus indépendants de l'entité en adoptant ValueObject. Par conséquent, lors de la création ou de la modification d'une entité, il n'est plus nécessaire de l'implémenter avec des propriétés spécifiques à l'esprit.
Récemment, j'ai été affecté à un projet qui pouvait être techniquement contesté, j'ai donc adopté Clean Architecture.
Je voulais re-verbaliser ce que j'avais appris quand je l'ai embauché.
Quand je l'ai implémenté, j'ai pensé qu'il aurait été préférable qu'il y ait un article expliquant les problèmes que chaque couche résoudrait dans la base de code.
J'ai décidé d'écrire cet article.
Comme mentionné ci-dessus, les articles actuellement publiés sur Clean Architecture sont Je pense personnellement qu'il se compose souvent des deux parties suivantes.
** En imaginant "à quels types de changements l'architecture propre résiste spécifiquement" **
Ce n'est pas une structure qui présente le code de l'artefact déjà terminé depuis le début
CleanArchitecture
*Je vais y arriver.
Ce que je souhaite clarifier dans cet article est
** "À quels types de changements Clean Architecture résiste-t-il spécifiquement?" **
est.
Ainsi, dans l'article, nous appliquerons une architecture propre avec le développement suivant.
Commençons.
Dans la partie 1, nous allons créer une API qui servira de base à l'explication de la partie suivante.
Lors de la création
J'ai essayé d'implémenter cette API afin qu'elle soit intentionnellement monolithique sans supposer un changement de spécification **.
En le rendant volontairement monolithique, l'objectif est de faciliter la visualisation des avantages du design lors de l'application de CleanArchitecture.
Observons dans les parties suivantes comment le fichier est progressivement divisé par responsabilité et la combinaison se desserre progressivement.
Cette fois
** Recevoir la demande POST et enregistrer les notes **
** Recevez une demande GET et reportez-vous au mémo enregistré **
Faites juste une note API.
Utilisez le framework d'application Web Flask pour créer une API simple.
Je republierai les exigences, mais l'API créée cette fois est
est.
Les implémentations qui satisfont aux exigences traiteront memo
avec memo_id
comme clé primaire.
Tout d'abord, préparez un point de terminaison qui exécute les deux points ci-dessus.
Utilisez Flask pour préparer un point de terminaison pour ** recevoir des requêtes POST et enregistrer des notes **.
from flask import Flask, request
app = Flask(__name__)
@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
#Obtenez de la valeur de la demande
memo: str = request.form["memo"]
pass
De même, préparez un point de terminaison pour ** recevoir une requête GET et référencer le mémo enregistré **.
@app.route('/memo/<int:memo_id>')
def get(memo_id: int) -> str:
pass
Décrivons maintenant l'interaction avec la base de données qui stocke le mémo sur ce point de terminaison. Cette fois, mysql est utilisé comme base de données à sauvegarder.
Tout d'abord, préparez une fonction pour vérifier si memo
existe dans memo_id
.
from mysql import connector
#Paramètres de connexion à la base de données
config = {
'user': 'root',
'password': 'password',
'host': 'mysql',
'database': 'test_database',
'autocommit': True
}
def exist(memo_id: int) -> bool:
#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
Ensuite, ajoutez la ** réponse à la demande POST et enregistrez le processus de note ** au point de terminaison créé.
from flask import Flask, request, jsonify
from mysql import connector
from werkzeug.exceptions import Conflict
app = Flask(__name__)
@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
#Vérifiez s'il y a un identifiant spécifié
is_exist: bool = exist(memo_id)
if is_exist:
raise Conflict(f'memo_id [{memo_id}] is already registered.')
#Obtenez de la valeur de la demande
memo: str = request.form["memo"]
#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 jsonify(
{
"message": "saved."
}
)
Ensuite, implémentez le ** processus qui reçoit la requête ** GET et fait référence au mémo enregistré dans la base de données externe **.
from werkzeug.exceptions import NotFound
@app.route('/memo/<int:memo_id>')
def get(memo_id: int) -> str:
#Vérifiez s'il y a un identifiant spécifié
is_exist: bool = 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 jsonify(
{
"message": f'memo : [{result[1]}]'
}
)
Ensuite, définissez le gestionnaire d'erreurs.
from http import HTTPStatus
from flask import make_response
@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)
Enfin, le processus de démarrage de ʻapp` avec chaque routeur généré jusqu'à présent est décrit dans le fichier.
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
main.py
from http import HTTPStatus
from flask import Flask, request, jsonify, make_response
from mysql import connector
from werkzeug.exceptions import Conflict, NotFound
app = Flask(__name__)
#Paramètres de connexion à la base de données
config = {
'user': 'root',
'password': 'password',
'host': 'mysql',
'database': 'test_database',
'autocommit': True
}
def exist(memo_id: int) -> bool:
#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
@app.route('/memo/<int:memo_id>')
def get(memo_id: int) -> str:
#Vérifiez s'il y a un identifiant spécifié
is_exist: bool = 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 jsonify(
{
"message": f'memo : [{result[1]}]'
}
)
@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
#Vérifiez s'il y a un identifiant spécifié
is_exist: bool = exist(memo_id)
if is_exist:
raise Conflict(f'memo_id [{memo_id}] is already registered.')
#Obtenez de la valeur de la demande
memo: str = request.form["memo"]
#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 jsonify(
{
"message": "saved."
}
)
@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)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
Vous disposez maintenant d'une API qui effectue les deux opérations suivantes:
Dans les articles suivants, pour chaque partie, tout le code, y compris l'environnement du conteneur, est stocké dans le référentiel suivant, donc Si vous souhaitez le déplacer à portée de main, veuillez vous référer à ce qui suit.
Part1: https://github.com/y-tomimoto/CleanArchitecture/tree/master/part1
À partir de la partie suivante, supposons une demande de modification de spécification pour cette API et appliquons une architecture propre étape par étape.