Interface Segregation Principle
อย่าบังคับให้ผู้ใช้พึ่งพา method ที่ไม่ได้ใช้
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Interface Segregation Principle (ISP) คือหลักการตัวที่สาม-สี่ในกลุ่ม SOLID ที่ Robert C. Martin (“Uncle Bob”) ตั้งชื่อและให้นิยามไว้ว่า
“Clients should not be forced to depend upon interfaces that they do not use.”
กล่าวคือ ผู้ใช้ (client) ไม่ควรถูกบังคับให้พึ่งพา method ที่พวกเขาไม่ได้ใช้ interface ควรออกแบบมาเพื่อผู้ใช้ ไม่ใช่เพื่อความสะดวกของ library หรือลำดับชั้น class นักพัฒนาควรเลือกใช้ interface ที่บางและมุ่งเฉพาะ (thin, focused) มากกว่า interface แบบ “อ้วน” (fat) ที่รวม function หลากหลายเกินกว่าที่ client หนึ่ง ๆ ต้องการ
Martin คิดหลักการนี้ขึ้นระหว่างเป็นที่ปรึกษาให้ Xerox ในโครงการซอฟต์แวร์ควบคุมเครื่องพิมพ์รุ่นใหม่ ระบบเดิมมี class Job ตัวเดียวที่ทุก feature (พิมพ์ เย็บเล่ม สแกน ฯลฯ) ต้องพึ่งพา ทำให้การแก้ code เล็ก ๆ จุดหนึ่งกระเทือนไปทั้งระบบ และการ build/deploy แต่ละครั้งกินเวลาเป็นชั่วโมง ทางแก้คือแทรก interface บาง ๆ เฉพาะบทบาท (เช่น PrintJobInterface, StapleJobInterface) คั่นระหว่าง client กับ class Job ที่ implement ทุก interface เหล่านั้น — client แต่ละตัวจะเห็นแค่ interface ที่ตัวเองต้องใช้ interface ลักษณะนี้บางครั้งเรียกว่า role interface เพราะสะท้อนบทบาทของผู้ใช้แต่ละกลุ่ม ไม่ใช่โครงสร้างภายในของ implementation
ในหลายภาษา เช่น C# interface สามารถสืบทอดจาก interface อื่นได้หลายตัว ดังนั้นหากบางส่วนของ application ต้องการ interface ที่ใหญ่กว่า แต่ส่วนอื่นไม่ต้องการ ก็สามารถประกอบ interface ใหญ่ขึ้นจาก interface เล็กหลายตัวได้ วิธีนี้ยังเหมาะเมื่อกำลัง refactor codebase เก่าที่มี interface ขนาดใหญ่ซึ่งแตกออกไม่ได้ทันที
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”1. ลดการ recompile/redeploy ที่ไม่จำเป็น — เมื่อ interface อ้วนถูกแก้ (เพิ่ม method ใหม่) ทุก class ที่ implement มันต้องถูกแตะต้อง แม้จะไม่ได้ใช้ method ใหม่นั้นเลย นี่คือปัญหาต้นตอที่ Martin เจอที่ Xerox — small change แต่ ripple effect กว้าง
2. ลดความเสี่ยงละเมิด Liskov Substitution Principle — interface ที่เล็กลง implement ได้ครบถ้วนง่ายกว่า จึงมีโอกาสน้อยที่จะเกิดการ implement เพียงบางส่วนแล้ว throw NotSupportedException ทิ้ง method ที่ไม่รองรับไว้ ซึ่งเป็นสัญญาณคลาสสิกของการละเมิด LSP
3. เพิ่มความยืดหยุ่นของ implementation — เพราะแต่ละส่วนของ interface ใหญ่สามารถ implement ได้ต่างวิธีกัน ลองพิจารณา Repository pattern ซึ่งมักมีทั้ง method สำหรับอ่านและเขียน รูปแบบเพิ่มประสิทธิภาพที่พบบ่อยสำหรับการอ่านคือเพิ่มชั้น caching แต่นั่นสมเหตุสมผลเฉพาะฝั่งอ่านเท่านั้น ในทำนองเดียวกัน scalability มักดีขึ้นได้ด้วยการเข้าคิวคำสั่งเขียนแทนการทำทันที แต่คงไม่เข้าคิว query การมี IRepository ที่ประกอบจาก IReadOnlyRepository และ IWriteRepository จึงเปิดทางให้มี implementation พื้นฐานที่คุยกับ data store ตรง ๆ และ implementation แยกที่ใช้ caching หรือ queuing ได้โดยไม่กระทบกัน
4. ทำให้ testing และ mocking ง่ายขึ้น — mock หรือ stub ของ interface บางที่มี method ไม่กี่ตัวเขียนและอ่านง่ายกว่า mock ของ interface ที่มี 20 method ซึ่ง test หนึ่ง ๆ ใช้แค่ 2 ตัว
5. ขยายไปถึงระบบ distributed ด้วย — แนวคิดเดียวกันนี้ปรากฏใน high-cohesion principle ของ GRASP และเป็นหนึ่งในหลักการ IDEALS สำหรับการออกแบบ microservice คือ service แต่ละตัวควรเปิด API เฉพาะที่ผู้บริโภคแต่ละกลุ่มต้องใช้จริง ไม่ใช่ endpoint รวมมิตรที่ผู้บริโภคส่วนใหญ่ไม่แตะ
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”interface แบบ “อ้วน” ที่มีแนวโน้มโตเกินควบคุม
public interface IMembership{ bool Login(string username, string password); void Logout(string username); Guid Register(string username, string password, string email); void ForgotPassword(string username);}ไม่ยากเลยที่จะจินตนาการว่า interface เช่นนี้จะโตจนควบคุมไม่ได้และมี function มากกว่าที่ class ใด class หนึ่งจะต้องการ เช่น ฟอร์ม login หน้าเว็บต้องการแค่ Login/Logout แต่ถ้าพึ่งพา IMembership ตรง ๆ มันก็ต้องรู้จัก Register และ ForgotPassword ไปด้วยทั้งที่ไม่เคยเรียกใช้ — และถ้าใครมาเพิ่ม method ResetPasswordWithSecurityQuestion เข้าไปใน IMembership ฟอร์ม login ก็ต้อง recompile ไปด้วยทั้งที่ไม่เกี่ยวข้องเลย
แยก interface เฉพาะสำหรับ login แล้วให้ตัวเดิมสืบทอด
public interface ILogin{ bool Login(string username, string password); void Logout(string username);}
public interface IMembership : ILogin{ Guid Register(string username, string password, string email); void ForgotPassword(string username);}ตอนนี้ฟอร์ม login (client) พึ่งพาแค่ ILogin เท่านั้น — ไม่รู้จักและไม่สนใจว่ามี Register หรือ ForgotPassword อยู่ด้วยหรือไม่
ขยายต่อไปได้อีกจนจบลงที่ interface “อ้วน” ตัวเดิมซึ่งเหลือไว้เพียงด้วยเหตุผลด้าน legacy และประกอบขึ้นจาก interface บางล้วน ๆ:
ประกอบ interface อ้วนขึ้นจาก interface ย่อยล้วน ๆ
public interface ILogin{ bool Login(string username, string password); void Logout(string username);}
public interface IRegister{ Guid Register(string username, string password, string email);}
public interface IForgotPassword{ void ForgotPassword(string username);}
// ยังคงมี IMembership ไว้เพื่อ backward compatibility// แต่ client ใหม่ ๆ ควรพึ่งพา interface ย่อยโดยตรงpublic interface IMembership : ILogin, IRegister, IForgotPassword{}ตัวอย่างที่สอง — เครื่องพิมพ์อเนกประสงค์ ใกล้เคียงกับกรณีดั้งเดิมของ Martin ที่ Xerox
// ก่อนแก้: interface อ้วนตัวเดียวบังคับให้ทุกเครื่องพิมพ์ implement ทุกความสามารถpublic interface IMultiFunctionDevice{ void Print(Document doc); void Scan(Document doc); void Fax(Document doc); void Staple(Document doc);}
// เครื่องพิมพ์ธรรมดาที่พิมพ์ได้อย่างเดียว ถูกบังคับให้ throw ทิ้ง method ที่ทำไม่ได้public class BasicPrinter : IMultiFunctionDevice{ public void Print(Document doc) { /* พิมพ์เอกสาร */ } public void Scan(Document doc) => throw new NotSupportedException(); public void Fax(Document doc) => throw new NotSupportedException(); public void Staple(Document doc) => throw new NotSupportedException();}// หลังแก้: แยกบทบาทเป็น interface บาง ๆ ตามความสามารถจริงpublic interface IPrinter{ void Print(Document doc);}
public interface IScanner{ void Scan(Document doc);}
public interface IFax{ void Fax(Document doc);}
// เครื่องพิมพ์ธรรมดา implement เฉพาะ interface ที่ตรงกับความสามารถจริงpublic class BasicPrinter : IPrinter{ public void Print(Document doc) { /* พิมพ์เอกสาร */ }}
// เครื่องอเนกประสงค์ implement หลาย interface ตามที่มันทำได้จริงpublic class MultiFunctionPrinter : IPrinter, IScanner, IFax{ public void Print(Document doc) { /* พิมพ์เอกสาร */ } public void Scan(Document doc) { /* สแกนเอกสาร */ } public void Fax(Document doc) { /* ส่งแฟกซ์ */ }}client ที่ต้องการแค่พิมพ์ ก็พึ่งพา IPrinter เท่านั้น ไม่ต้อง mock Scan/Fax/Staple ใน unit test และไม่ต้องกังวลว่า BasicPrinter จะ throw NotSupportedException เพราะมันไม่เคย implement method ที่ทำไม่ได้ตั้งแต่แรก
แผนภาพด้านล่างแสดงทิศทางการพึ่งพา: client แต่ละกลุ่มพึ่งพาเฉพาะ interface บทบาทของตัวเอง ส่วน class ที่ทำได้หลายอย่างจึงค่อย implement หลาย interface
flowchart LR
LoginClient["ฟอร์ม login"] --> ILogin
RegisterClient["หน้าสมัครสมาชิก"] --> IRegister
ForgotClient["หน้าลืมรหัสผ่าน"] --> IForgotPassword
ILogin -.-> Membership["MembershipService"]
IRegister -.-> Membership
IForgotPassword -.-> Membership
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- method ที่ throw
NotImplementedExceptionหรือNotSupportedException— สัญญาณชัดเจนที่สุดว่า class ถูกบังคับให้ implement สิ่งที่ไม่เกี่ยวข้องกับมัน - interface มี method จำนวนมากที่ client ส่วนใหญ่ใช้แค่ 1-2 ตัว — บ่งชี้ว่า interface รวมหลายบทบาทเข้าด้วยกันโดยไม่จำเป็น
- การแก้ interface ตัวเดียวทำให้ต้อง recompile/redeploy module ที่ไม่เกี่ยวข้อง — เหมือนปัญหาต้นทางที่ Xerox เจอ
- mock/stub ในเทสต้องเติม method ปลอมจำนวนมากที่ test ไม่ได้ใช้เลย — เพียงเพื่อให้ compile ผ่านตาม interface
- ชื่อ interface กว้างและกำกวม เช่น
IManager,IService,IHandlerที่รวมทุกอย่างเกี่ยวกับ entity หนึ่งไว้ในที่เดียว แทนที่จะแยกตามบทบาทของผู้ใช้แต่ละกลุ่ม - การเพิ่ม feature ใหม่ให้ระบบหนึ่ง ทำให้ทีมอื่นที่ไม่เกี่ยวข้องต้อง implement method เพิ่มใน class ของตัวเอง เพียงเพราะ implement interface อ้วนร่วมกันอยู่
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Liskov Substitution Principle
- SOLID
- Single Responsibility Principle
- Dependency Inversion Principle
- Repository pattern
- Law of Demeter
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/principles/interface-segregation
- Interface segregation principle — Wikipedia
- The Interface Segregation Principle (ISP) — Robert C. Martin, Object Mentor
- SOLID Design Principles Explained: The First Five Principles of Object-Oriented Design — DigitalOcean
- SOLID Class Design: The Interface Segregation Principle — Tom Dalling