Dependency Inversion Principle(ใน C++) คือหลักการออกแบบลำดับที่ห้าและสุดท้ายของซีรีส์ SOLID ซึ่งเป็นหลักการออกแบบ Rock หลักการออกแบบ SOLID มุ่งเน้นไปที่การพัฒนาซอฟต์แวร์ที่ง่ายต่อการบำรุงรักษา ใช้ซ้ำ และขยายได้ ในบทความนี้ เราจะเห็นโค้ดตัวอย่างพร้อมโฟลว์และแก้ไขด้วยความช่วยเหลือของ DIP เราจะดูแนวทางและประโยชน์ของกรมทรัพย์สินทางปัญญาในตอนท้ายของบทความด้วย

/!\: เผยแพร่ครั้งแรก @ www.vishalchovatiya.com.

อย่างไรก็ตาม หากคุณยังไม่ได้อ่านบทความก่อนหน้าของฉันเกี่ยวกับหลักการออกแบบ ลิงก์ด่วนด้านล่างนี้คือ:

  1. SRP — หลักการความรับผิดชอบเดียว
  2. OCP — หลักการเปิด/ปิด
  3. LSP — หลักการทดแทน Liskov
  4. ฉันSP — หลักการแยกส่วนต่อประสาน
  5. DIP — หลักการผกผันการพึ่งพา

ข้อมูลโค้ดที่คุณเห็นในบทความชุดนี้เป็นแบบเรียบง่ายไม่ซับซ้อน คุณมักจะเห็นฉันไม่ได้ใช้คำหลักเช่น override, final, public(ในขณะที่สืบทอด) เพียงเพื่อทำให้โค้ดกะทัดรัดและสิ้นเปลือง (ส่วนใหญ่) ในขนาดหน้าจอมาตรฐานเดียว ฉันยังชอบ struct แทนที่จะเป็น class เพียงเพื่อบันทึกบรรทัดโดยไม่เขียน 'public:' ในบางครั้ง และยังพลาด "Virtual Destructor", Constructor, "Copy Constructor", คำนำหน้า std::, การลบหน่วยความจำแบบไดนามิกโดยตั้งใจ ฉันยังถือว่าตัวเองเป็นคนจริงจังที่ต้องการถ่ายทอดแนวคิดด้วยวิธีที่ง่ายที่สุดเท่าที่จะเป็นไปได้ แทนที่จะเป็นวิธีมาตรฐานหรือใช้ศัพท์เฉพาะ

หมายเหตุ:

  • หากคุณสะดุดที่นี่โดยตรง ฉันขอแนะนำให้คุณอ่านว่า "รูปแบบการออกแบบคืออะไร" ประการแรก แม้ว่าจะเป็นเรื่องเล็กน้อยก็ตาม ฉันเชื่อว่ามันจะสนับสนุนให้คุณสำรวจเพิ่มเติมเกี่ยวกับหัวข้อนี้
  • โค้ดทั้งหมดที่คุณพบในบทความชุดนี้รวบรวมโดยใช้ C++20 (แม้ว่าฉันจะใช้ฟีเจอร์ Modern C++ สูงถึง C++17 ในกรณีส่วนใหญ่ก็ตาม) ดังนั้นหากคุณไม่สามารถเข้าถึงคอมไพเลอร์ล่าสุด คุณสามารถใช้ https://wandbox.org/ ซึ่งมีการติดตั้ง Boost Library ไว้ล่วงหน้าเช่นกัน

เจตนา

=> โมดูลระดับสูงไม่ควรขึ้นอยู่กับโมดูลระดับต่ำ ทั้งสองอย่างควรขึ้นอยู่กับนามธรรม
=› นามธรรมไม่ควรขึ้นอยู่กับรายละเอียด รายละเอียดควรขึ้นอยู่กับนามธรรม

  • บรรทัดด้านบนอาจดูคลุมเครือในตอนแรก แต่อย่าติดอยู่ตรงนี้ ทำต่อไป คุณจะได้รับมันตามตัวอย่าง

