[CleanArchitecture with Python] Teil 2: Frameworks & Drivers Layer: Die Entstehung des Web

Im vorherigen Teil 1, so monolithisch wie möglich,

  1. ** POST-Anfrage erhalten und Notizen speichern **

  2. ** Erhalten Sie eine GET-Anfrage und beziehen Sie sich auf das gespeicherte Memo **

Wir haben nur eine Memo-API vorbereitet.

In diesem Artikel basiert die Erklärung auf dem folgenden Code, der im vorherigen Kapitel erstellt wurde.

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

1. Erhalten Sie eine Spezifikationsänderungsanforderung für das Ergebnis

Ich habe eine Anfrage erhalten, die Spezifikationen der in Teil 1 erstellten "API, die mit dem Flask-Framework erstellt wurde" zu ändern.

** "Nehmen wir FastAPI anstelle von Flask für das Webanwendungsframework an." **

In Teil 1 betrachten wir ein Design, das gegenüber Spezifikationsänderungen resistent ist, unter der Annahme dieser Spezifikationsänderungsanforderung.


Ich habe nicht viele Fälle erlebt, in denen ich das Framework ersetzen möchte, aber ich dachte, es wäre ein leicht verständlicher Fall als Einführung, also habe ich ihn übernommen.

Abgesehen davon ist dies meine jüngste Erfahrung, aber aufgrund von Änderungen der Marktbedingungen wurde plötzlich der Antwortheader einer bestimmten Webanwendung hinzugefügt. Es gab eine Situation, in der ich einen bestimmten Header geben wollte.

Da das Header-Attribut in den letzten Jahren hinzugefügt wurde, wurde das zu diesem Zeitpunkt verwendete Webanwendungsframework verwendet In einigen Fällen wurde das Header-Attribut nicht unterstützt und das Webanwendungsframework selbst musste geändert werden. (Am Ende habe ich den Header roh in den benutzerdefinierten Header geschrieben und geantwortet, und ich habe nichts bekommen, aber ...)

2. Bedenken bei der Beantwortung von Spezifikationsänderungsanforderungen mit dem aktuellen Design

Kommen wir nun zur Geschichte zurück.

Derzeit werden die folgenden Prozesse in "main.py" zusammengefasst.

  1. Akzeptieren Sie Anfragen über das Framework
  2. Führen Sie die ursprünglich von der Anwendung erwartete Verarbeitung aus (Memo erfassen / speichern).

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

Codierung bei Änderungen am aktuellen Design

Was passiert, wenn Sie das Framework ändern, das Sie in Ihrem aktuellen Design verwenden?

Wenn Sie versuchen, das Framework von Flask auf Fast API zu ändern Sie werden wahrscheinlich die folgenden Änderungen an der vorhandenen main.py vornehmen.

  1. Schreiben Sie den vom Framework konfigurierten Router neu
  2. Schreiben Sie das Antwortformat neu
  3. Schreiben Sie den Fehlerbehandler neu
  4. Schreiben Sie neu, wie Sie die App starten

Wenn Sie das aktuelle Design beibehalten und tatsächliche Änderungen an der vorhandenen Datei "main.py" vornehmen, wird Folgendes angezeigt.

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

#Einstellungen für die DB-Verbindung
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

Obwohl es möglich ist, die Spezifikationen auf diese Weise mit Gewalt zu ändern, gibt es einige Bedenken.

Bedenken hinsichtlich der Codierung bei Änderungen am aktuellen Design

Dieser Fix ändert den Framework-Code in main.py.

In main.py wird jedoch nicht nur der Code beschrieben, der sich auf das Framework bezieht, sondern auch der ** Prozess des Abrufens und Speicherns von Notizen **, der ursprünglich von der Anwendung erwartet wurde.

Prinzip der Einzelverantwortung: Prinzip der Einzelverantwortung: https://note.com/erukiti/n/n67b323d1f7c5

Zu diesem Zeitpunkt können Sie versehentlich unnötige Änderungen am "Prozess des Erfassens und Speicherns von Notizen" vornehmen, den Sie ursprünglich von der Anwendung erwartet hatten **.

Ich möchte die Situation vermeiden, in der ich Korrekturen vornehme, während ich denke, dass dies versehentlich einen Fehler im Code verursachen kann, der bereits funktioniert.

In diesem Beispiel gibt es nur zwei Endpunkte. Wenn es sich jedoch um einen großen Dienst handelt und Sie mehrere Endpunkte haben, ist dieses Problem noch größer.

Offenes / geschlossenes Prinzip: Offenes / geschlossenes Prinzip: https://medium.com/eureka-engineering/go-open-closed-principle-977f1b5d3db0

3. Überlegen Sie sich als Antwort auf die Anfrage, durch welche Art von Design die Spezifikationen basierend auf Clean Architecture reibungslos geändert werden konnten.

i. Designanliegen neu organisieren

Bedenken: ** Kann unnötige Änderungen an vorhandenem Code vornehmen, der ordnungsgemäß funktioniert **

