จัดโครงสร้าง API ของคุณเป็นชั้น ๆ ที่มีความรับผิดชอบเดียว แล้วเชื่อมพวกมันเข้าด้วยกันด้วยการฉีดพึ่งพา (injection) แบบเบา เพื่อให้ทดสอบได้
เปิดบทเรียนนี้ใน Kodokonไฟล์เดียวที่ปนกันทั้ง routing, ตรรกะทางธุรกิจ และการเข้าถึงข้อมูล ย่อมมีราคาแพงเสมอในท้ายที่สุด คุณไม่สามารถทดสอบกฎทางธุรกิจได้โดยไม่ต้องเปิดเซิร์ฟเวอร์ขึ้นมา และคุณไม่สามารถสลับโครงสร้างพื้นฐาน (infrastructure) ได้โดยไม่ต้องเขียนใหม่ทั้งหมด การแยกออกเป็นสามชั้นช่วยแก้ทั้งสองปัญหา Routes ประกาศ URL และมอบหมายงานต่อ Controllers แปลง HTTP ให้เป็นการเรียกทางธุรกิจ คือ อ่านคำขอ เลือกรหัสสถานะ (status code) และแปลงคำตอบเป็นข้อมูลอนุกรม (serialize) Services เก็บกฎทางธุรกิจไว้ และไม่รู้อะไรเกี่ยวกับ Express เลย กฎการพึ่งพา (dependency) นั้นเข้มงวด แต่ละชั้นรู้จักเพียงชั้นที่อยู่ถัดลงไปเท่านั้น ไม่มีทางกลับกัน
import { Router } from 'express';
export function createUserRouter(controller) {
const router = Router();
router.get('/', controller.list);
router.post('/', controller.create);
return router;
}controller คือ factory ที่รับ service เข้ามาเป็นอาร์กิวเมนต์ มันไม่ได้เก็บกฎทางธุรกิจใด ๆ หน้าที่ของมันจำกัดอยู่แค่การดึงข้อมูลจาก req เรียก service และแปลงผลลัพธ์ให้เป็นคำตอบ HTTP ข้อผิดพลาดใด ๆ จะถูกส่งต่อไปยัง next เพื่อให้ error middleware ส่วนกลางจัดการ ไม่ใช่การกระจาย res.status(500) ไปทั่วทุก controller
export function createUserController(service) {
return {
async list(req, res, next) {
try {
const users = await service.listUsers();
res.json(users);
} catch (err) {
next(err);
}
},
async create(req, res, next) {
try {
const user = await service.createUser(req.body);
res.status(201).json(user);
} catch (err) {
next(err);
}
},
};
}service รวมศูนย์การตัดสินใจไว้ คือ ความไม่ซ้ำของอีเมล และกฎการสร้าง มันโยนข้อผิดพลาด ทางธุรกิจ ออกมา โดยอย่างมากที่สุดก็เสริมด้วยฟิลด์ status ในขณะที่ไม่รู้อะไรเกี่ยวกับ Express เลย มันเองก็รับการพึ่งพาของมัน คือ repository ของข้อมูล เข้ามาเป็นอาร์กิวเมนต์เช่นกัน นี่คือ การฉีดพึ่งพาแบบเบา (lightweight injection) ฟังก์ชัน factory ธรรมดาที่ถูกเรียกครั้งเดียวตอนเริ่มต้น เข้ามาแทนที่ injection container ที่หนักหน่วง และทำให้ทุกชั้นสลับเปลี่ยนได้ในการทดสอบ
export function createUserService(repository) {
return {
listUsers() {
return repository.findAll();
},
createUser(input) {
const found = repository.findByEmail(input.email);
if (found) {
const err = new Error('Email already used');
err.status = 409;
throw err;
}
return repository.insert(input);
},
};
}ที่เหลือคือ composition root จุดเดียวที่ factory ทั้งหลายประกอบเข้าด้วยกัน createApp รับการพึ่งพาของโครงสร้างพื้นฐาน (ในที่นี้คือฐานข้อมูล ซึ่งจะกล่าวถึงในบทถัดไป) และคืนแอปพลิเคชัน Express ที่สมบูรณ์ ซึ่ง ยังไม่ได้เริ่มฟัง (listening) การแยกการประกอบสร้างออกจากการฟังดูเหมือนเป็นเรื่องเล็กน้อย แต่มันคือสิ่งที่จะทำให้การทดสอบสามารถติดตั้ง API บนพอร์ตชั่วคราวได้ ในขณะที่เซิร์ฟเวอร์ production ยังคงมีเพียงสามบรรทัด คือ สร้างฐานข้อมูล สร้างแอป แล้วฟัง
import express from 'express';
import { createUserRepository } from './db/users.js';
import { createUserService } from './services/users.js';
import {
createUserController,
} from './controllers/users.js';
import { createUserRouter } from './routes/users.js';
export function createApp({ db }) {
const app = express();
app.use(express.json());
const repository = createUserRepository(db);
const service = createUserService(repository);
const controller = createUserController(service);
app.use('/api/users', createUserRouter(controller));
return app;
}createApp จึงไม่เริ่มฟังบนเครือข่ายด้วยตัวมันเอง?