🎯 เป้าหมาย: ประยุกต์ใช้แนวคิดเชิงวัตถุเพื่อสร้าง API ที่ยืดหยุ่นและดูแลรักษาง่าย
FastAPI ถูกสร้างมาบนพื้นฐานของ Python Type Hints ซึ่งสอดคล้องกับแนวคิด Class และ Object อย่างยิ่ง
Pydantic Models คือ Class ที่ใช้กำหนด "รูปร่าง" ของข้อมูล (Data Shape)
from pydantic import BaseModel class UserBase(BaseModel): username: str email: str # Inheritance in Action class UserCreate(UserBase): password: str
"หนึ่ง Class หรือ Function ควรมีเหตุผลเดียวในการเปลี่ยนแปลง"
"เปิดรับการขยาย (Extension) แต่ปิดกั้นการแก้ไข (Modification)"
ใน FastAPI เรามักใช้ Middleware หรือ Dependency Injection เพื่อเพิ่มความสามารถโดยไม่ต้องแก้โค้ดหลัก
ตัวอย่าง: การเพิ่มระบบ Logging หรือ Authentication สามารถทำได้โดยการ "Inject" เข้าไปใน Path Operation โดยไม่ต้องไปแก้ไส้ในของฟังก์ชันนั้น
# ขยายความสามารถด้วย Dependency def get_current_user(token: str = Depends(oauth2_scheme)): # Logic ตรวจสอบ user ... # ไม่ต้องแก้โค้ด read_items แค่เพิ่ม Depends @app.get("/items") def read_items(user = Depends(get_current_user)): ...
"Subtypes ต้องสามารถแทนที่ Base types ได้โดยไม่ทำให้โปรแกรมพัง"
หากเรามีฟังก์ชันที่รับค่าเป็น `UserBase` เราควรจะสามารถส่ง `UserCreate` (ซึ่งเป็นลูก) เข้าไปทำงานได้โดยไม่มีข้อผิดพลาด
"ไม่ควรบังคับให้ Client พึ่งพา Interface ที่พวกเขาไม่ได้ใช้"
ใน FastAPI เราประยุกต์ใช้โดยการแยก Request Model และ Response Model ออกจากกัน
ผู้ใช้ (Client) เวลาสร้าง User ต้องส่ง Password แต่เวลาดึงข้อมูล (Response) ไม่ควรเห็น Password
class UserBase(BaseModel): username: str class UserCreate(UserBase): password: str # Input มี password class UserResponse(UserBase): id: int # Output มี ID แต่ไม่มี Password class Config: orm_mode = True
FastAPI มีระบบ Dependency Injection (DI) ที่ทรงพลังมาก ทำให้เราทำตามหลักการนี้ได้ง่ายที่สุด
route -> สร้าง connection database เองในฟังก์ชัน
route -> รับ db session ผ่าน Depends(get_db)
การแบ่งเลเยอร์ช่วยให้โค้ดสะอาดและทำตามหลัก SRP
รับ Request / ส่ง Response (FastAPI APIRouter)
ตรรกะทางธุรกิจ การคำนวณ ตรวจสอบเงื่อนไข
คุยกับ Database (CRUD) ผ่าน ORM
Repository ทำหน้าที่เป็นตัวกลาง (Mediator) ระหว่าง Domain Logic และ Data Mapping Layer
ประโยชน์:class UserRepository: def __init__(self, db: Session): self.db = db def get_by_id(self, user_id: int): return self.db.query(User).filter(...).first() def create(self, user: UserCreate): # จัดการ logic การบันทึกลง DB ...
Service Layer คือที่อยู่ของ "Business Logic" ที่แท้จริง
Service จะเรียกใช้ Repository เพื่อเอาข้อมูลมาประมวลผล เช่น:
*Service ไม่ควรรู้เรื่อง HTTP Request/Response (นั่นเป็นหน้าที่ของ Router)
FastAPI มีระบบ DI ที่ทรงพลังมากผ่านคำสั่ง Depends()
# 1. นิยาม Dependency (เช่น การต่อ Database) def get_db(): db = SessionLocal() try: yield db finally: db.close() # 2. Inject เข้าไปใน Route @app.get("/users/") def read_users(db: Session = Depends(get_db)): # FastAPI จะรัน get_db() และส่งผลลัพธ์มาให้ตัวแปร db return repository.get_users(db)
วิธีนี้ทำให้เราสามารถเปลี่ยน implementation ของ get_db ได้ง่ายมาก (เช่น ตอนทำ Test เปลี่ยนไปใช้ Test DB)
ใช้สำหรับสิ่งที่ควรมีแค่อันเดียวในระบบ เช่น Configuration หรือ Database Connection Pool
ใน FastAPI เรานิยมใช้ @lru_cache จาก functools เพื่อทำ Singleton ให้กับการโหลด Settings
from functools import lru_cache class Settings(BaseSettings): app_name: str = "My API" db_url: str @lru_cache() def get_settings(): # โหลดไฟล์ config แค่ครั้งเดียว # ครั้งต่อไปจะดึงจาก Cache return Settings()
เพื่อให้ Service ไม่ยึดติดกับ Database ตัวใดตัวหนึ่ง เราควรกำหนด Interface ไว้ก่อน (DIP)
from abc import ABC, abstractmethod # Interface (Contract) class ITaskRepository(ABC): @abstractmethod def get_all(self): pass @abstractmethod def add(self, task): pass # Service จะคุยกับ ITaskRepository แทนที่จะคุยกับ SQLRepository โดยตรง
ข้อดีที่สุดของการออกแบบแบบ OOP/Layered คือ Testability
เราสามารถสร้าง MockRepository ที่สืบทอดมาจาก ITaskRepository เพื่อใช้ทดสอบ Service ได้โดยไม่ต้องต่อ Database จริง
class MockTaskRepository(ITaskRepository): def __init__(self): self.tasks = [] def add(self, task): self.tasks.append(task) return task # Test Service โดยใช้ Mock def test_create_task(): repo = MockTaskRepository() service = TaskService(repo) service.create_task(...)
ใช้สร้าง Object ที่ซับซ้อน หรือเลือก Implementation ตามสถานการณ์
ใน FastAPI เรามักใช้ Factory ภายใน Dependency Injection
def get_repository(repo_type: str = "sql"):if repo_type == "sql":return SqlTaskRepository()elif repo_type == "memory":return MemoryTaskRepository()โค้ดหาง่าย แก้ไขง่าย เพราะแยกส่วนชัดเจน (SRP)
ทดสอบง่ายเพราะทำ Mocking ได้ (DIP, LSP)
เพิ่มฟีเจอร์ใหม่ได้โดยไม่กระทบของเดิม (OCP)
นำ Logic หรือ Repository ไปใช้ซ้ำได้
คำถามก่อนเริ่มลงมือปฏิบัติ?
สร้างโครงสร้างโฟลเดอร์เพื่อรองรับ Clean Architecture
ติดตั้ง Library ที่จำเป็น
uv inituv add fastapi uvicorn
*หมายเหตุ: ใช้ uv ตามที่เรียนในสัปดาห์แรก หรือใช้ pip ก็ได้
แก้ไขไฟล์ app/models.py เพื่อกำหนดโครงสร้างข้อมูลด้วย Pydantic
from pydantic import BaseModel from typing import Optional # Base Model (Shared properties) class TaskBase(BaseModel): title: str description: Optional[str] = None completed: bool = False # Model for Creation (Input) class TaskCreate(TaskBase): pass # Model for Reading (Output - includes ID) class Task(TaskBase): id: int class Config: orm_mode = True
แก้ไขไฟล์ app/repositories.py สร้าง Abstract Base Class (DIP Principle)
from abc import ABC, abstractmethod from typing import List from .models import Task, TaskCreate class ITaskRepository(ABC): @abstractmethod def get_all(self) -> List[Task]: pass @abstractmethod def create(self, task: TaskCreate) -> Task: pass @abstractmethod def get_by_id(self, task_id: int) -> Optional[Task]: pass
เพิ่มโค้ดใน app/repositories.py เพื่อจำลอง Database ด้วย List
class InMemoryTaskRepository(ITaskRepository): def __init__(self): self.tasks = [] self.current_id = 1 def get_all(self) -> List[Task]: return self.tasks def create(self, task_in: TaskCreate) -> Task: task = Task( id=self.current_id, **task_in.dict() ) self.tasks.append(task) self.current_id += 1 return task def get_by_id(self, task_id: int) -> Optional[Task]: for task in self.tasks: if task.id == task_id: return task return None
แก้ไข app/services.py (Business Logic)
Service จะรับ Repository เข้ามาผ่าน Constructor (Dependency Injection)
ตรงนี้เราสามารถใส่ Logic เพิ่มเติมได้ เช่น การตรวจสอบว่า Task ซ้ำหรือไม่ หรือการส่งแจ้งเตือน
from .repositories import ITaskRepository from .models import TaskCreate class TaskService: def __init__(self, repo: ITaskRepository): self.repo = repo def get_tasks(self): return self.repo.get_all() def create_task(self, task_in: TaskCreate): # Business logic could go here return self.repo.create(task_in)
แก้ไข app/main.py
from fastapi import FastAPI, Depends from typing import List from .models import Task, TaskCreate from .repositories import InMemoryTaskRepository, ITaskRepository from .services import TaskService app = FastAPI() # Singleton Repository Instance task_repo = InMemoryTaskRepository() # Dependency Provider def get_task_service(): return TaskService(task_repo) @app.get("/tasks", response_model=List[Task]) def read_tasks(service: TaskService = Depends(get_task_service)): return service.get_tasks() @app.post("/tasks", response_model=Task) def create_task( task: TaskCreate, service: TaskService = Depends(get_task_service) ): return service.create_task(task)
uvicorn app.main:app --reload
เปิด Browser ไปที่ http://127.0.0.1:8000/docs
คุณจะเห็น Swagger UI ที่ FastAPI สร้างให้อัตโนมัติ ลอง:
ตอนนี้เราใช้ In-Memory ถ้าปิดแอปข้อมูลจะหาย เราจะเปลี่ยนไปใช้ SQLite โดยไม่แก้ Service Code
สิ่งที่ต้องทำ:
sqlalchemyITaskRepositorymain.py# app/repositories.py (เพิ่มต่อท้าย) from sqlalchemy.orm import Session from . import models_orm # ต้องสร้าง SQLAlchemy Model แยก class SqlTaskRepository(ITaskRepository): def __init__(self, db: Session): self.db = db def get_all(self) -> List[Task]: return self.db.query(models_orm.Task).all() def create(self, task_in: TaskCreate) -> Task: db_task = models_orm.Task(**task_in.dict()) self.db.add(db_task) self.db.commit() self.db.refresh(db_task) return db_task def get_by_id(self, id: int): # ... implementation ... pass
กลับไปที่ main.py และเปลี่ยนแค่ Dependency Provider
# main.py # เดิม: # def get_task_service(): # return TaskService(InMemoryTaskRepository()) # ใหม่ (ใช้ SQL): def get_task_service(db: Session = Depends(get_db)): repo = SqlTaskRepository(db) return TaskService(repo)
นี่คือพลังของ Polymorphism และ Dependency Injection
สร้างไฟล์ tests/test_service.py
from app.services import TaskService from app.repositories import InMemoryTaskRepository from app.models import TaskCreate def test_create_task(): # Arrange repo = InMemoryTaskRepository() service = TaskService(repo) task_data = TaskCreate(title="Test Task") # Act created_task = service.create_task(task_data) # Assert assert created_task.title == "Test Task" assert created_task.id == 1 assert len(repo.get_all()) == 1
ให้เพิ่ม Endpoint PUT /tasks/{id}/complete เพื่อเปลี่ยนสถานะงาน
update ใน ITaskRepositoryInMemoryTaskRepositoryTaskServicemain.pyห้ามสร้าง Task ที่มีชื่อ (Title) ซ้ำกัน
TaskService เท่านั้นget_by_title ใน RepositoryHTTPException จาก Service (หรือสร้าง Custom Exception)คุณได้สร้าง API ที่มีโครงสร้างระดับ Production Grade แล้ว