อินเทอร์เฟซและการสืบทอด: สุดยอดของทั้งสองโลก


10

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

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

กรณีที่ 1 (การสืบทอดกับสมาชิกส่วนตัวการห่อหุ้มที่ดีเข้าด้วยกันอย่างแน่นหนา)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

กรณีที่ 2 (เพียงอินเทอร์เฟซ)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

กรณีที่ 3: (ดีที่สุดของทั้งสองโลก)

คล้ายกับกรณีที่ 1 แต่คิดว่า (สมมุติฐาน) C ++ไม่อนุญาตให้เอาชนะวิธีการยกเว้นวิธีการเหล่านั้นที่มีความบริสุทธิ์เสมือน

ดังนั้นในกรณีที่ 1 การแทนที่ do_work () จะทำให้เกิดข้อผิดพลาดในเวลาคอมไพล์ ในการแก้ไขปัญหานี้เราได้ตั้ง do_work () เป็นเสมือนจริงและเพิ่มเมธอด increment_money_earned () แยกต่างหาก ตัวอย่างเช่น:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

แต่ถึงอย่างนี้ก็มีปัญหา จะเกิดอะไรขึ้นถ้า 3 เดือนจากนี้ Joe Coder สร้าง Doctor Employee แต่เขาลืมโทรไปที่ increment_money_earned () ใน do_work ()?


คำถาม:

  • เป็นกรณีที่ 3ดีกว่ากรณีที่ 1 ? เป็นเพราะ 'encapsulation ที่ดีกว่า' หรือ 'double loosely ที่มากกว่า' หรือด้วยเหตุผลอื่น?

  • เป็นกรณีที่ 3ดีกว่ากรณีที่ 2เพราะมันสอดคล้องกับแห้ง?


2
... คุณสร้างชั้นเรียนที่เป็นนามธรรมหรืออะไร
ZJR

คำตอบ:


10

วิธีหนึ่งในการแก้ปัญหาที่ลืมไม่ได้ที่จะเรียก superclass คือการให้การควบคุมกลับไปที่ superclass! ฉันได้ทบทวนตัวอย่างแรกของคุณอีกครั้งเพื่อแสดงให้เห็นว่า (และทำให้คอมไพล์ได้อย่างไร)) โอ้ฉันยังสมมติว่าdo_work()ในEmployeeนั้นควรจะเป็นvirtualในตัวอย่างแรกของคุณ

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

ตอนนี้do_work()ไม่สามารถเขียนทับได้ หากคุณต้องการที่จะขยายมันคุณจะต้องทำผ่านทางon_do_work()ที่do_work()มีการควบคุม

แน่นอนว่าสิ่งนี้สามารถใช้กับอินเทอร์เฟซจากตัวอย่างที่สองของคุณได้เช่นกันหากEmployeeขยายออกไป ดังนั้นถ้าฉันเข้าใจคุณอย่างถูกต้องฉันคิดว่านั่นทำให้กรณีนี้ 3 แต่ไม่ต้องใช้สมมุติ C ++! มันแห้งและมีการห่อหุ้มที่ดี


3
และนั่นคือรูปแบบการออกแบบที่รู้ว่าเป็น "วิธีการแบบ" ( en.wikipedia.org/wiki/Template_method_pattern )
Joris Timmermans

ใช่นี้เป็นไปตาม Case 3 สิ่งนี้ดูมีแนวโน้ม จะทำการตรวจสอบในรายละเอียด นอกจากนี้นี่เป็นระบบเหตุการณ์บางประเภท มีชื่อสำหรับ 'รูปแบบ' นี้หรือไม่?
MustafaM

@MadKeithV คุณแน่ใจหรือว่านี่เป็น 'วิธีการแบบเทมเพลต'
MustafaM