Ii. Welche Art von Design könnte die Bedenken vermeiden und die Spezifikationen ändern?

Dieses Problem ist auf die Tatsache zurückzuführen, dass main.py nicht nur das Framework enthält, sondern auch den ** Prozess zum Abrufen und Speichern von ** Notizen **, der ursprünglich von der Anwendung erwartet wurde.

Daher ist die Sorge dieses Mal "main.py",

Es scheint gelöst zu werden, indem es in ** Framework ** und ** Verarbeitung unterteilt wird, die ursprünglich von der Anwendung erwartet werden **.

Wenn der Code in Rollen unterteilt werden soll, kann der Umfang der Änderung anscheinend auf diese Rolle beschränkt werden.

Iii. Wenn das ideale Design von CleanArchitecture interpretiert wird

In main.py,

  1. Empfangen Sie Anfragen mit dem Kolbenrahmen
  2. Speichern Sie das Memo oder holen Sie sich das Memo

Es gibt zwei Prozesse.

Mit anderen Worten, in Bezug auf CleanArchitecture,

  1. Webanwendungsframework
  2. Funktionen, die ursprünglich von der Anwendung erwartet wurden

ist.

Bei der Interpretation mit CleanArchitecture ist in der folgenden Abbildung Folgendes dargestellt:

  1. Es scheint, dass "1" als Web (Teil der Frameworks & Drivers-Schicht) beschrieben werden kann.

  2. In Bezug auf "2" scheint es, da es sich um eine Funktion handelt, die ursprünglich von der Anwendung erwartet wurde, entweder der Ebene "Anwendungsgeschäftsregeln" oder der Ebene "Unternehmensgeschäftsregeln" zu entsprechen. Hier jedoch "Speichern Sie das Memo oder rufen Sie das Memo ab" Beschreiben wir die Funktion als MemoHandler.

Es scheint ausgedrückt zu werden als.

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

Teilen wir nun main.py in die Frameworks & Drivers-Ebene auf: Web und MemoHandler.

Iv. Tatsächliche Codierung

Rufen Sie in main.py die Ebene Frameworks & Drivers auf: Web Router, Entwerfen Sie, um "memo_handler.py" von jedem Router aufzurufen.

Wenn Sie mit diesem Design das Framework ändern möchten, ändern Sie einfach das in main.py aufgerufene Framework. Der vorhandene Prozess memo_handler.py selbst wird nicht geändert, sodass der vorhandene Prozess nicht versehentlich geändert wird.

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

Frameworks & Treiber-Ebene

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

#Konfiguration für SQL Client
config = {
    'user': 'root',
    'password': 'password',
    'host': 'mysql',
    'database': 'test_database',
    'autocommit': True
}


class MemoHandler:

    def exist(self, memo_id: int):
        #Erstellen Sie einen DB-Client
        conn = connector.connect(**config)
        cursor = conn.cursor()

        # memo_Überprüfen Sie, ob eine ID vorhanden ist
        query = "SELECT EXISTS(SELECT * FROM test_table WHERE memo_id = %s)"
        cursor.execute(query, [memo_id])
        result: tuple = cursor.fetchone()

        #Schließen Sie den DB-Client
        cursor.close()
        conn.close()

        #Überprüfen Sie die Existenz, indem Sie prüfen, ob es ein Suchergebnis gibt
        if result[0] == 1:
            return True
        else:
            return False

    def get(self, memo_id: int):

        #Überprüfen Sie, ob eine bestimmte ID vorhanden ist
        is_exist: bool = self.exist(memo_id)

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

        #Erstellen Sie einen DB-Client
        conn = connector.connect(**config)
        cursor = conn.cursor()
        # memo_Führen Sie eine Suche nach ID durch
        query = "SELECT * FROM test_table WHERE memo_id = %s"
        cursor.execute(query, [memo_id])
        result: tuple = cursor.fetchone()

        #Schließen Sie den DB-Client
        cursor.close()
        conn.close()

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

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

        #Überprüfen Sie, ob eine bestimmte ID vorhanden ist
        is_exist: bool = self.exist(memo_id)

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

        #Erstellen Sie einen DB-Client
        conn = connector.connect(**config)
        cursor = conn.cursor()

        #Memo speichern
        query = "INSERT INTO test_table (memo_id, memo) VALUES (%s, %s)"
        cursor.execute(query, (memo_id, memo))

        #Schließen Sie den DB-Client
        cursor.close()
        conn.close()

        return "saved."



main.py

Wechseln Sie das zu übernehmende Framework auf "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

---

#Bei der Übernahme der Flasche als Rahmen
flask_app.run(debug=True, host='0.0.0.0')

---

#Schnell wie ein Rahmen_Bei der Annahme von API
uvicorn.run(app=fastapi_app, host="0.0.0.0",port=5000)

4. Welche Art von Spezifikationsänderungen haben die Designänderungen ermöglicht, standzuhalten?

