AI / Kiến thức · 14/06/2023 0

5 Design Patterns phải biết khi xây dựng ứng dụng với FastAPI

Design Patterns là những giải pháp có thể tái sử dụng cho những vấn đề thường gặp trong phát triển phần mềm, qua đó, ta có thể tiết kiệm thời gian và công sức bằng cách không phải giải quyết những vấn đề tương tự một cách lặp đi lặp lại.

Trong FastAPI, đây là những Design Pattern mà bạn cần phải biết:

  • Singleton: đảm bảo chỉ có một instance của một class tồn tại và đảm bảo có thể truy xuất nó từ mọi nơi (global access)
  • Factory: tạo ra các object mà không cần chỉ định class chính xác của object được tạo ra.
  • Observer: xác định quan hệ một-nhiều giữa các object sao cho khi một object thay đổi trạng thái, tất cả các object phụ thuộc của nó sẽ được thông báo và cập nhật tự động.
  • Strategy: cho phép bạn định nghĩa một nhóm thuật toán, đóng gói từng thuật toán và làm cho chúng có thể thay thế lẫn nhau tùy thời điểm chạy.
  • Decorator: cho phép bạn thêm hành vi vào một object cụ thể, cả tĩnh và động, mà không ảnh hưởng đến hành vi của các đối tượng khác cùng lớp.

1. Singleton

 Singleton Pattern là pattern mà đảm bảo chỉ có duy nhất một instance của một class tồn tại, và ta có thể truy xuất nó từ bất cứ đâu trong chương trình (global access). Pattern này thường được sử dụng khi bạn cần đảm bảo chỉ có duy nhất một instance trên toàn hệ thống, ví dụ như database connection hay hệ thống logging.

class Singleton:
    _instance = None

    def __new__(cls):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

Ta có thể định nghĩa một private constructor và một static method trả về instance của chính nó. Static method sẽ kiểm tra xem instance của class đã được tạo chưa, nếu rồi, nó sẽ trả về instance đó, nếu không, nó sẽ tạo một instance mới và trả về. Instance này sẽ được lưu nhưu một private static variable trong class

Đây là một ví dụ về Singleton trong FastAPI

from fastapi import FastAPI, Depends
from pymongo import MongoClient
from typing import Optional

class Database:
    instance = None

    def __new__(cls):
        if cls.instance is None:
            cls.instance = super().__new__(cls)
            cls.instance.client = MongoClient("mongodb://localhost:27017/")
            cls.instance.db = cls.instance.client["mydatabase"]
        return cls.instance

def get_db() -> Optional[Database]:
    return Database()

app = FastAPI()

@app.get("/")
def read_root(db: Optional[Database] = Depends(get_db)):
    if db:
        result = db.db.my_collection.find_one()
        return {"message": result}
    else:
        return {"message": "Failed to connect to database."}

Trong ví dụ trên, ta định nghĩa một class Database để tạo một connection tới mongoDB thông qua Pymongo. class Database này sử dụng một Singleton để đảm bảo chỉ có một instance duy nhất được tạo ra.

Sau đó, ta định nghĩa một function get_db trả về instance của class Database, function này được sử dụng như một dependancy trong FastAPI route.

Khi hàm read_root được gọi, hàm phụ thuộc get_db sẽ được gọi theo và tạo một instance của class Database nếu nó chưa được tạo trước đó.

Bằng cách sử dụng Singleton, ta có thể đảm bảo chỉ có 1 instance của Database được tạo ra và qua đó có thể giảm tài nguyên sử dụng và cải thiện hiệu suất, đặc biệt là cho những ứng dụng truy xuất tới databse thường xuyên.

Lưu ý rằng trong khi ví dụ này kết nối mongoDB thông qua hàm get_db, bạn cũng có thể sử dụng Singleton với bất kỳ thư viện quản lý database nào khác như sqlite, sqlalchemy

2. Factory