@illmath - ใช่มันเป็นวิธีการที่ไม่ใช่เสมือนสาธารณะซึ่งมอบหมายรายละเอียดบางส่วนของการดำเนินการให้กับวิธีการป้องกัน / ส่วนตัวเสมือน
Joris Timmermans

@illmath ฉันไม่เคยคิดว่ามันเป็นวิธีเทมเพลตมาก่อน แต่ฉันเชื่อว่ามันเป็นตัวอย่างพื้นฐานของหนึ่ง ฉันเพิ่งพบบทความนี้ซึ่งคุณอาจต้องการอ่านที่ผู้เขียนเชื่อว่าสมควรได้รับชื่อของตัวเอง: ไม่ใช่อินเทอร์เฟซเสมือน Idiom
Gyan aka Gary Buyn

1

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

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

ในทางกลับกันSOLIDจะบอกคุณว่าทุกคลาสควรมีส่วนต่อประสาน

Case 3 เหนือกว่า Case 1 หรือไม่ เป็นเพราะ 'encapsulation ที่ดีกว่า' หรือ 'double loosely ที่มากกว่า' หรือด้วยเหตุผลอื่น?

ไม่ได้กรณีที่ 3 ไม่เหนือกว่ากรณีที่ 1 คุณต้องตัดสินใจ หากคุณต้องการมีการใช้งานเริ่มต้นให้ทำเช่นนั้น หากคุณต้องการวิธีการที่บริสุทธิ์แล้วไปกับมัน

จะเกิดอะไรขึ้นถ้า 3 เดือนจากนี้ Joe Coder สร้าง Doctor Employee แต่เขาลืมโทรไปที่ increment_money_earned () ใน do_work ()?

จากนั้นโจ Coder ควรได้รับสิ่งที่เขาสมควรได้รับเพราะละเลยการทดสอบหน่วยที่ล้มเหลว เขาทดสอบวิชานี้ใช่มั้ย :)

กรณีใดดีที่สุดสำหรับโครงการซอฟต์แวร์ที่อาจมีรหัส 40,000 บรรทัด

ขนาดเดียวไม่พอดีทั้งหมด ไม่สามารถบอกได้ว่าอันไหนดีกว่ากัน มีบางกรณีที่หนึ่งจะพอดีดีกว่านั้นอีก

บางทีคุณควรเรียนรู้รูปแบบการออกแบบแทนที่จะพยายามประดิษฐ์ของคุณเอง


ฉันเพิ่งรู้ว่าคุณกำลังมองหารูปแบบการออกแบบอินเตอร์เฟสที่ไม่เสมือนเพราะนั่นคือสิ่งที่คลาส 3 ของคุณดูเหมือน


ขอบคุณสำหรับความคิดเห็น ฉันได้อัปเดตกรณีที่ 3 เพื่อให้ความตั้งใจชัดเจนยิ่งขึ้น
MustafaM

1
ฉันจะต้อง -1 คุณที่นี่ ไม่มีเหตุผลเลยที่จะบอกว่าอินเทอร์เฟซทั้งหมดควรบริสุทธิ์หรือว่าคลาสทั้งหมดควรสืบทอดจากอินเตอร์เฟส
DeadMG

@DeadMG ISP
BЈовић

@VJovic: มีความแตกต่างใหญ่ระหว่าง SOLID และ "ทุกสิ่งต้องสืบทอดจากส่วนต่อประสาน"
DeadMG

"ขนาดเดียวไม่พอดีทั้งหมด" และ "เรียนรู้รูปแบบการออกแบบบางอย่าง" ถูกต้องส่วนที่เหลือของคำตอบของคุณละเมิดคำแนะนำของคุณเองว่าขนาดเดียวไม่พอดีทั้งหมด
Joris Timmermans

0

การเชื่อมต่อสามารถมีการใช้งานเริ่มต้นใน C ++ ไม่มีอะไรบอกว่าการใช้งานเริ่มต้นของฟังก์ชั่นไม่เพียง แต่ขึ้นอยู่กับสมาชิกเสมือนอื่น ๆ (และข้อโต้แย้ง) ดังนั้นจึงไม่เพิ่มการมีเพศสัมพันธ์ชนิดใด ๆ

