แผนการเรียน
/

OOP & Design Patterns
ใน FastAPI

สถาปัตยกรรมซอฟต์แวร์สมัยใหม่: SOLID, Layered Architecture และ Dependency Injection

🎯 เป้าหมาย: ประยุกต์ใช้แนวคิดเชิงวัตถุเพื่อสร้าง API ที่ยืดหยุ่นและดูแลรักษาง่าย

ทำไมต้องใช้ OOP ใน FastAPI?

แบบเดิม (Function-Based/Script)

  • เขียนทุกอย่างรวมกันในไฟล์เดียว (Routes + Logic + DB)
  • ยากต่อการทดสอบ (Unit Testing ยากมาก)
  • เมื่อเปลี่ยน Database ต้องแก้โค้ดเกือบทั้งหมด

แบบ OOP & Patterns

  • Encapsulation: รวม Business Logic ไว้ใน Service Classes
  • Abstraction: ซ่อนการทำงานของ Database ผ่าน Repository
  • Dependency Injection: สลับส่วนประกอบได้ง่าย (FastAPI เก่งเรื่องนี้มาก)

รากฐานของ FastAPI: Type System & OOP

FastAPI ถูกสร้างมาบนพื้นฐานของ Python Type Hints ซึ่งสอดคล้องกับแนวคิด Class และ Object อย่างยิ่ง

Pydantic Models คือ Class ที่ใช้กำหนด "รูปร่าง" ของข้อมูล (Data Shape)

  • ตรวจสอบประเภทข้อมูล (Validation)
  • แปลงข้อมูล JSON <-> Python Object (Serialization)
  • ใช้หลักการ Inheritance ได้
from pydantic import BaseModel

class UserBase(BaseModel):
    username: str
    email: str

# Inheritance in Action
class UserCreate(UserBase):
    password: str

S - Single Responsibility Principle

"หนึ่ง Class หรือ Function ควรมีเหตุผลเดียวในการเปลี่ยนแปลง"

❌ สิ่งที่ห้ามทำใน Route Function:
  • ตรวจสอบความถูกต้องของข้อมูล (Validation Logic)
  • คำนวณ Business Logic ซับซ้อน
  • เขียนคำสั่ง SQL หรือคุยกับ Database โดยตรง
✅ สิ่งที่ควรทำ (แยกไฟล์/Class):
  • Schemas (Pydantic): รับผิดชอบ Validation
  • Services: รับผิดชอบ Business Logic
  • Repositories: รับผิดชอบ Database Access
  • Routes: รับผิดชอบแค่รับ Request และส่ง Response

O - Open/Closed Principle

"เปิดรับการขยาย (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)):
    ...

L - Liskov Substitution Principle

"Subtypes ต้องสามารถแทนที่ Base types ได้โดยไม่ทำให้โปรแกรมพัง"

ตัวอย่าง: Pydantic Inheritance

หากเรามีฟังก์ชันที่รับค่าเป็น `UserBase` เราควรจะสามารถส่ง `UserCreate` (ซึ่งเป็นลูก) เข้าไปทำงานได้โดยไม่มีข้อผิดพลาด

def process_user(user: UserBase): ...
process_user(UserCreate(...)) # ✅ ต้องทำงานได้

I - Interface Segregation Principle

"ไม่ควรบังคับให้ 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

D - Dependency Inversion Principle

"High-level module ไม่ควรขึ้นกับ Low-level module"

FastAPI มีระบบ Dependency Injection (DI) ที่ทรงพลังมาก ทำให้เราทำตามหลักการนี้ได้ง่ายที่สุด

แบบผูกมัด (Tight Coupling)

route -> สร้าง connection database เองในฟังก์ชัน

แบบ Inversion (DI)

route -> รับ db session ผ่าน Depends(get_db)

โครงสร้างสถาปัตยกรรม (Layered Architecture)

การแบ่งเลเยอร์ช่วยให้โค้ดสะอาดและทำตามหลัก SRP

1. Presentation Layer (Routers)

รับ Request / ส่ง Response (FastAPI APIRouter)

⬇️ Call

2. Service Layer (Business Logic)

ตรรกะทางธุรกิจ การคำนวณ ตรวจสอบเงื่อนไข

⬇️ Use

3. Data Access Layer (Repositories)

คุยกับ Database (CRUD) ผ่าน ORM

Design Pattern: Repository Pattern

Repository ทำหน้าที่เป็นตัวกลาง (Mediator) ระหว่าง Domain Logic และ Data Mapping Layer

ประโยชน์:
  • ซ่อนความซับซ้อนของ SQL/ORM
  • เปลี่ยน Database ได้ง่าย (เช่น เปลี่ยนจาก SQLite เป็น PostgreSQL หรือ MongoDB)
  • Mock ข้อมูลเพื่อทำ Test ได้ง่าย
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
        ...

Design Pattern: Service Layer

Service Layer คือที่อยู่ของ "Business Logic" ที่แท้จริง

หน้าที่ของ Service

Service จะเรียกใช้ Repository เพื่อเอาข้อมูลมาประมวลผล เช่น:

  • ตรวจสอบว่า User นี้มีสิทธิ์ได้รับส่วนลดหรือไม่?
  • คำนวณราคาสินค้าหลังหักภาษี
  • ส่ง Email หลังจากสมัครสมาชิกสำเร็จ

*Service ไม่ควรรู้เรื่อง HTTP Request/Response (นั่นเป็นหน้าที่ของ Router)

Design Pattern: Dependency Injection (DI)

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)

Design Pattern: Singleton

ใช้สำหรับสิ่งที่ควรมีแค่อันเดียวในระบบ เช่น Configuration หรือ Database Connection Pool

lru_cache Strategy

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

โครงสร้างโปรเจกต์แบบ Production Grade

