Pendahuluan — class yang sama, pintu HTTP

Di Seri 3 kamu sudah punya mental model objek. Tier 2 membawa OOP ke dua arah: perangkat di MicroPython (#51), dan sekarang web API. Flask/FastAPI hanya “pintu”; logika bisnis tetap di class — pola yang sama dengan service di Capstone (#49) dan factory ringan di Factory (#50).

Artikel ini sengaja runnable di PC tanpa wajib install framework dulu: stub HTTP + class service. Di bagian porting, kamu lihat bagaimana stub itu dipasang ke Flask/FastAPI. Tujuannya bukan tutorial framework lengkap, melainkan menjaga batas OOP tetap jelas saat request masuk.

Kalau Capstone melatih CLI dan MicroPython melatih node, di sini kamu melatih adapter: HTTP di tepi, domain di tengah. Ganti Flask dengan FastAPI (atau sebaliknya) seharusnya tidak merombak PerpustakaanService.

Prasyarat: MicroPython OOP (#51) atau minimal Composition (#47) + Capstone (#49) · Mengenal OOP (#40). Opsional: Factory (#50).

Kenapa class di web, bukan view panjang?

Pendekatan Gejala Akibat
Semua di fungsi route Validasi + DB + JSON campur Sulit diuji; route jadi “tuhan”
Class service Route tipis; service punya koleksi/aturan Bisa demo() tanpa server; ganti framework lebih aman

Ini composition: aplikasi punya service, service punya item — bukan mewarisi Flask. Inheritance framework mengikat domain ke satu library; composition menjaga service bisa diuji dan diport.

# Anti-pola (jangan): class App(Flask): ...
# Pola OOP: app punya service

class PerpustakaanService:
    def __init__(self):
        self._items = []

    @property
    def jumlah(self):
        return len(self._items)


class AppShell:
    """Pengganti 'app' tipis — composition, bukan inheritance framework."""

    def __init__(self, service):
        self.service = service


app = AppShell(PerpustakaanService())
print("service terpasang, jumlah=", app.service.jumlah)

Output:

service terpasang, jumlah= 0
HTTP tipis + service OOP Request Handler Service Item punya
Handler hanya menerjemahkan HTTP; aturan domain tinggal di PerpustakaanService.

Item + service — domain yang sudah dikenal

Tetap domain perpustakaan agar jembatan ke Capstone jelas (semangat Encapsulation (#43)):

class BukuItem:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def as_dict(self):
        return {"judul": self.judul, "penulis": self.penulis}


class PerpustakaanService:
    def __init__(self):
        self._items = []

    def tambah(self, judul, penulis):
        if not judul.strip():
            raise ValueError("judul wajib")
        item = BukuItem(judul.strip(), penulis.strip() or "Anonim")
        self._items.append(item)
        return item

    def daftar(self):
        return [i.as_dict() for i in self._items]


svc = PerpustakaanService()
svc.tambah("OOP Python", "Kindo")
print(svc.daftar())

Output:

[{'judul': 'OOP Python', 'penulis': 'Kindo'}]

Stub HTTP — uji tanpa Flask terpasang

Framework boleh diganti; kontrak response yang kita kendalikan:

class BukuItem:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def as_dict(self):
        return {"judul": self.judul, "penulis": self.penulis}


class PerpustakaanService:
    def __init__(self):
        self._items = []

    def tambah(self, judul, penulis):
        if not judul.strip():
            raise ValueError("judul wajib")
        item = BukuItem(judul.strip(), penulis.strip() or "Anonim")
        self._items.append(item)
        return item

    def daftar(self):
        return [i.as_dict() for i in self._items]


class HttpResponse:
    def __init__(self, status, body):
        self.status = status
        self.body = body


def handle_list(service):
    return HttpResponse(200, {"items": service.daftar()})


def handle_create(service, judul, penulis):
    try:
        item = service.tambah(judul, penulis)
    except ValueError as exc:
        return HttpResponse(400, {"error": str(exc)})
    return HttpResponse(201, item.as_dict())


svc = PerpustakaanService()
print(handle_create(svc, "Flask Ringkas", "Dewi").status)
print(handle_list(svc).body)
print(handle_create(svc, "  ", "X").status)

Output:

201
{'items': [{'judul': 'Flask Ringkas', 'penulis': 'Dewi'}]}
400

Satu antarmuka handler, banyak “kerangka” di belakang — selaras Polymorphism (#45).

Factory opsional — jenis item tanpa hutan if di route

Kalau tipe mulai bertambah, pakai factory ringan (pola Factory (#50)), tetap di luar Flask. EbookItem memakai pewarisan sederhana (lihat Inheritance (#44)), tapi keputusan “buku atau ebook” tetap di fungsi factory — bukan di decorator route:

class BukuItem:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def as_dict(self):
        return {"jenis": "buku", "judul": self.judul, "penulis": self.penulis}


class EbookItem(BukuItem):
    def as_dict(self):
        d = super().as_dict()
        d["jenis"] = "ebook"
        return d


def buat_item(jenis, judul, penulis):
    if jenis == "buku":
        return BukuItem(judul, penulis)
    if jenis == "ebook":
        return EbookItem(judul, penulis)
    raise ValueError(f"jenis tidak dikenal: {jenis}")


print(buat_item("ebook", "FastAPI Ringkas", "Sari").as_dict())
try:
    buat_item("majalah", "X", "Y")
except ValueError as exc:
    print("error:", exc)

Output:

{'jenis': 'ebook', 'judul': 'FastAPI Ringkas', 'penulis': 'Sari'}
error: jenis tidak dikenal: majalah

Porting singkat ke Flask / FastAPI

Setelah demo() hijau, pasang service yang sama. Blok di bawah sketsa (bukan wajib dijalankan di audit PC):

# Flask (sketsa)
# from flask import Flask, request, jsonify
# app = Flask(__name__)
# svc = PerpustakaanService()
#
# @app.get("/api/buku")
# def list_buku():
#     r = handle_list(svc)
#     return jsonify(r.body), r.status
#
# @app.post("/api/buku")
# def create_buku():
#     data = request.get_json(force=True) or {}
#     r = handle_create(svc, data.get("judul", ""), data.get("penulis", ""))
#     return jsonify(r.body), r.status

# FastAPI (sketsa)
# from fastapi import FastAPI
# from fastapi.responses import JSONResponse
# app = FastAPI()
# svc = PerpustakaanService()
#
# @app.get("/api/buku")
# def list_buku():
#     r = handle_list(svc)
#     return JSONResponse(r.body, status_code=r.status)
#
# # Sketsa: query param biar pendek; body JSON nyata = model Pydantic belakangan
# @app.post("/api/buku")
# def create_buku(judul: str = "", penulis: str = ""):
#     r = handle_create(svc, judul, penulis)
#     return JSONResponse(r.body, status_code=r.status)

Perhatikan: PerpustakaanService tidak mengimpor Flask/FastAPI. Itu titik OOP-nya. Sketsa di atas sengaja tipis — body JSON nyata bisa pakai model Pydantic belakangan, setelah stub hijau.

Pola Dasar — OOP di pintu web

  1. 1
    Model domain dulu BukuItem + as_dict() — bukan dict longgar di route.
  2. 2
    Service dengan composition PerpustakaanService punya koleksi — pola Composition (#47).
  3. 3
    Handler tipis Terjemahkan HTTP <-> service; jangan taruh aturan di decorator saja.
  4. 4
    Uji dengan stub HttpResponse + demo() — tanpa server, tanpa input menggantung.
  5. 5
    Pasang framework Flask atau FastAPI hanya adapter — service tetap sama.

Kode lengkap — perpustakaan_api_oop.py

Simpan dan jalankan: python perpustakaan_api_oop.py.

"""OOP ringan untuk API perpustakaan — stub HTTP (Tier 2 #52).
Pasang ke Flask/FastAPI lewat handler yang sama (lihat sketsa di artikel).
"""

from __future__ import annotations


class BukuItem:
    def __init__(self, judul: str, penulis: str) -> None:
        self.judul = judul
        self.penulis = penulis

    def as_dict(self) -> dict:
        return {"judul": self.judul, "penulis": self.penulis}


class PerpustakaanService:
    def __init__(self) -> None:
        self._items: list[BukuItem] = []

    def tambah(self, judul: str, penulis: str) -> BukuItem:
        if not judul.strip():
            raise ValueError("judul wajib")
        item = BukuItem(judul.strip(), penulis.strip() or "Anonim")
        self._items.append(item)
        return item

    def daftar(self) -> list[dict]:
        return [i.as_dict() for i in self._items]


class HttpResponse:
    def __init__(self, status: int, body) -> None:
        self.status = status
        self.body = body


def handle_list(service: PerpustakaanService) -> HttpResponse:
    return HttpResponse(200, {"items": service.daftar()})


def handle_create(service: PerpustakaanService, judul: str, penulis: str) -> HttpResponse:
    try:
        item = service.tambah(judul, penulis)
    except ValueError as exc:
        return HttpResponse(400, {"error": str(exc)})
    return HttpResponse(201, item.as_dict())


def demo() -> None:
    svc = PerpustakaanService()
    ok = handle_create(svc, "OOP Python", "Kindo")
    bad = handle_create(svc, "   ", "X")
    listed = handle_list(svc)
    print(ok.status, ok.body)
    print(bad.status, bad.body)
    print(listed.status, listed.body)


if __name__ == "__main__":
    demo()

Output yang diharapkan:

201 {'judul': 'OOP Python', 'penulis': 'Kindo'}
400 {'error': 'judul wajib'}
200 {'items': [{'judul': 'OOP Python', 'penulis': 'Kindo'}]}

Kesalahan umum

Gejala Penyebab tipikal Perbaikan
Route 200 baris Logika domain di view Pindahkan ke PerpustakaanService
Sulit unit-test Import Flask di class domain Service murni; framework hanya di adapter
Validasi hilang Percaya JSON mentah ValueError di service -> status 400 di handler
Warisi Flask / FastAPI Salah inheritance Composition: app punya service
Over-engineer Pydantic dulu Terburu schema penuh Class biasa + stub dulu; schema belakangan
Status selalu 200 di FastAPI Return dict mentah tanpa status_code Pakai JSONResponse(..., status_code=r.status) seperti sketsa

Latihan singkat

  1. Tambah handle_cari(service, kata) yang filter judul (case-insensitive).
  2. Ganti pembuatan item lewat fungsi factory sederhana (pola Factory (#50)) untuk tipe ebook/buku.
  3. Sketsa: satu endpoint yang memanggil service Node dari MicroPython (#51) (mis. status suhu) — tanpa wajib wire MQTT.

FAQ singkat

Harus Flask atau FastAPI?
Keduanya OK. FastAPI nyaman untuk type hint & docs; Flask ringan untuk belajar adapter. OOP-nya sama: service di tengah.

Kenapa tidak class App(Flask)?
Karena domain ikut terikat ke framework. Composition (AppShell / app punya service) membuat unit-test dan ganti pintu HTTP jauh lebih murah — semangat yang sama dengan Composition (#47).

Apakah ini pengganti Capstone?
Bukan. Capstone CLI tetap fondasi. Artikel ini hanya membuka pintu HTTP dengan class yang sama.

Apakah perlu instal package untuk latihan?
Tidak untuk stub. Install Flask/FastAPI saat kamu siap menjalankan sketsa porting.

Type hint di kode lengkap wajib?
Tidak. Type hint membantu FastAPI/IDE; di stub PC cukup untuk kejelasan. Jangan biarkan anotasi menunda demo() hijau.

Dari jalur IoT, mulai di mana?
Kalau kamu baru dari perangkat, selesaikan dulu MicroPython (#51) (pola service punya sensor). Pola yang sama dipakai di capstone greenhouse (#39) — di sini pintu HTTP-nya.

Lanjut ke mana?
Ide berikutnya: seri Web Dev penuh (routing, auth, DB) — belum live sebagai slug hardlink di Kindo. Fondasi OOP-mu sudah siap.

Kesimpulan & langkah berikutnya

Web tidak membatalkan OOP — ia meminta batas yang lebih jelas: domain di class, HTTP di adapter. Stub dulu, framework belakangan. Kalau route sudah “tuhan”, pecah lagi: item, service, handler, baru pintu Flask/FastAPI.

Artikel ini adalah #52 (ini) — pintu web setelah MicroPython (#51) dan Factory (#50). Tier 2 menutup jembatan perangkat + web; seri Web Dev penuh menunggu sebagai backlog terpisah.

Tier 2 progress: langkah #52 (ini) · MicroPython (#51) LIVE · Factory (#50) LIVE · Seri 3 tetap 10/10. Prasyarat: #51 · Capstone (#49) · Composition (#47) · Factory (#50) · OOP (#40).