สำหรับกรณีที่ 2 DRY จะถูกแทนที่ที่นี่ Encapsulation มีอยู่เพื่อป้องกันโปรแกรมของคุณจากการเปลี่ยนแปลงจากการใช้งานที่แตกต่างกัน แต่ในกรณีนี้คุณไม่มีการใช้งานที่แตกต่างกัน ดังนั้นการห่อหุ้ม YAGNI

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

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

ฉันหยุดอ่าน YAGNI C ++ คือสิ่งที่ C ++ คือและคณะกรรมการมาตรฐานจะไม่ดำเนินการเปลี่ยนแปลงเช่นนี้ด้วยเหตุผลที่ยอดเยี่ยม


คุณพูดว่า "คุณไม่มีการใช้งานที่แตกต่าง" แต่ฉันทำ. ฉันมีพยาบาลนำไปใช้ของพนักงานและฉันอาจมีการใช้งานอื่น ๆ ในภายหลัง (หมอ, ภารโรง ฯลฯ ) ฉันได้อัปเดต Case 3 เพื่อให้ชัดเจนยิ่งขึ้นว่าฉันหมายถึงอะไร
MustafaM

@illmath: แต่คุณไม่มีการใช้งานอื่น ๆ get_nameของ get_nameทั้งหมดของการใช้งานที่คุณเสนอจะแบ่งการดำเนินงานเดียวกันของ นอกจากที่ฉันกล่าวว่าไม่มีเหตุผลที่จะเลือกคุณสามารถมีทั้ง นอกจากนี้กรณีที่ 3 ก็ไร้ค่าอย่างที่สุด คุณสามารถแทนที่เสมือนที่ไม่บริสุทธิ์ดังนั้นลืมเกี่ยวกับการออกแบบที่คุณไม่สามารถทำได้
DeadMG

อินเทอร์เฟซไม่เพียง แต่สามารถมีการใช้งานเริ่มต้นใน C ++ แต่ยังสามารถมีการใช้งานเริ่มต้นและยังคงเป็นนามธรรม! ie virtual void IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans

@MadKeithV: ฉันไม่เชื่อว่าคุณสามารถกำหนดแบบอินไลน์ แต่ประเด็นยังคงเหมือนเดิม
DeadMG

@MadKeith: ราวกับว่า Visual Studio เป็นตัวแทน C ++ ที่แม่นยำเป็นพิเศษ
DeadMG

0

Case 3 เหนือกว่า Case 1 หรือไม่ เป็นเพราะ 'encapsulation ที่ดีกว่า' หรือ 'double loosely ที่มากกว่า' หรือด้วยเหตุผลอื่น?

จากสิ่งที่ฉันเห็นในการนำไปใช้ของคุณการดำเนินการของCase 3ของคุณต้องการคลาสนามธรรมซึ่งสามารถใช้วิธีเสมือนจริงซึ่งสามารถเปลี่ยนแปลงได้ในภายหลังในคลาสที่ได้รับมา กรณีที่ 3จะดีกว่าเนื่องจากคลาสที่ได้รับสามารถเปลี่ยนการใช้งาน do_work ได้ตามต้องการและเมื่อจำเป็นและอินสแตนซ์ที่ได้รับทั้งหมดจะเป็นของประเภทนามธรรมพื้นฐาน

เคสใดดีที่สุดสำหรับโครงการซอฟต์แวร์ที่อาจมีโค้ด 40,000 บรรทัด

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

แก้ไขตามคำถาม

จะเกิดอะไรขึ้นถ้า 3 เดือนจากนี้ Joe Coder สร้าง Doctor Employee แต่เขาลืมโทรไปที่ increment_money_earned () ใน do_work ()?

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


0

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.