Der endgültige Code ist hier. : https://github.com/y-tomimoto/CleanArchitecture/blob/master/part2

Durch Ausschneiden jedes Frameworks in die Ebene Frameworks & Drivers: Web und Ausschneiden der Verarbeitung, die ursprünglich von der Anwendung für "MemoHandler" erwartet wurde, Durch einfaches Aufrufen des Routers, den Sie mit main.py übernehmen möchten, können Sie das Framework ** flexibel ändern, ohne memo_handler.py zu ändern. Dies ist der Prozess, den Sie ursprünglich von der Anwendung erwartet hatten. Ich tat.

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

Dieses Design implementiert eine der CleanArchitecture-Regeln, ** Framework Independence **.

Saubere Architektur (übersetzt von The Clean Architecture): https://blog.tai2.net/the_clean_architecture.html

Framework-Unabhängigkeit: Die Architektur ist nicht auf die Verfügbarkeit einer funktionsreichen Softwarebibliothek angewiesen. Dies ermöglicht die Verwendung solcher Frameworks als Werkzeuge und zwingt das System nicht dazu, in die begrenzten Einschränkungen des Frameworks gezwungen zu werden.

Recommended Posts

[CleanArchitecture with Python] Teil 2: Frameworks & Drivers Layer: Die Entstehung des Web
[Teil 2] Crawlen mit Python! Klicken Sie auf die Webseite, um sich zu bewegen!
Laden Sie mit Python Dateien im Web herunter
Versuchen Sie es mit dem Python-Webframework Tornado Part 1
Versuchen Sie es mit dem Python-Webframework Tornado Part 2
Machen Sie mit Python einen Haltepunkt auf der c-Ebene
Webanwendung erstellt mit Python3.4 + Django (Teil.1 Umgebungskonstruktion)
Bildverarbeitung mit Python (Teil 2)
Python mit freeCodeCamp Teil1 studieren
Angrenzende Bilder mit Python Teil 1
Web Scraping mit Python + JupyterLab
Schaben mit Selen + Python Teil 1
Python studieren mit freeCodeCamp part2
Bildverarbeitung mit Python (Teil 1)
Web-API mit Python + Falcon
Vergleich von 4 Arten von Python-Webframeworks
Nampre mit Python lösen (Teil 2)
Bildverarbeitung mit Python (3)
Schaben mit Selen + Python Teil 2
Rufen Sie die API mit python3 auf.
Webanwendung mit Python + Flask ② ③
Speichern Sie Bilder im Web mit Python (Colab) auf einem Laufwerk.
Web Scraping Anfänger mit Python
Optimieren Sie die Websuche mit Python
Webanwendung mit Python + Flask ④
Die erste künstliche Intelligenz. Fordern Sie die Webausgabe mit Python heraus. ~ Kolbeneinführung
[Python, Ruby] Selen-Holen Sie sich Webseiteninhalte mit Webdriver
Visualisieren Sie Ihre Taschengelddateien mit Dash, dem Python-Webframework
Spielen Sie handschriftliche Zahlen mit Python Part 1
Extrahieren Sie die xz-Datei mit Python
[Mit Python automatisiert! ] Teil 1: Datei einstellen
Erste Schritte mit Python-Webanwendungen
Web Scraping mit Python Erster Schritt
Ich habe versucht, WebScraping mit Python.
Überwachen Sie Python-Webanwendungen mit Prometheus
Holen Sie sich das Wetter mit Python-Anfragen
Holen Sie sich Web-Screen-Capture mit Python
Finden Sie die Bearbeitungsentfernung (Levenshtein-Entfernung) mit Python
Klicken Sie mit Python auf die Etherpad-Lite-API
Installieren Sie das Python-Plug-In mit Netbeans 8.0.2
Klicken Sie auf die Web-API in Python
Ich mochte den Tweet mit Python. ..
Beherrsche den Typ mit Python [Python 3.9 kompatibel]
Automatisieren Sie einfache Aufgaben mit Python Part0
[Mit Python automatisiert! ] Teil 2: Dateivorgang
So schneiden Sie den unteren rechten Teil des Bildes mit Python OpenCV
Treffen Sie eine Methode einer Klasseninstanz mit der Python Bottle Web API
Machen Sie die Python-Konsole mit UNKO bedeckt
Einführung in das BOT-Framework Minette für Python
[Python] Legen Sie den Diagrammbereich mit matplotlib fest
WEB-Scraping mit Python (für persönliche Notizen)
Spielen Sie handschriftliche Zahlen mit Python Teil 2 (identifizieren)
Verarbeiten Sie Pubmed .xml-Daten mit Python [Teil 2]
Hinter dem Flyer: Docker mit Python verwenden
Automatisieren Sie einfache Aufgaben mit Python Part1 Scraping
Erste Schritte mit Python Web Scraping Practice
Überprüfen Sie die Existenz der Datei mit Python
[Python] Ruft den Variablennamen mit str ab
[Python] Runden Sie nur mit dem Operator ab