สถานการณ์ปัจจุบัน
การตั้งค่าปัจจุบันละเมิดหลักการแยกส่วนต่อประสาน (I ใน SOLID)
การอ้างอิง
ตามที่วิกิพีเดียหลักการอินเตอร์เฟซที่แยกจากกัน (ISP) กล่าวว่าลูกค้าที่ไม่ควรถูกบังคับให้ขึ้นอยู่กับวิธีการก็ไม่ได้ใช้ หลักการแยกส่วนต่อประสานถูกกำหนดขึ้นโดย Robert Martin ในช่วงกลางทศวรรษ 1990
กล่าวอีกนัยหนึ่งถ้านี่เป็นอินเทอร์เฟซของคุณ:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
ดังนั้นทุกคลาสที่ใช้อินเทอร์เฟซนี้จะต้องใช้ทุกวิธีที่ระบุไว้ของอินเตอร์เฟส ไม่มีข้อยกเว้น.
ลองนึกภาพถ้ามีวิธีการแบบทั่วไป:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
หากคุณต้องทำให้จริงเพื่อให้มีเพียงคลาสการใช้งานบางส่วนเท่านั้นที่สามารถลบผู้ใช้ได้ดังนั้นวิธีการนี้จะทำให้ใบหน้าของคุณระเบิด (หรือไม่ต้องทำอะไรเลย) นั่นไม่ใช่การออกแบบที่ดี
โซลูชันที่คุณเสนอ
ฉันได้เห็นวิธีแก้ปัญหาที่ IUserInterface มีวิธี ImplementActions ที่คืนค่าจำนวนเต็มซึ่งเป็นผลมาจากค่าบิต OR ของการกระทำ bitwise ANDed ด้วยการกระทำที่ร้องขอ
สิ่งที่คุณต้องการทำคือ:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
ฉันเพิกเฉยต่อวิธีที่เราพิจารณาว่าคลาสที่กำหนดนั้นสามารถลบผู้ใช้ได้หรือไม่ ไม่ว่าจะเป็นบูลีนแฟล็กบิต ... ไม่สำคัญ ทุกอย่างเดือดลงไปที่คำตอบแบบไบนารี: มันสามารถลบผู้ใช้ใช่หรือไม่?
นั่นจะช่วยแก้ปัญหาใช่มั้ย ในทางเทคนิคแล้วมันก็เป็นเช่นนั้น แต่ตอนนี้คุณกำลังละเมิดหลักการทดแทน Liskov (L ใน SOLID)
ละทิ้งความซับซ้อนค่อนข้างคำอธิบายที่วิกิพีเดียผมพบว่าเป็นตัวอย่างที่ดีใน StackOverflow สังเกตตัวอย่าง "เลว":
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
ฉันถือว่าคุณเห็นความคล้ายคลึงกันที่นี่ มันเป็นวิธีการที่ควรจะจัดการใจลอยวัตถุ ( IDuck, IUserBackend) แต่เป็นเพราะการออกแบบระดับที่ถูกบุกรุกก็จะถูกบังคับให้จับครั้งแรก โดยเฉพาะการใช้งาน ( ElectricDuckตรวจสอบให้แน่ใจว่ามันไม่ได้เป็นIUserBackendระดับที่ผู้ใช้ไม่สามารถลบ)
สิ่งนี้เอาชนะวัตถุประสงค์ของการพัฒนาแนวทางที่เป็นนามธรรม
หมายเหตุ: ตัวอย่างที่นี่แก้ไขได้ง่ายกว่าเคสของคุณ ตัวอย่างก็พอเพียงที่จะมีElectricDuckการเปิดตัวเองในภายในSwim()วิธี เป็ดทั้งสองยังสามารถว่ายน้ำได้ดังนั้นผลการทำงานก็เหมือนกัน
คุณอาจต้องการทำสิ่งที่คล้ายกัน อย่า คุณไม่สามารถหลอกว่าจะลบผู้ใช้ แต่ในความเป็นจริงมีเนื้อเรื่องวิธีว่างเปล่า แม้ว่ามันจะทำงานได้จากมุมมองทางเทคนิค แต่ก็เป็นไปไม่ได้ที่จะรู้ว่าชั้นเรียนที่ใช้งานจริงของคุณจะทำอะไรบางอย่างเมื่อถูกขอให้ทำอะไรสักอย่าง นั่นเป็นแหล่งผสมพันธุ์สำหรับรหัสที่ไม่มีวันลืมได้
ทางออกที่ฉันเสนอ
แต่คุณบอกว่าเป็นไปได้ (และถูกต้อง) สำหรับคลาสที่ใช้งานเพื่อจัดการกับวิธีการเหล่านี้บางอย่างเท่านั้น
เพื่อประโยชน์ของตัวอย่างสมมติว่าสำหรับทุกชุดที่เป็นไปได้ของวิธีการเหล่านี้มีชั้นเรียนที่จะใช้มัน ครอบคลุมฐานทั้งหมดของเรา
วิธีการแก้ปัญหาที่นี่คือการแยกอินเตอร์เฟซ
public interface IGetUserService
{
User getUser(int uid);
}
public interface ICreateUserService
{
User createUser(int uid);
}
public interface IDeleteUserService
{
void deleteUser(int uid);
}
public interface ISetPasswordService
{
void setPassword(int uid, string password);
}
โปรดทราบว่าคุณอาจเห็นการเริ่มต้นของคำตอบของฉัน อินเตอร์เฟสแยกหลักการชื่อแล้วเผยให้เห็นว่าหลักการนี้ถูกออกแบบมาเพื่อทำให้คุณแยกการเชื่อมต่อในระดับที่เพียงพอ
สิ่งนี้ช่วยให้คุณสามารถผสมผสานอินเตอร์เฟสและจับคู่ตามที่คุณต้องการ:
public class UserRetrievalService
: IGetUserService, ICreateUserService
{
//getUser and createUser methods implemented here
}
public class UserDeleteService
: IDeleteUserService
{
//deleteUser method implemented here
}
public class DoesEverythingService
: IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
//All methods implemented here
}
ทุกคลาสสามารถตัดสินใจได้ว่าพวกเขาต้องการทำอะไรโดยไม่ทำลายสัญญาของอินเทอร์เฟซ
นอกจากนี้ยังหมายความว่าเราไม่จำเป็นต้องตรวจสอบว่าคลาสที่แน่นอนสามารถลบผู้ใช้ได้หรือไม่ คลาสที่ใช้ทุกIDeleteUserServiceอินเตอร์เฟซจะสามารถที่จะลบผู้ใช้ = ไม่มีการละเมิดของ Liskov ชดเชยหลักการ
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
หากใครพยายามส่งวัตถุที่ไม่ได้ใช้IDeleteUserServiceโปรแกรมจะปฏิเสธที่จะรวบรวม นี่คือเหตุผลที่เราชอบความปลอดภัยประเภท
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
เชิงอรรถ
ฉันนำตัวอย่างไปสุดขั้วโดยแยกอินเทอร์เฟซเป็นชิ้นเล็กที่สุด อย่างไรก็ตามหากสถานการณ์ของคุณแตกต่างคุณสามารถหนีไปกับกลุ่มก้อนที่ใหญ่กว่าได้
ตัวอย่างเช่นหากทุกบริการที่สามารถสร้างผู้ใช้สามารถลบผู้ใช้ (และในทางกลับกัน) คุณสามารถใช้วิธีการเหล่านี้เป็นส่วนหนึ่งของอินเทอร์เฟซเดียว:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
ไม่มีประโยชน์ทางเทคนิคทำเช่นนี้แทนที่จะแยกเป็นชิ้นเล็ก ๆ แต่จะทำให้การพัฒนาง่ายขึ้นเล็กน้อยเพราะต้องใช้การชุบไอน้ำน้อย
IUserBackendไม่ควรมีdeleteUserวิธีการที่ทุกคน ควรเป็นส่วนหนึ่งของIUserDeleteBackend(หรืออะไรก็ตามที่คุณต้องการเรียก) รหัสที่ต้องการลบผู้ใช้จะมีอาร์กิวเมนต์IUserDeleteBackendรหัสที่ไม่ต้องการให้ฟังก์ชันการทำงานใช้IUserBackendและจะไม่มีปัญหาใด ๆ กับวิธีการที่ไม่ได้ใช้งาน