ข้าม​ไป​ยัง​เนื้อหา

Vertical Slices

สร้าง feature ที่​ตัด​ผ่าน​ทุก​ชั้น แทนที่​จะ​สร้าง​ที​ละ​ชั้น

ระบบ​ซอฟต์แวร์​มัก​ถูก​แบ่ง​ออก​เป็น​ชุด​ของ​ชั้น (layers) โดย​ทั่วไป​มัก​วาด​เป็นกล่อง​แนว​นอน​ซ้อน​กัน แทน logical component ต่าง ๆ ของ​ระบบ เช่น UI, business/service logic และ data access การ​แยก application ออก​เป็น​ชั้น​เช่น​นี้​มี​คุณค่า เพราะ​ให้ Separation of Concerns ช่วย​ลด​ความ​ซับซ้อน​และ​ความ​ซ้ำซ้อน​ภายใน application

Vertical slice คือ​หน่วย​งาน​ที่​ตัด​ผ่าน​ทุก​ชั้น​เหล่า​นั้น​ใน​แนว​ตั้ง เพื่อ​ส่ง​มอบ feature 1 feature ให้​ทำงาน​ได้​ครบ​วงจร ตั้งแต่ UI ลง​ไป​จนถึง​ฐาน​ข้อมูล วิกิพีเดีย​อธิบาย​ว่า vertical slice คือ “the sum of the work that has to be done in every layer that is involved in getting a specific feature working” — ผล​รวม​ของ​งาน​ทุก​ชั้น​ที่​จำเป็น​สำหรับ​ทำให้ feature หนึ่ง ๆ ใช้งาน​ได้​จริง แนวคิด​นี้​ใช้​กัน​มาก​ใน​บริบท Scrum ที่​วางแผนงาน​เป็น feature หรือ user story ไม่ใช่​เป็น​งานที​ละ​ชั้น

สอง​แนวทางใน​การ​สร้าง​ระบบ​แบบ​มี​ชั้น Horizontal vs. vertical

หัวข้อ​ที่​มีชื่อ​ว่า “สอง​แนวทางใน​การ​สร้าง​ระบบ​แบบ​มี​ชั้น Horizontal vs. vertical”

เมื่อ​พัฒนา application แบบ​มี​ชั้น โดย​เฉพาะ​ช่วง​เริ่มต้น project โดย​ทั่วไป​มี​สอง​แนวทาง

แนวทาง​แรก​คือ​สร้าง ชั้น​แนว​นอน (horizontal layers) ที​ละ​ชั้น มัก​เริ่ม​จาก​ชั้น​ล่าง​สุด​เพราะ​แต่ละ​ชั้น​บน​ต้อง​พึ่ง​ชั้น​ล่าง เช่น เริ่ม​จาก​ออกแบบ database schema จาก​นั้น​เขียน​ชุด function เข้าถึง​ข้อมูล แล้ว​เพิ่ม​ชั้น business/service แล้ว​จึง​สร้าง user interface สุดท้าย ใน​แต่ละ​ขั้น​นัก​พัฒนา​อาจ​เลือก​สร้าง library หรือ component ที่ (หวัง​ว่า​จะ) นำ​กลับ​มา​ใช้​ซ้ำ​ได้ (แนว​นอน​ไม่​จำเป็น​ต้อง bottom-up เสมอ​ไป จะ​เริ่ม​จาก UI ลง​ล่าง​ก็ได้ แต่​พบ​น้อย​กว่า​มาก)