โมดูลระดับสูงและระดับต่ำคืออะไร

=› โมดูลระดับสูง: อธิบายการดำเนินการ ซึ่งเป็นนามธรรมมากกว่าและมีตรรกะที่ซับซ้อนมากขึ้น โมดูลเหล่านี้ประสานโมดูลระดับต่ำในแอปพลิเคชันของเรา
=› โมดูลระดับต่ำ: อธิบายการใช้งาน เฉพาะเจาะจงมากขึ้น & เป็นรายบุคคลสำหรับส่วนประกอบที่เน้นรายละเอียดและส่วนเล็ก ๆ ของแอปพลิเคชัน โมดูลเหล่านี้ถูกใช้ภายในโมดูลระดับสูง

การละเมิดหลักการผกผันการพึ่งพา

enum class Relationship { parent, child, sibling };
struct Person {
    string      m_name;
};
struct Relationships {      // Low-level <<<<<<<<<<<<-------------------------
    vector<tuple<Person, Relationship, Person>>     m_relations;
    void add_parent_and_child(const Person &parent, const Person &child) {
        m_relations.push_back({parent, Relationship::parent, child});
        m_relations.push_back({child, Relationship::child, parent});
    }
};
struct Research {           // High-level  <<<<<<<<<<<<------------------------
    Research(const Relationships &relationships) {
        for (auto &&[first, rel, second] : relationships.m_relations) {// Need C++17 here
            if (first.m_name == "John" && rel == Relationship::parent)
                cout << "John has a child called " << second.m_name << endl;
        }
    }
};
int main() {
    Person parent{"John"};
    Person child1{"Chris"};
    Person child2{"Matt"};
    Relationships relationships;
    relationships.add_parent_and_child(parent, child1);
    relationships.add_parent_and_child(parent, child2);
    Research _(relationships);
    return EXIT_SUCCESS;
}
  • เมื่อต่อมาบนคอนเทนเนอร์ของ Relationships เปลี่ยนจาก vector เป็น set หรือคอนเทนเนอร์อื่นๆ คุณต้องเปลี่ยนในหลายที่ซึ่งไม่ใช่การออกแบบที่ดีนัก แม้ว่าชื่อสมาชิกข้อมูลเช่น Relationships::m_relations จะเปลี่ยนไป คุณจะพบว่าตัวเองทำลายส่วนอื่นๆ ของโค้ด
  • ดังที่คุณเห็นโมดูลระดับต่ำ เช่น Relationships ขึ้นอยู่กับโมดูลระดับสูงโดยตรง เช่น Research ซึ่งโดยพื้นฐานแล้วถือเป็นการละเมิด DIP

ตัวอย่างหลักการผกผันการพึ่งพา

  • แต่เราควรสร้างสิ่งที่เป็นนามธรรมและผูกโมดูลระดับต่ำและระดับสูงเข้ากับสิ่งที่เป็นนามธรรมนั้น พิจารณาการแก้ไขต่อไปนี้:
struct RelationshipBrowser {
    virtual vector<Person> find_all_children_of(const string &name) = 0;
};
struct Relationships : RelationshipBrowser {     // Low-level <<<<<<<<<<<<<<<------------------------
    vector<tuple<Person, Relationship, Person>>     m_relations;
    void add_parent_and_child(const Person &parent, const Person &child) {
        m_relations.push_back({parent, Relationship::parent, child});
        m_relations.push_back({child, Relationship::child, parent});
    }
    vector<Person> find_all_children_of(const string &name) {
        vector<Person> result;
        for (auto &&[first, rel, second] : m_relations) {
            if (first.name == name && rel == Relationship::parent) {
                result.push_back(second);
            }
        }
        return result;
    }
};
struct Research {                                // High-level <<<<<<<<<<<<<<<----------------------
    Research(RelationshipBrowser &browser) {
        for (auto &child : browser.find_all_children_of("John")) {
            cout << "John has a child called " << child.name << endl;
        }
    }
    //  Research(const Relationships& relationships)
    //  {
    //    auto& relations = relationships.relations;
    //    for (auto&& [first, rel, second] : relations)
    //    {
    //      if (first.name == "John" && rel == Relationship::parent)
    //      {
    //        cout << "John has a child called " << second.name << endl;
    //      }
    //    }
    //  }
};
  • ไม่ว่าตอนนี้ ชื่อของคอนเทนเนอร์หรือตัวคอนเทนเนอร์จะเปลี่ยนไปในโมดูลระดับต่ำ โมดูลระดับสูง หรือส่วนอื่นๆ ของโค้ดที่ตามหลัง DIP จะยังคงเหมือนเดิม
  • หลักการผกผันการพึ่งพา (DIP) แนะนำว่าระบบที่ยืดหยุ่นที่สุดคือระบบที่การพึ่งพาซอร์สโค้ดอ้างอิงถึงนามธรรมเท่านั้น ไม่ใช่การรวม
  • นี่คือเหตุผลว่าทำไม dev ที่มีประสบการณ์ส่วนใหญ่จึงใช้ฟังก์ชัน STL หรือไลบรารีร่วมกับคอนเทนเนอร์ทั่วไป แม้แต่การใช้คีย์เวิร์ด auto ในตำแหน่งที่เหมาะสมก็อาจช่วยในการสร้างพฤติกรรมทั่วไปที่มีโค้ดที่เปราะบางน้อยกว่าได้
  • มีหลายวิธีที่คุณสามารถใช้ DIP ได้ ตราบใดที่ C++ กังวลว่าคนส่วนใหญ่ใช้ความหลากหลายแบบคงที่ (เช่น CRTP เว้นแต่พวกเขาต้องการแบบไดนามิก), ความเชี่ยวชาญพิเศษของเทมเพลต, รูปแบบการออกแบบอะแดปเตอร์, การลบประเภท ฯลฯ

ปทัฏฐานในการสร้างซอฟต์แวร์ที่เป็นมิตรกับหลักการผกผันการพึ่งพา (DIP)

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

ประโยชน์

=› การนำกลับมาใช้ใหม่ได้

  • DIP ช่วยลดการเชื่อมต่อระหว่างส่วนต่างๆ ของโค้ดได้อย่างมีประสิทธิภาพ ดังนั้นเราจึงได้โค้ดที่นำมาใช้ซ้ำได้

=› การบำรุงรักษา

  • สิ่งสำคัญที่ต้องกล่าวถึงคือการเปลี่ยนแปลงโมดูลที่นำไปใช้แล้วนั้นมีความเสี่ยง ด้วยการขึ้นอยู่กับสิ่งที่เป็นนามธรรมและไม่ใช่การนำไปปฏิบัติอย่างเป็นรูปธรรม เราสามารถลดความเสี่ยงนั้นได้โดยไม่ต้องเปลี่ยนโมดูลระดับสูงในโครงการของเรา
  • สุดท้ายนี้ DIP เมื่อใช้อย่างถูกต้องจะทำให้เรามีความยืดหยุ่นและมีเสถียรภาพในระดับสถาปัตยกรรมทั้งหมดของแอปพลิเคชันของเรา แอปพลิเคชันของเราจะสามารถพัฒนาได้อย่างปลอดภัยยิ่งขึ้นและมีเสถียรภาพและแข็งแกร่ง

บทสรุป

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

อย่าใช้วัตถุที่เป็นรูปธรรมโดยตรง เว้นแต่คุณจะมีเหตุผลหนักแน่นที่จะทำเช่นนั้น ใช้นามธรรมแทน

กรมทรัพย์สินทางปัญญาฝึกให้เราคิดถึงชั้นเรียนในแง่ของพฤติกรรมมากกว่าการสร้างหรือการนำไปปฏิบัติ

มีข้อเสนอแนะ ข้อสงสัย หรือต้องการพูด Hi หรือไม่? คลายความกดดัน เพียงคลิกเดียว🖱️