my_fastapi_app/
├── app/
│ ├── main.py # Entry point
│ ├── api/ # Routes (Controllers)
│ ├── core/ # Config, Security
│ ├── crud/ # Repositories
│ ├── models/ # Database Models (ORM)
│ ├── schemas/ # Pydantic Models (DTOs)
│ └── services/ # Business Logic
├── tests/
└── requirements.txt └── pyproject.toml

การใช้ Abstract Base Class (ABC)

เพื่อให้ 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 โดยตรง

การทดสอบ (Testing)

ข้อดีที่สุดของการออกแบบแบบ 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(...)

Design Pattern: Factory Pattern

ใช้สร้าง 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()

สรุปประโยชน์ของการใช้ OOP ใน FastAPI

Maintainability

โค้ดหาง่าย แก้ไขง่าย เพราะแยกส่วนชัดเจน (SRP)

Testability

ทดสอบง่ายเพราะทำ Mocking ได้ (DIP, LSP)

Scalability

เพิ่มฟีเจอร์ใหม่ได้โดยไม่กระทบของเดิม (OCP)

Reusability

นำ Logic หรือ Repository ไปใช้ซ้ำได้

เตรียมตัวเข้าสู่ Lab Session

สิ่งที่เราจะสร้าง: Task Management API

  • ออกแบบ Pydantic Models (User, Task)
  • สร้าง Abstract Repository
  • Implement In-Memory Repository (สำหรับทดสอบ)
  • เชื่อมต่อ Dependency Injection
  • (Advance) เปลี่ยนไปใช้ SQLAlchemy Repository

จบการบรรยาย

คำถามก่อนเริ่มลงมือปฏิบัติ?

"Theory without practice is empty; practice without theory is blind."

Lab: Task Management API

สร้าง REST API ด้วย FastAPI โดยใช้ Clean Architecture และ OOP Patterns

วัตถุประสงค์

1
ประยุกต์ใช้ Repository Pattern เพื่อแยก Data Layer
2
ใช้งาน Dependency Injection ของ FastAPI
3
เขียนโค้ดตามหลัก SOLID

Step 1: การตั้งค่าโปรเจกต์

สร้างโครงสร้างโฟลเดอร์เพื่อรองรับ Clean Architecture

fastapi-lab/
├── app/
│ ├── __init__.py
│ ├── models.py # Entities
│ ├── repositories.py # Data Access
│ ├── services.py # Business Logic
│ └── main.py # App & Routes
└── requirements.txt └── pyproject.toml

ติดตั้ง Library ที่จำเป็น

uv init
uv add fastapi uvicorn

*หมายเหตุ: ใช้ uv ตามที่เรียนในสัปดาห์แรก หรือใช้ pip ก็ได้

Step 2: สร้าง Models (DTOs)

แก้ไขไฟล์ 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

Step 3: สร้าง Repository Interface

แก้ไขไฟล์ 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

Step 4: สร้าง InMemoryRepository

เพิ่มโค้ดใน 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

Step 5: สร้าง Service Layer

แก้ไข 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)

Step 6: เชื่อมต่อใน Main App

แก้ไข 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)

Step 7: รันและทดสอบ

Command Line

uvicorn app.main:app --reload

เปิด Browser ไปที่ http://127.0.0.1:8000/docs

คุณจะเห็น Swagger UI ที่ FastAPI สร้างให้อัตโนมัติ ลอง:

  1. ใช้ POST /tasks เพื่อสร้าง Task ใหม่
  2. ใช้ GET /tasks เพื่อดูรายการ Task

Step 8: ก้าวสู่ Database จริง (Advanced)

ตอนนี้เราใช้ In-Memory ถ้าปิดแอปข้อมูลจะหาย เราจะเปลี่ยนไปใช้ SQLite โดยไม่แก้ Service Code

สิ่งที่ต้องทำ:

  • ติดตั้ง sqlalchemy
  • สร้าง SQL Repository ใหม่ที่สืบทอดจาก ITaskRepository
  • เปลี่ยน Dependency Injection ใน main.py

SQL Repository Implementation

# 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

Step 9: สลับ Repository (Magic of DIP)

กลับไปที่ 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)
สังเกตว่า Code ส่วน Controller และ Service ไม่ต้องแก้เลย!

นี่คือพลังของ Polymorphism และ Dependency Injection

Step 10: การเขียน Test

สร้างไฟล์ 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

ภารกิจท้าทาย (Challenge 1)

โจทย์: เพิ่มฟีเจอร์ "Mark as Complete"

ให้เพิ่ม Endpoint PUT /tasks/{id}/complete เพื่อเปลี่ยนสถานะงาน

  • เพิ่ม method update ใน ITaskRepository
  • Implement method ใน InMemoryTaskRepository
  • เพิ่ม Logic ใน TaskService
  • สร้าง Route ใหม่ใน main.py

ภารกิจท้าทาย (Challenge 2)

โจทย์: Validation Logic

ห้ามสร้าง Task ที่มีชื่อ (Title) ซ้ำกัน

  • ห้าม เขียน Logic นี้ใน Route
  • ต้องเขียนใน TaskService เท่านั้น
  • อาจจะต้องเพิ่ม method get_by_title ใน Repository
  • ถ้าชื่อซ้ำ ให้ Raise HTTPException จาก Service (หรือสร้าง Custom Exception)

Lab Complete!

คุณได้สร้าง API ที่มีโครงสร้างระดับ Production Grade แล้ว

Key Takeaways

  • Pydantic = Data Class
  • Service/Repository Pattern
  • Dependency Injection is Powerful

Next Steps

  • ลองต่อ PostgreSQL จริง
  • ทำ Authentication (User Service)
  • Deploy ด้วย Docker