อีก​แนวทาง​หนึ่ง​คือ​สร้าง feature ราย​ตัว​ที่​ตัด​ผ่าน​ทุก​ชั้น​เป็น vertical slice เมื่อ​ทำ​ตาม​แนวทาง​นี้ จะ​สร้าง​เฉพาะ function ขั้น​ต่ำ​ที่​จำเป็น​สำหรับ feature หรือ user story นั้น ๆ ใน​แต่ละ​ชั้น เช่น หาก​ข้อ​กำหนด​แรก ๆ คือ​ให้​ผู้​ใช้​สมัคร​และ​ยืนยัน​ตัวตน​ได้ ระบบ​เริ่มต้น​อาจ​ประกอบ​ด้วย​หน้า​จอ​สมัคร​หนึ่ง​หน้า บริการ​ที่​จัดการ​บันทึก​ข้อมูล​ผู้​ใช้ และ​ฐาน​ข้อมูล​ที่​เก็บ​ข้อมูล​นั้น เมื่อ​เพิ่ม feature อื่น ๆ เช่น login และ​การ​ส่ง​อีเมล​ลืม​รหัส​ผ่าน function ที่​จำเป็น​สำหรับ​แต่ละ feature จึง​ถูก​เพิ่ม​เข้าไป​ใน​แต่ละ​ชั้น โดย​จะ refactor เพื่อ​ความ reuse หรือ​ห่อ​หุ้ม​พฤติกรรม​ก็​ต่อ​เมื่อ​พบ​ความ​ซ้ำซ้อน​จริง ๆ (ตาม​หลัก DRY)

ไม่ใช่​ทุก feature จะ​แตะ​ทุก​ชั้น​เสมอ​ไป — วิกิพีเดีย​ยก​ตัวอย่าง​ว่า feature login/logout แบบ web service อาจ​ไม่​ต้อง​แตะ UI เลย แต่​ยัง​ต้อง​แก้ business logic และ​ฐาน​ข้อมูล

ประโยชน์​หลัก: การ​สร้าง​ซอฟต์แวร์​เป็น​ชุด vertical slice ทำให้​ลูกค้า​ได้​ใช้งาน​ซอฟต์แวร์​ที่​ทำงาน​ได้​จริง​เร็ว​ขึ้น​มาก และ​สมมติฐาน​เกี่ยว​กับ​สถาปัตยกรรม​และ infrastructure ถูก​ทดสอบ​เร็ว​ขึ้น​มาก​ด้วย เพราะ​ทุก​ชั้น​ถูก​เชื่อม​ต่อ​กัน​จริง​ตั้งแต่ slice แรก ไม่ใช่​มา​รู้​ปัญหา integration ตอน​ท้าย project

เมื่อ​ทีม​ยึด​แนวทาง vertical slice อย่าง​ต่อ​เนื่อง​จน​กลาย​เป็น​หลักการ​จัด​โครงสร้าง code ถาวร แนวคิด​นี้​จะ​ขยาย​กลาย​เป็น Vertical Slice Architecture ซึ่ง​เป็น​คำ​ที่ Jimmy Bogard บัญญัติ​ขึ้น​หลัง​จาก​ทีม​ของ​เขา​ย้าย​ออก​จาก onion/layered architecture คำ​อธิบาย​ของ Bogard คือ “my architecture is built around distinct requests, encapsulating and grouping all concerns from front-end to back” — จัด code ตาม request หรือ feature แทนที่​จะ​จัด​ตาม​ชั้น​เทคนิค (Controller, Service, Repository) และ​หลักการ​สำคัญ​ที่สุด​ของ​เขา​คือ “Minimize coupling between slices, and maximize coupling in a slice” คือ​ให้ code ภายใน slice เดียวกัน​ผูก​กัน​แน่น​ได้ แต่​ลด coupling ระหว่าง slice ให้​น้อย​ที่สุด เพราะ​ระบบ​ส่วน​ใหญ่​แยก​เป็น command (เปลี่ยน​สถานะ) กับ query (อ่าน​ข้อมูล) ได้​ตาม​ธรรมชาติ วิธี​นี้​จึง​มักได้​ประโยชน์​ของ CQRS มา​โดย​ไม่​ต้อง​ตั้งใจ​นำ​มา​ใช้​โดยตรง

  1. เริ่ม​จาก use case ไม่ใช่​จาก layer — เมื่อ​ได้​รับ requirement หรือ user story ใหม่ ให้​ถาม​ว่า “ต้อง​แตะ​อะไร​บ้าง​เพื่อ​ให้ feature นี้​ใช้งาน​ได้​จริง” แล้ว​สร้าง​เฉพาะ​ส่วน​นั้น
  2. จัด folder ตาม feature (feature folder) แทน​ตาม​ชั้น​เทคนิค เช่น Features/Orders/PlaceOrder/ แทนที่​จะ​กระจาย file ไป​ตาม Controllers/, Services/, Repositories/
  3. อย่า​รีบ abstraction — อย่า​เพิ่ง​สร้าง interface, repository หรือ service กลาง​จนกว่า​จะ​เห็น​ความ​ซ้ำซ้อน​จริง​ระหว่าง slice ตั้งแต่2 slice ขึ้น​ไป (สอดคล้อง​กับ YAGNI)
  4. ยอมรับ​ความ​ซ้ำซ้อน​เล็กน้อย​ระหว่าง slice เพื่อ​แลก​กับ​ความ​เป็น​อิสระ — 2 slice ที่​หน้าตา​คล้าย​กัน​แต่​มี query หรือ validation ต่าง​กัน​เล็กน้อย ไม่​จำเป็น​ต้อง​บังคับ reuse เสมอ​ไป
  5. Refactor เมื่อ​เจอ pattern ซ้ำ​จริง — เมื่อ​พบ​ว่า​หลาย slice ทำ​สิ่ง​เดียวกัน​ซ้ำ ๆ ค่อย​ดึง​ออก​มา​เป็น shared behavior ตาม​หลัก DRY แต่​ต้อง​ระวัง​ไม่​ให้ coupling ระหว่าง slice เพิ่ม​ขึ้น​จน​เสีย​จุด​ประสงค์​เดิม
  6. ทดสอบ end-to-end ต่อ slice — เขียน test ที่​ยืนยัน​ว่า feature นั้น​ทำงาน​ได้​ครบ​วงจร​จริง ไม่ใช่​แค่ unit test แยก​ตาม​ชั้น
