การคัดเลือกนักแสดง
นี่เกือบจะเป็นวิธีการสัมผัสที่สมบูรณ์ของหนังสือที่อ้างถึง แต่วิธีหนึ่งที่จะสอดคล้องกับ ISP ได้ดีกว่าคือการใช้ความคิดในการคัดเลือกนักแสดงในพื้นที่ส่วนกลางของรหัสฐานข้อมูลของคุณโดยใช้วิธีการแบบQueryInterface
COM
สิ่งล่อใจมากมายในการออกแบบอินเตอร์เฟสที่ทับซ้อนกันในบริบทของอินเตอร์เฟซที่บริสุทธิ์มักมาจากความปรารถนาที่จะทำให้อินเทอร์เฟซ "แบบพอเพียง" มากกว่าการแสดงความรับผิดชอบที่แม่นยำเหมือนสไนเปอร์
ตัวอย่างเช่นมันอาจดูแปลกที่การออกแบบฟังก์ชั่นไคลเอนต์เช่นนี้:
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
const Vec2i xy = position->xy();
auto parent = parenting->parent();
if (parent)
{
// If the entity has a parent, return the sum of the
// parent position and the entity's local position.
return xy + abs_position(dynamic_cast<IPosition*>(parent),
dynamic_cast<IParenting*>(parent));
}
return xy;
}
... เช่นเดียวกับที่ค่อนข้างน่าเกลียด / อันตรายเนื่องจากเรากำลังรับผิดชอบในการส่งข้อผิดพลาดไปยังรหัสลูกค้าโดยใช้ส่วนต่อประสานเหล่านี้และ / หรือผ่านวัตถุเดียวกันเป็นอาร์กิวเมนต์หลาย ๆ ครั้งไปยังพารามิเตอร์หลายตัวเดียวกัน ฟังก์ชัน ดังนั้นเรามักจะต้องการออกแบบอินเทอร์เฟซที่เจือจางมากขึ้นซึ่งรวมความกังวลของIParenting
และIPosition
ในที่เดียวเช่นIGuiElement
หรือสิ่งที่ชอบซึ่งจะกลายเป็นความเสี่ยงที่จะทับซ้อนกับความกังวลของอินเทอร์เฟซแบบฉากซึ่งจะถูกล่อลวง เหตุผล "ความพอเพียงของตนเอง"
ความรับผิดชอบในการผสมกับการคัดเลือกนักแสดง
เมื่อออกแบบอินเทอร์เฟซที่มีความรับผิดชอบพิเศษที่กลั่นเป็นเอกเทศสิ่งล่อใจมักจะยอมรับการดาวน์สตรีมหรืออินเทอร์เฟซแบบรวมเพื่อตอบสนองความรับผิดชอบหลายอย่าง (ดังนั้นจึงเหยียบทั้ง ISP และ SRP)
ด้วยการใช้วิธี COM-style (เพียงQueryInterface
บางส่วน) เราเล่นกับวิธีการ downcasting แต่รวมการคัดเลือกเข้ากับศูนย์กลางใน codebase และสามารถทำอะไรแบบนี้ได้มากขึ้น:
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
// `Object::query_interface` returns nullptr if the interface is
// not provided by the entity. `Object` is an abstract base class
// inherited by all entities using this interface query system.
IPosition* position = obj->query_interface<IPosition>();
assert(position && "obj does not implement IPosition!");
const Vec2i xy = position->xy();
IParenting* parenting = obj->query_interface<IParenting>();
if (parenting && parenting->parent()->query_interface<IPosition>())
{
// If the entity implements IParenting and has a parent,
// return the sum of the parent position and the entity's
// local position.
return xy + abs_position(parenting->parent());
}
return xy;
}
... แน่นอนว่าหวังว่าด้วยเครื่องห่อแบบปลอดภัยและทุกอย่างที่คุณสามารถสร้างได้จากส่วนกลางเพื่อให้ได้สิ่งที่ปลอดภัยกว่าตัวชี้แบบดิบ
ด้วยสิ่งนี้สิ่งล่อใจในการออกแบบอินเทอร์เฟซที่ทับซ้อนกันมักจะถูกลดลงจนเหลือน้อยที่สุด ช่วยให้คุณสามารถออกแบบส่วนต่อประสานที่มีความรับผิดชอบเป็นเอกเทศ (บางครั้งเป็นเพียงฟังก์ชันสมาชิกเดียว) ที่คุณสามารถผสมผสานและจับคู่สิ่งที่คุณต้องการโดยไม่ต้องกังวลกับ ISP และรับความยืดหยุ่นในการพิมพ์หลอกเป็ดที่รันไทม์ใน C ++ การแลกเปลี่ยนของการลงโทษรันไทม์เพื่อค้นหาวัตถุเพื่อดูว่าพวกเขาสนับสนุนอินเทอร์เฟซเฉพาะ) ส่วนรันไทม์อาจมีความสำคัญเช่นการตั้งค่าด้วยชุดพัฒนาซอฟต์แวร์ที่ฟังก์ชั่นจะไม่มีข้อมูลเวลาคอมไพล์ของปลั๊กอินล่วงหน้าที่ใช้อินเทอร์เฟซเหล่านี้
แม่แบบ
หากแม่แบบมีความเป็นไปได้ (เรามีข้อมูลเวลารวบรวมที่จำเป็นล่วงหน้าซึ่งไม่สูญหายไปตามเวลาที่เราได้รับวัตถุเช่น) จากนั้นเราก็สามารถทำสิ่งนี้ได้:
// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
const Vec2i xy = obj.xy();
if (obj.parent())
{
// If the entity has a parent, return the sum of the parent
// position and the entity's local position.
return xy + abs_position(obj.parent());
}
return xy;
}
... แน่นอนในกรณีเช่นนี้parent
วิธีนี้จะต้องคืนค่าEntity
ชนิดเดียวกันซึ่งในกรณีนี้เราอาจต้องการหลีกเลี่ยงการเชื่อมต่อแบบทันที
ระบบส่วนประกอบของเอนทิตี
หากคุณเริ่มดำเนินการตามแนวทาง COM เพิ่มเติมจากความยืดหยุ่นหรือจุดยืนด้านประสิทธิภาพคุณมักจะจบลงด้วยระบบส่วนประกอบเอนทิตีคล้ายกับเอ็นจิ้นเกมที่ใช้ในอุตสาหกรรม ณ จุดนี้คุณจะตั้งฉากกับแนวทางเชิงวัตถุจำนวนมากอย่างสมบูรณ์ แต่ ECS อาจใช้กับการออกแบบ GUI (ที่เดียวที่ฉันคิดว่าใช้ ECS นอกโฟกัสแบบฉาก แต่คิดว่ามันสายเกินไปหลังจากนั้น ลองใช้วิธีแบบ COM เพื่อลองดู)
โปรดทราบว่าโซลูชันสไตล์ COM นี้มีความสมบูรณ์ตราบที่ชุดเครื่องมือ GUI ออกแบบไปและ ECS จะยิ่งมากขึ้นดังนั้นจึงไม่ใช่สิ่งที่จะได้รับการสนับสนุนจากทรัพยากรจำนวนมาก แต่แน่นอนว่ามันจะช่วยให้คุณลดการล่อลวงในการออกแบบส่วนต่อประสานที่มีความรับผิดชอบซ้อนทับกันให้เหลือน้อยที่สุดแน่นอนทำให้ไม่ต้องกังวล
แนวทางปฏิบัติ
แน่นอนว่าทางเลือกอื่นคือคลายความปลอดภัยเล็กน้อยหรือออกแบบอินเตอร์เฟสในระดับย่อยแล้วเริ่มสืบทอดมันเพื่อสร้างส่วนต่อประสานที่คุณใช้เช่นIPositionPlusParenting
ที่มาจากทั้งสองIPosition
และIParenting
(หวังว่าด้วยชื่อที่ดีกว่านั้น) ด้วยอินเทอร์เฟซแท้ๆมันไม่ควรละเมิด ISP มากพอ ๆ กับวิธีการแบบลำดับชั้นลึกแบบเสาหินที่ใช้กันทั่วไป (Qt, MFC, ฯลฯ ) ซึ่งเอกสารมักจะรู้สึกว่าจำเป็นต้องซ่อนสมาชิกที่ไม่เกี่ยวข้องเนื่องจากการละเมิด ISP ของการออกแบบ) ดังนั้นวิธีการปฏิบัติอาจเพียงแค่ยอมรับการทับซ้อนบางอย่างที่นี่และที่นั่น แต่วิธีการในรูปแบบ COM นี้จะช่วยหลีกเลี่ยงความจำเป็นในการสร้างอินเทอร์เฟซรวมสำหรับทุกชุดที่คุณเคยใช้ ข้อกังวลเรื่อง "การพึ่งตัวเอง" ได้ถูกขจัดออกไปอย่างสมบูรณ์ในกรณีเช่นนี้และมักจะกำจัดแหล่งที่มาที่สุดของการล่อลวงให้ออกแบบส่วนต่อประสานที่มีความรับผิดชอบที่ทับซ้อนกันซึ่งต้องการต่อสู้กับทั้ง SRP และ ISP