Dependency Inversion Principle(ใน C++) คือหลักการออกแบบลำดับที่ห้าและสุดท้ายของซีรีส์ SOLID ซึ่งเป็นหลักการออกแบบ Rock หลักการออกแบบ SOLID มุ่งเน้นไปที่การพัฒนาซอฟต์แวร์ที่ง่ายต่อการบำรุงรักษา ใช้ซ้ำ และขยายได้ ในบทความนี้ เราจะเห็นโค้ดตัวอย่างพร้อมโฟลว์และแก้ไขด้วยความช่วยเหลือของ DIP เราจะดูแนวทางและประโยชน์ของกรมทรัพย์สินทางปัญญาในตอนท้ายของบทความด้วย
/!\: เผยแพร่ครั้งแรก @ www.vishalchovatiya.com.
อย่างไรก็ตาม หากคุณยังไม่ได้อ่านบทความก่อนหน้าของฉันเกี่ยวกับหลักการออกแบบ ลิงก์ด่วนด้านล่างนี้คือ:
- SRP — หลักการความรับผิดชอบเดียว
- OCP — หลักการเปิด/ปิด
- LSP — หลักการทดแทน Liskov
- ฉันSP — หลักการแยกส่วนต่อประสาน
- 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
หรือไม่? คลายความกดดัน เพียงคลิกเดียว🖱️