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
จาก practice สู่ architecture
หัวข้อที่มีชื่อว่า “จาก practice สู่ architecture”เมื่อทีมยึดแนวทาง 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 มาโดยไม่ต้องตั้งใจนำมาใช้โดยตรง
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”- เริ่มจาก use case ไม่ใช่จาก layer — เมื่อได้รับ requirement หรือ user story ใหม่ ให้ถามว่า “ต้องแตะอะไรบ้างเพื่อให้ feature นี้ใช้งานได้จริง” แล้วสร้างเฉพาะส่วนนั้น
- จัด folder ตาม feature (feature folder) แทนตามชั้นเทคนิค เช่น
Features/Orders/PlaceOrder/แทนที่จะกระจาย file ไปตามControllers/,Services/,Repositories/ - อย่ารีบ abstraction — อย่าเพิ่งสร้าง interface, repository หรือ service กลางจนกว่าจะเห็นความซ้ำซ้อนจริงระหว่าง slice ตั้งแต่2 slice ขึ้นไป (สอดคล้องกับ YAGNI)
- ยอมรับความซ้ำซ้อนเล็กน้อยระหว่าง slice เพื่อแลกกับความเป็นอิสระ — 2 slice ที่หน้าตาคล้ายกันแต่มี query หรือ validation ต่างกันเล็กน้อย ไม่จำเป็นต้องบังคับ reuse เสมอไป
- Refactor เมื่อเจอ pattern ซ้ำจริง — เมื่อพบว่าหลาย slice ทำสิ่งเดียวกันซ้ำ ๆ ค่อยดึงออกมาเป็น shared behavior ตามหลัก DRY แต่ต้องระวังไม่ให้ coupling ระหว่าง slice เพิ่มขึ้นจนเสียจุดประสงค์เดิม
- ทดสอบ 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:
// 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 ได้โดยไม่ต้องเปลี่ยนสถาปัตยกรรมทั้งระบบ