flowchart LR
    subgraph horizontal[แนวทางแนวนอน]
        direction TB
        H1[Database schema] --> H2[Data access layer]
        H2 --> H3[Business logic layer]
        H3 --> H4[User interface]
    end
    subgraph vertical[แนวทาง vertical slice]
        direction TB
        S1[Slice A: Register] --- S1a[UI + Logic + Data]
        S2[Slice B: Login] --- S2a[UI + Logic + Data]
        S3[Slice C: Reset Password] --- S3a[UI + Logic + Data]
    end

ฝั่ง​ซ้าย​ต้อง​สร้าง​ครบ​ทุก​ชั้น​ก่อน​จึง​เห็น feature แรก​ทำงาน ฝั่ง​ขวา​แต่ละ slice ส่ง​มอบ​คุณค่า​ให้​ผู้​ใช้ได้​ทันที​ที่​เสร็จ โดย​ไม่​ต้อง​รอ layer อื่น​เสร็จ​ก่อน

โครงสร้าง folder แบบ vertical slice ใน ASP.NET Core มัก​จัด​กลุ่ม file ตาม feature โดย​แต่ละ slice มี request, handler และ endpoint ของ​ตัวเอง แทนที่​จะ​กระจาย​ไป​ตาม Controller/Service/Repository:

Features/Users/RegisterUser/RegisterUserEndpoint.cs
// slice นี้รวมทุกอย่างที่ต้องใช้สำหรับ use case "สมัครสมาชิก" ไว้ในที่เดียว
public record RegisterUserCommand(string Email, string Password) : IRequest<Guid>;
public class RegisterUserHandler : IRequestHandler<RegisterUserCommand, Guid>
{
private readonly AppDbContext _db;
public RegisterUserHandler(AppDbContext db) => _db = db;
public async Task<Guid> Handle(RegisterUserCommand cmd, CancellationToken ct)
{
// งานของทุกชั้น (validation, business rule, persistence)
// อยู่รวมกันใน slice เดียว ไม่กระจายไปหลาย file ทั่ว project
var user = new User(cmd.Email, HashPassword(cmd.Password));
_db.Users.Add(user);
await _db.SaveChangesAsync(ct);
return user.Id;
}
private static string HashPassword(string raw) => /* hashing logic */ raw;
}
// endpoint ผูก HTTP request เข้ากับ command ของ slice นี้โดยตรง
app.MapPost("/users/register", async (RegisterUserCommand cmd, ISender sender) =>
{
var id = await sender.Send(cmd);
return Results.Created($"/users/{id}", id);
});

