การแยกส่วนอินเทอร์เฟซหลักการ: จะทำอย่างไรถ้าส่วนต่อประสานมีการทับซ้อนกันอย่างมีนัยสำคัญ?


9

จากการพัฒนาซอฟต์แวร์หลักการรูปแบบและการปฏิบัติ: Pearson New International Edition :

บางครั้งวิธีที่เรียกใช้โดยกลุ่มลูกค้าที่แตกต่างกันจะทับซ้อนกัน หากการซ้อนทับมีขนาดเล็กดังนั้นอินเทอร์เฟซสำหรับกลุ่มควรจะแยกจากกัน ควรประกาศฟังก์ชันทั่วไปในอินเทอร์เฟซที่ทับซ้อนกันทั้งหมด คลาสเซิร์ฟเวอร์จะสืบทอดฟังก์ชันทั่วไปจากแต่ละอินเตอร์เฟสเหล่านั้น แต่จะใช้งานเพียงครั้งเดียว

ลุงบ๊อบพูดถึงกรณีที่มีการเหลื่อมกันเล็กน้อย

เราควรทำอย่างไรหากมีการทับซ้อนอย่างมีนัยสำคัญ

บอกว่าเรามี

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

เราควรทำอย่างไรหากมีการทับซ้อนกันระหว่างUiInterface1และUiInterface2?


เมื่อฉันเจออินเทอร์เฟซที่ทับซ้อนกันอย่างหนักฉันสร้างอินเทอร์เฟซหลักซึ่งจัดกลุ่มวิธีการทั่วไปแล้วสืบทอดจากคอมมอนทั่วไปนี้เพื่อสร้างความเชี่ยวชาญพิเศษ แต่! หากคุณไม่ต้องการให้ใครใช้อินเทอร์เฟซทั่วไปโดยไม่มีความชำนาญเฉพาะด้านคุณต้องไปทำซ้ำรหัสเพราะถ้าคุณแนะนำอินเทอร์เฟซที่พบบ่อย parrent ผู้คนสามารถใช้มันได้
Andy

คำถามนี้ค่อนข้างคลุมเครือสำหรับฉันใครสามารถตอบได้ด้วยวิธีแก้ปัญหาที่แตกต่างกันขึ้นอยู่กับกรณีและปัญหา ทำไมการทับซ้อนถึงเติบโต?
Arthur Havlicek

คำตอบ:


1

การคัดเลือกนักแสดง

นี่เกือบจะเป็นวิธีการสัมผัสที่สมบูรณ์ของหนังสือที่อ้างถึง แต่วิธีหนึ่งที่จะสอดคล้องกับ ISP ได้ดีกว่าคือการใช้ความคิดในการคัดเลือกนักแสดงในพื้นที่ส่วนกลางของรหัสฐานข้อมูลของคุณโดยใช้วิธีการแบบQueryInterfaceCOM

สิ่งล่อใจมากมายในการออกแบบอินเตอร์เฟสที่ทับซ้อนกันในบริบทของอินเตอร์เฟซที่บริสุทธิ์มักมาจากความปรารถนาที่จะทำให้อินเทอร์เฟซ "แบบพอเพียง" มากกว่าการแสดงความรับผิดชอบที่แม่นยำเหมือนสไนเปอร์

ตัวอย่างเช่นมันอาจดูแปลกที่การออกแบบฟังก์ชั่นไคลเอนต์เช่นนี้:

// 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


11

นี่คือการเรียกการตัดสินที่คุณต้องทำเป็นกรณีไป

ก่อนอื่นจำไว้ว่าหลักการ SOLID เป็นเพียงแค่ ... หลักการ พวกเขาไม่ใช่กฎ พวกเขาไม่ใช่กระสุนเงิน มันเป็นแค่หลักการ ที่ไม่ได้ใช้เวลาห่างจากความสำคัญของคุณควรเสมอยันต่อพวกเขาต่อไป แต่พวกเขาแนะนำระดับความเจ็บปวดที่สองคุณควรทิ้งพวกเขาจนกว่าคุณต้องการ

ลองนึกถึงสาเหตุที่คุณแยกอินเทอร์เฟซของคุณออกมาตั้งแต่แรก แนวคิดของอินเทอร์เฟซคือการพูดว่า "หากรหัสการบริโภคนี้ต้องการชุดวิธีการที่จะนำไปใช้กับคลาสที่กำลังถูกใช้งานฉันต้องกำหนดสัญญาในการดำเนินการ: หากคุณให้วัตถุกับอินเตอร์เฟสนี้ฉันสามารถทำงานได้ กับมัน "

วัตถุประสงค์ของ ISP คือการพูดว่า "ถ้าสัญญาที่ฉันต้องการเป็นเพียงส่วนย่อยของอินเทอร์เฟซที่มีอยู่ฉันไม่ควรบังคับใช้อินเทอร์เฟซที่มีอยู่ในคลาสใด ๆ ในอนาคตที่อาจส่งผ่านไปยังวิธีการของฉัน"

พิจารณารหัสต่อไปนี้:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

ตอนนี้เรามีสถานการณ์ที่ถ้าเราต้องการส่งวัตถุใหม่ไปยัง ConsumeX มันต้องใช้ X () และ Y () เพื่อให้เหมาะสมกับสัญญา

ดังนั้นเราควรเปลี่ยนรหัสตอนนี้เพื่อให้ดูเหมือนกับตัวอย่างต่อไปหรือไม่

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

ISP แนะนำให้เราควรดังนั้นเราควรโน้มตัวไปสู่การตัดสินใจนั้น แต่ถ้าไม่มีบริบทมันก็ยากที่จะแน่ใจ เป็นไปได้ไหมที่เราจะขยาย A และ B? มีแนวโน้มว่าพวกเขาจะขยายอิสระ เป็นไปได้หรือไม่ที่ B จะใช้วิธีการที่ไม่ต้องการ (ถ้าไม่ใช่เราสามารถสร้าง A สืบทอดมาจาก B. )

นี่คือคำพิพากษาที่คุณต้องทำ และหากคุณไม่มีข้อมูลเพียงพอที่จะทำการโทรคุณควรใช้ตัวเลือกที่ง่ายที่สุดซึ่งอาจเป็นรหัสแรก

ทำไม? เพราะมันง่ายที่จะเปลี่ยนความคิดของคุณในภายหลัง เมื่อคุณต้องการคลาสใหม่นั้นเพียงสร้างอินเทอร์เฟซใหม่และปรับใช้ทั้งในคลาสเก่าของคุณ


1
"ก่อนอื่นจำไว้ว่าหลักการของ SOLID นั้นเป็นเพียงแค่ ... หลักการพวกเขาไม่ใช่กฎพวกเขาไม่ใช่กระสุนเงินพวกเขาเป็นเพียงหลักการนั่นคือการไม่เอาไปจากความสำคัญคุณควรเรียนรู้เสมอ แต่สิ่งที่สองคือระดับความเจ็บปวดคุณควรทิ้งมันไว้จนกว่าคุณจะต้องการ ". ควรอยู่ในหน้าแรกของรูปแบบการออกแบบ / หนังสือหลักการ มันควรจะปรากฏขึ้นทุก ๆ 50 หน้าเพื่อเป็นการเตือน
Christian Rodriguez
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.