Click here for the final sample code of Clean Architecture using 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
By cutting out each Web application framework you want to adopt to Frameworks & Drivers layer: Web, and cutting out the processing originally expected from the application to MemoHandler
,
By simply calling the router you want to adopt with main.py
, you can flexibly change the framework ** without modifying memo_handler.py
, which is the process you originally expected from the application. ..
This design implements one of the rules of CleanArchitecture, ** framework independence **.
Clean Architecture (Translated by The Clean Architecture): https://blog.tai2.net/the_clean_architecture.html
Framework Independence: The architecture does not rely on the availability of a full-featured library of software. This allows such frameworks to be used as tools and does not force the system to be forced into the limited constraints of the framework.
memo_handler.py
that describes the processing that you originally expected from the application
Divided into.
This will make memo_handler.py
,
By dividing into, when changing the specifications of the application, it is designed so that the specifications can be flexibly modified and extended without affecting the existing principle processing.
By leveraging the Controller in the Interface Adapters layer The part of changing the frequently updated "external request format" to a format suitable for actual processing, I was able to cut it out of the framework.
This allows you to change the format of requests that your application can accept. It is designed so that you can modify your code without considering existing web application frameworks or business rules.
Adopting DTO facilitates data access between layers and at the same time It is designed so that the impact on each layer can be minimized when the data structure handled by the application changes.
In addition to implementing Presenter, we also implemented OutputPort.
As a result, when changing the UI, it is designed so that only the UI can be changed independently without considering the existing web application framework or business rules.
With the introduction of this Presenter, CleanArchitecture rules and UI independence have been achieved.
Clean Architecture (Translated by The Clean Architecture): https://blog.tai2.net/the_clean_architecture.html
The UI can be easily changed. No need to change the rest of the system. For example, the web UI can be replaced with the console UI without changing business rules.
We implemented DB in the DB layer and adopted Gataways,
As a result, when changing the DB, it is designed so that the DB can be switched without considering each layer.
As a result, CleanArchitecture rules and database independence have been achieved.
Clean Architecture (Translated by The Clean Architecture): https://blog.tai2.net/the_clean_architecture.html
Database independent. You can replace Oracle or SQL Server with Mongo, BigTable, CoucheDB or anything else. Business rules are not tied to the database.
Communicate with DB using Entity, an object with the same structure as the database In order to hide highly confidential properties, we designed it to adopt DTO within each Business Rule.
As a result, each Business Rule is designed so that it can handle values on the DB without being aware of confidential properties.
In addition, the validate and processing of each property is made independent of Entity by adopting ValueObject. As a result, when creating or changing an Entity, it is no longer necessary to implement a specific property in each Entity.
Recently, I was assigned to a project that could be technically challenged, so I adopted Clean Architecture.
I wanted to re-verbalize what I learned when I hired him.
When I was implementing it, I thought it would have been better if there was an article explaining the problems that each layer would solve in the code base.
I decided to write this article.
As mentioned above, the articles currently published about Clean Architecture are I personally think that it often consists of the following two parts.
** In imagining "what kind of changes the Clean Architecture is specifically resistant to" **
It is not a structure that presents the code of the already completed artifact from the beginning, but
CleanArchitecture
*I'm going to make it.
What I want to clarify in this article is
** "What kind of changes is Clean Architecture specifically resistant to?" **
is.
So, in the article, we will apply Clean Architecture with the following development.
Let's get started.
In Part 1, we will create an API that will be the basis for the explanation of the following Part.
When creating
I tried to implement this API so that it would be intentionally monolithic without assuming a specification change **.
By intentionally making it monolithic, the aim is to make it easier to visualize the benefits of the design when applying CleanArchitecture.
Let's observe in the following parts how the file is gradually divided by responsibility and the combination gradually becomes loose.
This time
** Receive a POST request and save a note **
** Receive a GET request and refer to the saved memo **
Just make a note API.
Employ the web application framework Flask to create a simple api.
I will repost the requirements, but the api created this time is
is.
Implementations that meet the requirements will treat memo
with memo_id
as the primary key.
First, prepare an endpoint that executes the above two points.
Use Flask to prepare an endpoint for ** receiving POST requests and saving notes **.
from flask import Flask, request
app = Flask(__name__)
@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
#Get value from request
memo: str = request.form["memo"]
pass
Similarly, prepare an endpoint for ** receiving a GET request and referencing the saved memo **.
@app.route('/memo/<int:memo_id>')
def get(memo_id: int) -> str:
pass
Now, let's describe the interaction with the DB that stores the memo on this endpoint. This time, mysql is used as the db to save.
First, prepare a function to check if memo
exists in memo_id
.
from mysql import connector
#Settings for DB connection
config = {
'user': 'root',
'password': 'password',
'host': 'mysql',
'database': 'test_database',
'autocommit': True
}
def exist(memo_id: int) -> bool:
#Create a DB client
conn = connector.connect(**config)
cursor = conn.cursor()
# memo_Check if there is an id
query = "SELECT EXISTS(SELECT * FROM test_table WHERE memo_id = %s)"
cursor.execute(query, [memo_id])
result: tuple = cursor.fetchone()
#Close the DB client
cursor.close()
conn.close()
#Check for existence by checking if there is one search result
if result[0] == 1:
return True
else:
return False
Next, add the ** response to POST request and save note ** process to the created endpoint.
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:
#Check if there is a specified id
is_exist: bool = exist(memo_id)
if is_exist:
raise Conflict(f'memo_id [{memo_id}] is already registered.')
#Get value from request
memo: str = request.form["memo"]
#Create a DB client
conn = connector.connect(**config)
cursor = conn.cursor()
#Save memo
query = "INSERT INTO test_table (memo_id, memo) VALUES (%s, %s)"
cursor.execute(query, (memo_id, memo))
#Close the DB client
cursor.close()
conn.close()
return jsonify(
{
"message": "saved."
}
)
Next, implement the ** process that receives the ** GET request and refers to the memo saved in the external DB **.
from werkzeug.exceptions import NotFound
@app.route('/memo/<int:memo_id>')
def get(memo_id: int) -> str:
#Check if there is a specified id
is_exist: bool = exist(memo_id)
if not is_exist:
raise NotFound(f'memo_id [{memo_id}] is not registered yet.')
#Create a DB client
conn = connector.connect(**config)
cursor = conn.cursor()
# memo_Perform a search by id
query = "SELECT * FROM test_table WHERE memo_id = %s"
cursor.execute(query, [memo_id])
result: tuple = cursor.fetchone()
#Close the DB client
cursor.close()
conn.close()
return jsonify(
{
"message": f'memo : [{result[1]}]'
}
)
Next, set up the error handler.
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)
Finally, the process of starting ʻapp` with each router generated so far is described in the file.
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__)
#Settings for DB connection
config = {
'user': 'root',
'password': 'password',
'host': 'mysql',
'database': 'test_database',
'autocommit': True
}
def exist(memo_id: int) -> bool:
#Create a DB client
conn = connector.connect(**config)
cursor = conn.cursor()
# memo_Check if there is an id
query = "SELECT EXISTS(SELECT * FROM test_table WHERE memo_id = %s)"
cursor.execute(query, [memo_id])
result: tuple = cursor.fetchone()
#Close the DB client
cursor.close()
conn.close()
#Check for existence by checking if there is one search result
if result[0] == 1:
return True
else:
return False
@app.route('/memo/<int:memo_id>')
def get(memo_id: int) -> str:
#Check if there is a specified id
is_exist: bool = exist(memo_id)
if not is_exist:
raise NotFound(f'memo_id [{memo_id}] is not registered yet.')
#Create a DB client
conn = connector.connect(**config)
cursor = conn.cursor()
# memo_Perform a search by id
query = "SELECT * FROM test_table WHERE memo_id = %s"
cursor.execute(query, [memo_id])
result: tuple = cursor.fetchone()
#Close the DB client
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:
#Check if there is a specified id
is_exist: bool = exist(memo_id)
if is_exist:
raise Conflict(f'memo_id [{memo_id}] is already registered.')
#Get value from request
memo: str = request.form["memo"]
#Create a DB client
conn = connector.connect(**config)
cursor = conn.cursor()
#Save memo
query = "INSERT INTO test_table (memo_id, memo) VALUES (%s, %s)"
cursor.execute(query, (memo_id, memo))
#Close the DB client
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')
Now you have an API that does the following two things:
In the following articles, for each part, all the code including the container environment is stored in the following repository, so If you want to move it at hand, please refer to the following.
Part1: https://github.com/y-tomimoto/CleanArchitecture/tree/master/part1
From the next part, let's assume a specification change request for this API and apply Clean Architecture step by step.
Recommended Posts