Factory pattern là một pattern cung cấp khả năng tạo object mà không cần chỉ định chính xác class. Pattern này thường được sử dụng trong trường hợp bạn có một class dạng thế hệ bvà bạn muộn tạo objject của những class khác nhau dựa trên input, giống như những lựa chọn của user hoặc là tham số cài đặt.

class ShapeFactory:
    @staticmethod
    def create_shape(shape_type):
        if shape_type == "circle":
            return Circle()
        elif shape_type == "square":
            return Square()
        elif shape_type == "triangle":
            return Triangle()
        else:
            raise ValueError("Invalid shape type")

Để triển khai Factory pattern, bạn có thể định nghĩa một mothod factory, tạo và trả về một instance của class phù hợp dựa trên một vài input. Method này phải là static method và được định nghĩa trong một class khác với based class trong cây thế hệ.

Đây là một ví dụ về cách sử dụng một factory pattern trong FastAPI để tạo ra một dymamic object dựa trên input

from fastapi import FastAPI, HTTPException

class PaymentProcessor:
    def process_payment(self, amount: float):
        pass

class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount: float):
        return f"Processing credit card payment for {amount} dollars."

class PayPalProcessor(PaymentProcessor):
    def process_payment(self, amount: float):
        return f"Processing PayPal payment for {amount} dollars."

class PaymentProcessorFactory:
    @staticmethod
    def create_processor(processor_type: str) -> PaymentProcessor:
        if processor_type == "credit_card":
            return CreditCardProcessor()
        elif processor_type == "paypal":
            return PayPalProcessor()
        else:
            raise HTTPException(status_code=400, detail="Invalid payment processor type.")

app = FastAPI()

@app.post("/process_payment/{processor_type}")
def process_payment(processor_type: str, amount: float):
    processor = PaymentProcessorFactory.create_processor(processor_type)
    return processor.process_payment(amount)

Trong ví dụ này, ta định nghĩa một class PaymentProcessor, đại diện cho một payment processor, cũng như 2 class con kế thừa CreditCardProcessor và PaypalProcessor. Sau đó ta định nghĩa một class PaymentProcessorFactory sử dụng Factory pattern để tạo instance của CreditCardProcessor và PaypalProcessor một cách linh hoạt dựa trên user input.

Trong ứng dụng FastAPI, ta định nghĩa một route handler để xử lý payment nhận processor_type và amount làm tham số. Hàm process_payment sử dụng class PaymentProcessorFactory để tạo một instance của phương thức thanh toán phù hợp dựa trên processor_type, sau đó gọi tới method process_payment trên instance đã tạo để xử lý payment.

Bằng cách sử dụng Factory pattern theo cách này, ta có thể linh hoạt tạo ra các object dựa trên input mà không cần chỉ định tên class, cách này có thể khiến code của bạn trở trên tách biệt hơn giữa cáca module và dễ dàng hơn trong việc bảo trì về lau dài.

3. Observe

Observe pattern là một pattern định nghĩa quan hệ một-nhiều giữa các object class sao cho mỗi khi một object thay đổi trạng thái thì tất cả nhữg object còn lại cũng được thông báo vào thay đổi theo. Pattern này thường được sử dụng trong trường hợp bạn có một tập các object cần được cập nhật mỗi khi một trong số đó thay đổi, ví dụ nhưng trong một kiến trúc model-view-controller MVC

class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update()

class Observer:
    def update(self):
        pass

Để triển khai Observe, bạn có thể định nghĩa một class chứa list tất cả các object phụ thuộc, kèm theo các method cho phép bạn thêm vào, xóa đi các object. Class này cũng cung cấp 1 method để thông báo và cập nhật các object khi nhận thấy một thay đổi, đồng thời, class object phụ thuộc cũng có một method cho phép nhận lệnh update từ class lớn.

Đây là một ví dụ về triển khai Observe pattern trong FastAPI

from fastapi import FastAPI
from typing import List

class Subject:
    def __init__(self):
        self.observers = []

    def register_observer(self, observer):
        self.observers.append(observer)

    def remove_observer(self, observer):
        self.observers.remove(observer)

    def notify_observers(self, message):
        for observer in self.observers:
            observer.update(message)

