[CleanArchitecture with Python] Apply CleanArchitecture step by step to a simple API and try to understand "what kind of change is strong" in the code base.

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


Summary: How to be resistant to changes for each layer you adopt

Presenting each benefit obtained by applying Clean Architecture step by step

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

Part2: Frameworks & Drivers Layer: Introducing the Web

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.

Part3: Enterprise Business Rules Layer & Application Business Rules Layer Appears

memo_handler.py that describes the processing that you originally expected from the application

Divided into.

This will make memo_handler.py,

  1. Principle processing in the application and
  2. Utilizing them for fluid processing that meets the application specifications

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.

Part4: Interface Adapters Layer: Introducing Controllers

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.

Part5: ~ Extra Edition ~ Utilization of DTO

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.

Part6: Interface Adapters Layer: Presenter Appears

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.

Part7: Frameworks & Drivers tier: DB and Interface Adapters tier: Gateways is here

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.

Part8: Enterprise Business Rules Layer: Adopting Entity & Value Object

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.

Why write this article

Recently, I was assigned to a project that could be technically challenged, so I adopted Clean Architecture.

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

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.

How to write an article

As mentioned above, the articles currently published about Clean Architecture are I personally think that it often consists of the following two parts.

  1. The code of the artifact created by * Clean Architecture looks like this. *
  2. The code of * ○○ corresponds to the ○○ layer, and the ○○ layer plays such a role. *

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

    • We will gradually solve the problems that existing deliverables have when changing specifications *
    • The final configuration is CleanArchitecture *

I'm going to make it.

About the story of each part

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.

table of contents

[Part1: Create a simple base API](https://qiita.com/y_tom/items/ac6f6a08bdc374336dc4#part1-%E3%83%99%E3%83%BC%E3%82%B9%E3 % 81% A8% E3% 81% AA% E3% 82% 8B% E3% 82% B7% E3% 83% B3% E3% 83% 97% E3% 83% AB% E3% 81% AA-api-% E3% 82% 92% E4% BD% 9C% E6% 88% 90% E3% 81% 99% E3% 82% 8B-1)

Part2: Frameworks & Drivers Layer: Introducing the Web

Part3: Enterprise Business Rules Layer & Application Business Rules Layer Appears

Part4: Interface Adapters Layer: Introducing Controllers

Part5: ~ Extra Edition ~ Utilization of DTO

Part6: Interface Adapters Layer: Presenter Appears

Part7: Frameworks & Drivers tier: DB and Interface Adapters tier: Gateways is here

Part8: Enterprise Business Rules Layer: Adopting Entity & Value Object

Part9: Testable ~ Summary

Part1: Create a simple base API

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.

The first deliverable to apply Clean Architecture in stages

This time

  1. ** Receive a POST request and save a note **

  2. ** Receive a GET request and refer to the saved memo **

Just make a note API.

Implementation

Employ the web application framework Flask to create a simple api.

1. Prepare the endpoint

I will repost the requirements, but the api created this time is

  1. ** Receive a POST request and save a note **
  2. ** Receive a GET request and refer to the saved memo **

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.


  1. 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
    
  2. 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
    

2. Prepare a part for exchanging memos with DB

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.


  1. 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
    
  2. 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."
            }
        )
    
    

  1. 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]}]'
            }
        )
    
    
  2. 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)
    

3. Launch app

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

4. Final code

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


After Part 1

Now you have an API that does the following two things:

  1. ** Receive a POST request and save a note **
  2. ** Receive a GET request and refer to the saved memo **

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