เมื่อ​มี requirement ใหม่​เช่น “login” หรือ “ลืม​รหัส​ผ่าน” จะ​เพิ่ม slice ใหม่ (Features/Users/LoginUser/, Features/Users/ResetPassword/) แต่ละ​อัน​มี request/handler/endpoint เป็น​ของ​ตัวเอง หาก​ภายหลัง​พบ​ว่า​หลาย slice ต้อง hash password แบบ​เดียวกัน​ซ้ำ ๆ ค่อย​ดึง​ออก​มา​เป็น shared service เดียว — ไม่ใช่​สร้าง​ไว้​ล่วงหน้า​ตั้งแต่ slice แรก

ทีม​ที่​ใช้​แนวทาง​นี้​บ่อย​ครั้ง​ใช้ library อย่าง MediatR เพื่อ route command/query ไป​ยัง handler ของ slice ที่​ถูกต้อง ซึ่ง​ทำให้​ระบบ​ได้​ประโยชน์​ของ CQRS มา​โดย​ธรรมชาติ เพราะ query (GET) และ command (POST/PUT/DELETE) ถูก​แยก​เป็น class ของ​ตัวเอง​อยู่​แล้ว

ประโยชน์

  • ลูกค้า​และ​ผู้​มี​ส่วน​ได้​ส่วน​เสีย​เห็น​ซอฟต์แวร์​ที่​ใช้งาน​ได้​จริง​เร็ว​ขึ้น​มาก แทนที่​จะ​รอ​จน​ทุก​ชั้น​เสร็จ
  • สมมติฐาน​ด้าน​สถาปัตยกรรม​และ infrastructure ถูก​ทดสอบ​ตั้งแต่ slice แรก ลด​ความ​เสี่ยง​ที่​จะ​พบ​ปัญหา integration ใหญ่​ตอน​ท้าย project
  • code ที่​เกี่ยวข้อง​กับ feature เดียวกัน​อยู่​ใกล้​กัน อ่าน​และ​แก้ไข​ง่าย ลด Shotgun Surgery ที่​ต้อง​กระโดด​ไป​แก้​หลาย file กระจัดกระจาย​เมื่อ​เปลี่ยน feature เดียว
  • แต่ละ slice เลือก pattern ที่​เหมาะ​กับ​ความ​ซับซ้อน​ของ​ตัวเอง​ได้ — บาง slice ง่าย​พอ​จะ​เป็น transaction script ตรง ๆ บาง slice ซับซ้อน​พอ​จะ​ใช้ domain model เต็ม​รูปแบบ

ข้อ​ควร​ระวัง

  • ต้อง​อาศัย​วินัย​ของ​ทีม​ใน​การ​รู้จัก​ว่า​เมื่อไหร่​ควร refactor ดึง logic ซ้ำ​ออก​มา และ​เมื่อไหร่​ควร​ปล่อย​ให้​ซ้ำ​ไว้​ก่อน ทีม​ที่​ขาด​ประสบการณ์​อาจ​กลาย​เป็น Copy-Paste Programming โดย​ไม่รู้ตัว
  • หาก​ไม่​ระวัง อาจ​เกิด​ความ​ซ้ำซ้อน​ของ business rule ระหว่าง slice จน​แก้ไข​ไม่​สอดคล้อง​กัน (rule เปลี่ยน​ใน slice หนึ่ง​แต่​ลืม​อีก slice)
  • ไม่​เหมาะ​กับ​ทุก​บริบท — ระบบ​ที่​มี business logic ซับซ้อน​และ​ใช้​ร่วม​กัน​มาก​จริง ๆ (shared domain invariants) อาจ​ได้​ประโยชน์​จาก​การ​รวม​ศูนย์​ผ่าน domain model มากกว่า​การกระจาย​เป็น slice อิสระ
  • ต้อง​แยก​ให้​ออก​ระหว่าง vertical slice ใน​ฐานะ “วิธี​วางแผน​และ​ส่ง​มอบ​งาน” กับ Vertical Slice Architecture ใน​ฐานะ “วิธี​จัด​โครงสร้าง code ถาวร” — ทีม​สามารถ​ทำงาน​แบบ vertical slice ได้​โดย​ไม่​ต้อง​เปลี่ยน​สถาปัตยกรรม​ทั้ง​ระบบ