class Resource(Subject):
    def __init__(self):
        super().__init__()
        self.data = []

    def add_data(self, new_data):
        self.data.append(new_data)
        self.notify_observers(new_data)

class Subscriber:
    def __init__(self, name):
        self.name = name

    def update(self, message):
        print(f"{self.name} received message: {message}")

app = FastAPI()
resource = Resource()

@app.post("/add_data/{new_data}")
def add_data(new_data: str):
    resource.add_data(new_data)
    return {"message": "Data added."}

@app.post("/subscribe/{subscriber_name}")
def subscribe(subscriber_name: str):
    subscriber = Subscriber(subscriber_name)
    resource.register_observer(subscriber)
    return {"message": f"{subscriber_name} subscribed."}

@app.post("/unsubscribe/{subscriber_name}")
def unsubscribe(subscriber_name: str):
    subscriber = Subscriber(subscriber_name)
    resource.remove_observer(subscriber)
    return {"message": f"{subscriber_name} unsubscribed."}

Trong ví dụ này, ta định nghĩa một class trừu tượng Subject đại diện cho class chủ thể, và đóng vai trò quan sát, cũng như class Resource được extend từ Subject và đại diện cho một object có thể được thay đổi. Ta cũng định nghĩa một class Subscriber đại diện cho một quan sát của các đổi tượng

Ứng dụng FastAPI định nghĩa 3 route: /add_data, /subscribe và /unsubscribe. Khi một POST request được gửi tới /add_data với tham số new_data, hàm add_data sẽ thêm new_data vào Resource và gọi tơií method notify_observers để thông báo tới tất cả subcriber về thay đổi.

4. Strategy

Strategy pattern là pattern cho phép bạn định nghĩa một họ các thuật toán, đóng gói chúng, và làm đúng có thể chuyển qua lại giữa các thuật toán dễ dàng. Phương pháp này thường được dùng khi bạn có một tập các thuật toaná thực hiện các công việc tương tự nhau với cách triển khai khác nhau và bạn muốn dễ dàng đổi qua lại giữa chúng một các dễ dàng tùy điều kiện

class Sort: 
    def sort(self, data): 
        pass

class QuickSort(Sort): 
    def sort(self, data): 
        # Implementation of quicksort algorithm pass

class MergeSort(Sort): 
    def sort(self, data): 
        # Implementation of mergesort algorithm pass

class Client: 
    def init(self, sort_strategy): 
        self.sort_strategy = sort_strategy

    def do_sort(self, data):
        self.sort_strategy.sort(data)

Để triển khai pattern Strategy, bạn có thể định nghĩa một class trừu tượng, trong đó có một method thực hiện công việc, sau đó định nghĩa một tập hợp các class kế thừa từ class đó.

from fastapi import FastAPI
from typing import List

class SortStrategy:
    def sort(self, data: List[int]) -> List[int]:
        pass

class BubbleSort(SortStrategy):
    def sort(self, data: List[int]) -> List[int]:
        # Implementation of bubbleSort algorithm pass

class QuickSort(SortStrategy):
    def sort(self, data: List[int]) -> List[int]:
        # Implementation of quicksort algorithm pass

class Sorter:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy

    def set_strategy(self, strategy: SortStrategy):
        self.strategy = strategy

    def sort(self, data: List[int]) -> List[int]:
        return self.strategy.sort(data)

app = FastAPI()
sorter = Sorter(BubbleSort())

@app.post("/sort/{strategy}")
def sort_data(strategy: str, data: List[int]):
    if strategy == "bubble":
        sorter.set_strategy(BubbleSort())
    elif strategy == "quick":
        sorter.set_strategy(QuickSort())
    else:
        return {"message": "Invalid sorting strategy."}
    sorted_data = sorter.sort(data)
    return {"sorted_data": sorted_data}

Trong ví dụ này, ta định nghĩa một lớp trừu tượng SortStrategy, đại diện cho một phương pháp sắp xếp, đồng thời ta cũng tạo 2 class BubbleSort và QuickSort kế thừa từ SortStrategy.