[CleanArchitecture with Python] Apply CleanArchitecture step by step to a simple API and try to understand "what kind of change is strong" in the code base.
Try to implement and understand the segment tree step by step (python)
[Python] Visualize Arashi's lyrics with WordCloud and try to understand what I wanted to convey to fans in the 20th year of formation.
[Introduction to statistics] What kind of distribution is the t distribution, chi-square distribution, and F distribution? A little summary of how to use [python]
A story about porting the code of "Try and understand how Linux works" to Rust
Find the white Christmas rate by prefecture with Python and map it to a map of Japan
If you try to make a word cloud with comments from WEB manga, it is interesting to visually understand what kind of manga it is.
[Introduction to Python] What is the difference between a list and a tuple?
Sample code to get the Twitter API oauth_token and oauth_token_secret in Python 2.7
What kind of book is the best-selling "Python Crash Course" in the world?
List of language codes used in twitter (including API) (with Python dictionary). What is the most commonly used language?
Introduction and usage of Python bottle ・ Try to set up a simple web server with login function
[Introduction to Python] What is the method of repeating with the continue statement?
What is the fastest way to create a reverse dictionary in python?
Try to solve the traveling salesman problem with a genetic algorithm (Python code)
How to get a list of files in the same directory with python
Try to find the probability that it is a multiple of 3 and not a multiple of 5 when one is removed from a card with natural numbers 1 to 100 using Ruby and Python.
An introduction to cross-platform GUI software made with Python / Tkinter! (And many Try and Error)! (In the middle of writing)
Run the output code with tkinter, saying "A, pretending to be B" in python
Automate background removal for the latest portraits in a directory with Python and API
How to identify the element with the smallest number of characters in a Python list?
[Python] Change the text color and background color of a specific keyword in print output
[Note] How to write QR code and description in the same image with python
How to check in Python if one of the elements of a list is in another list
It is easy to execute SQL with Python and output the result in Excel
What is God? Make a simple chatbot with python
Make a note of what you want to do in the future with Raspberry Pi
I made a class to get the analysis result by MeCab in ndarray with python
Recursively get the Excel list in a specific folder with python and write it to Excel.
I also tried to imitate the function monad and State monad with a generator in Python
I wrote a doctest in "I tried to simulate the probability of a bingo game with Python"
[Python] What is a tuple? Explains how to use without tuples and how to use it with examples.
Try to import to the database by manipulating ShapeFile of national land numerical information with Python
Try scraping the data of COVID-19 in Tokyo with Python
Try hitting the Twitter API quickly and easily with Python
Change the standard output destination to a file in Python
I want to improve efficiency with Python even in the experimental system (5) I want to send a notification at the end of the experiment with the slack API
Try to bring up a subwindow with PyQt5 and Python
Try to automate the operation of network devices with Python
[Python] The first step to making a game with Pyxel
Just try to receive a webhook in ngrok and python
[Python] What is a slice? An easy-to-understand explanation of how to use it with a concrete example.
Write a script in Shell and Python to notify you in Slack when the process is finished
I tried to verify the speaker identification by the Speaker Recognition API of Azure Cognitive Services with Python. # 1
[Python] What is pip? Explain the command list and how to use it with actual examples
I tried to verify the speaker identification by the Speaker Recognition API of Azure Cognitive Services with Python. # 2
What to do when a part of the background image becomes transparent when the transparent image is combined with Pillow
[Python] The role of the asterisk in front of the variable. Divide the input value and assign it to a variable
A simple reason why the return value of round (2.675,2) is 2.67 in python (it should be 2.68 in reality ...)
Minimum knowledge required when dealing with "angles" and "coordinates" in Ruby + In what direction is Mr. B from the perspective of Mr. A? Algorithm
Operate Jupyter with REST API to extract and save Python code
How to determine the existence of a selenium element in Python
A simple data analysis of Bitcoin provided by CoinMetrics in Python
Rubyist tried to make a simple API with Python + bottle + MySQL
Try to get a list of breaking news threads in Python.
[Cloudian # 9] Try to display the metadata of the object in Python (boto3)
How to give and what the constraints option in scipy.optimize.minimize is
How to check the memory size of a variable in Python
What I was addicted to with json.dumps in Python base64 encoding
First python ② Try to write code while examining the features of python
Try to create a python environment with Visual Studio Code & WSL