Khi một POST request được gửi tới /sort kèm theo tên một phương pháp sắp xếp nhất định và một chuối các số nguyên để sắp xếp, hàm sort_data sẽ tạo một object mới với class Sorter với phương pháp sắp xếp tương ứng, sau đó gọi hàm sort từ object Sorter để thực hiện sắp xếp dữ liệu.

Bằng cách sử dụng Strategy pattern, chúng ta có thể đóng gói nhiều thuật toán khác nhau và làm chúng thay thế lẫn nhau tùy vào mục đích sử dụng. Phương pháp này có thể giúp mô đun hóa và bảo trì code được thuận lợi hơn.

5. Decorator

Decorator là một pattern cho phép bạn thêm các hành vi khác nhau vào một object mà không làm ảnh hưởng tới hành vi của các object khác trong cùng một class. Pattern này rất hữu dụng khi bạn có một tập các object thực hiện cùng một chức năng nhưng khác nhau trong hành vi, và bạn muốn thêm hoặc xóa các hành vi khác nhau cho từng object mà không làm ảnh hưởng tới các object khác.

class Component:
    def operation(self):
        pass

class ConcreteComponent(Component):
    def operation(self):
        return "ConcreteComponent"

class Decorator(Component):
    def __init__(self, component):
        self._component = component

    def operation(self):
        return self._component.operation()

class ConcreteDecoratorA(Decorator):
    def operation(self):
        return f"ConcreteDecoratorA({self._component.operation()})"

class ConcreteDecoratorB(Decorator):
    def operation(self):
        return f"ConcreteDecoratorB({self._component.operation()})"

Để triển khai Decorator pattern, bạn có thể định nghĩa một base class chứa các method và thuộc tính của object, sau đó định nghĩa một tập các object con của nó và thêm các hành vi tương ứng vào các object này. Cuối cùng, bạn có thể định nghĩa một tập các decorator class chứa các clas con và thêm hoặc bớt hành vi từ các object đó.

Đây là mọt ví dụ về sử dụng Decorator pattern trong FastAPI

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, world!"}

def add_greeting(original_function):
    def new_function():
        return {"greeting": "Welcome to my FastAPI application!"} | original_function()
    return new_function

@app.get("/decorated")
@add_greeting
def read_decorated():
    return {"data": "This is some data."}

Trong ví dụ này, ta định ghĩa một ưngs dụng FastAPI cơ bản với các route handler cho đường dẫn gốc, trả về một tin nhắn đơn giản.

Sau đó, ta định nghĩa một hàm add_greeting nhận một hàm có sẵn như một tham số và trả về hàm mới được thêm vào các tin nhắn chào hỏi trong output. Hàm mới này sử dụng phép tóan | để gộp các output từ hàm gốc.

Ta cũng định nghĩa một endpoint với tên /decorated với decorator là @add_greeting, decorator này sẽ thêm tin nhắn chào hỏi vào output của hàm read_decorated, và nó sẽ trả về một vài data mẫu.

Khi /decorated nhận được một GET request, hàm add_greeting sẽ được gọi để thêm tin nhắn vào output của hàm read_decorated, và kết quả là output được trả về như trong response.

Bằng cách sử dụng decorator như thế này, ta có thể thêm các chức năng vào một endpoint mà không cần chỉnh sửa hàm gốc hoặc đoạn code gọi nó. Việc này giúp cải thiện tính minh bạch và bảo trì code được dễ dàng hơn theo thời gian.

Tổng kết

Chúng ta đã đi qua 5 loại design pattern mà có thể được áp dụng trong khi xây dựng ứng dụng Fast API, bao gồm Singleton, Factory, Observe, Strategy và Decorator. Bằng cách hiểu và tận dụng nhữung design pattern này, bạn có thể mô-đun hóa code của bạn khiến chúng dễ dàng tái sử dụng và bảo trì.

Hi vọng bài viết này sẽ đem lại một vài kiến thức bổ ích cho các bạn. Thân ái!