Giao diện và kế thừa: Tốt nhất của cả hai thế giới?


10

Tôi 'khám phá' giao diện và tôi bắt đầu yêu thích chúng. Vẻ đẹp của giao diện là nó là một hợp đồng và bất kỳ đối tượng nào đáp ứng hợp đồng đó đều có thể được sử dụng ở bất cứ nơi nào có giao diện đó được yêu cầu.

Vấn đề với giao diện là nó không thể có cài đặt mặc định, điều này gây khó khăn cho các thuộc tính trần tục và đánh bại DRY. Điều này cũng tốt, bởi vì nó giữ cho việc thực hiện và hệ thống tách rời. Kế thừa, trên tay, duy trì một khớp nối chặt chẽ hơn, và có khả năng phá vỡ đóng gói.

Trường hợp 1 (Kế thừa với các thành viên tư nhân, đóng gói tốt, kết hợp chặt chẽ)

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();
)

Trường hợp 2 (chỉ là một giao diện)

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.

Trường hợp 3: (tốt nhất của cả hai thế giới?)

Tương tự như trường hợp 1 . Tuy nhiên, hãy tưởng tượng rằng (giả thiết) C ++ không cho phép ghi đè các phương thức ngoại trừ các phương thức đó là thuần ảo .

Vì vậy, trong trường hợp 1 , ghi đè do_work () sẽ gây ra lỗi thời gian biên dịch. Để khắc phục điều này, chúng tôi đặt do_work () là ảo thuần túy và thêm một phương thức riêng tăng dần_money_earned (). Ví dụ:

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(); ); .
};

Nhưng ngay cả điều này có vấn đề. Điều gì sẽ xảy ra nếu 3 tháng kể từ bây giờ, Joe Coder tạo ra một Nhân viên Bác sĩ, nhưng anh ta quên gọi tăng_money_earned () trong do_work ()?


Câu hỏi:

  • Trường hợp 3 vượt trội so với Trường hợp 1 ? Có phải vì nó là 'đóng gói tốt hơn' hay 'kết nối lỏng lẻo hơn', hay một số lý do khác?

  • Trường hợp 3 vượt trội so với Trường hợp 2 vì nó phù hợp với DRY?


2
... bạn đang phát minh lại các lớp trừu tượng hay gì?
ZJR

Câu trả lời:


10

Một cách để giải quyết vấn đề quên gọi cho siêu lớp là đưa điều khiển trở lại cho siêu lớp! Tôi đã nói đùa lại ví dụ đầu tiên của bạn để chỉ ra cách (và làm cho nó biên dịch;)). Ồ, tôi cũng cho rằng do_work()trong Employeeđược cho là virtualtrong ví dụ đầu tiên của bạn.

#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();
}

Bây giờ do_work()không thể bị ghi đè. Nếu bạn muốn mở rộng nó, bạn phải thực hiện thông qua on_do_work()đó do_work()có quyền kiểm soát.

Điều này, tất nhiên, có thể được sử dụng với giao diện từ ví dụ thứ hai của bạn nếu Employeemở rộng nó. Vì vậy, nếu tôi hiểu bạn một cách chính xác, tôi nghĩ rằng nó tạo ra Trường hợp 3 này nhưng không phải sử dụng giả thuyết C ++! Đó là DRY và nó được đóng gói mạnh mẽ.


3
Và đó là mẫu thiết kế được gọi là "phương thức mẫu" ( en.wikipedia.org/wiki/Template_method_potype ).
Joris Timmermans

Có, đây là trường hợp 3 tuân thủ. Điều này có vẻ đầy hứa hẹn. Sẽ kiểm tra chi tiết. Ngoài ra, đây là một số loại hệ thống sự kiện. Có một tên cho 'mẫu' này?
MustafaM

@MadKeithV bạn có chắc đây là 'phương thức mẫu' không?
MustafaM

@illmath - vâng, đó là một phương thức công khai không ảo, ủy thác các phần chi tiết triển khai của nó cho các phương thức được bảo vệ / riêng tư ảo.
Joris Timmermans

@illmath Trước đây tôi chưa nghĩ nó là một phương thức mẫu nhưng tôi tin rằng đây là một ví dụ cơ bản. Tôi vừa tìm thấy bài viết này mà bạn có thể muốn đọc nơi tác giả tin rằng nó xứng đáng với tên riêng của nó: Thành ngữ giao diện không ảo
Gyan aka Gary Buyn 17/212

1

Vấn đề với giao diện là nó không thể có cài đặt mặc định, điều này gây khó khăn cho các thuộc tính trần tục và đánh bại DRY.

Theo ý kiến ​​riêng của tôi, các giao diện chỉ nên có các phương thức thuần túy - không có triển khai mặc định. Nó không phá vỡ nguyên tắc DRY theo bất kỳ cách nào, bởi vì các giao diện cho thấy cách truy cập một số thực thể. Chỉ để tham khảo, tôi đang xem phần giải thích DRY ở đây :
"Mỗi phần kiến ​​thức phải có một đại diện duy nhất, rõ ràng, có thẩm quyền trong một hệ thống."

Mặt khác, RẮN cho bạn biết rằng mỗi lớp nên có một giao diện.

Trường hợp 3 có vượt trội hơn Trường hợp 1 không? Có phải vì nó là 'đóng gói tốt hơn' hay 'kết nối lỏng lẻo hơn', hay một số lý do khác?

Không, trường hợp 3 không vượt trội so với trường hợp 1. Bạn phải quyết định. Nếu bạn muốn có một triển khai mặc định thì hãy làm như vậy. Nếu bạn muốn một phương pháp thuần túy thì hãy đi với nó.

Điều gì sẽ xảy ra nếu 3 tháng kể từ bây giờ, Joe Coder tạo ra một Nhân viên Bác sĩ, nhưng anh ta quên gọi tăng_money_earned () trong do_work ()?

Sau đó, Joe Coder sẽ có được những gì anh ta xứng đáng để bỏ qua các bài kiểm tra đơn vị thất bại. Anh ấy đã kiểm tra lớp này, phải không? :)

Trường hợp nào là tốt nhất cho một dự án phần mềm có thể có 40.000 dòng mã?

Một kích thước không phù hợp với tất cả. Không thể nói cái nào tốt hơn. Có một số trường hợp người này sẽ phù hợp hơn người kia.

Có lẽ bạn nên tìm hiểu một số mẫu thiết kế thay vì cố gắng phát minh ra một số mẫu của riêng bạn.


Tôi chỉ nhận ra rằng bạn đang tìm kiếm mẫu thiết kế giao diện không ảo , bởi vì đó là trường hợp 3 lớp của bạn trông như thế nào.


Cảm ơn các bình luận. Tôi đã cập nhật Trường hợp 3 để làm cho ý định của tôi rõ ràng hơn.
MustafaM

1
Tôi sẽ phải -1 bạn ở đây. Không có lý do nào để nói rằng tất cả các giao diện nên thuần túy hoặc tất cả các lớp nên kế thừa từ một giao diện.
DeadMG

@DeadMG ISP
BЈовић

@VJovic: Có một sự khác biệt lớn giữa RẮN và "Mọi thứ phải được kế thừa từ một giao diện".
DeadMG

"Một kích thước không phù hợp với tất cả" và "tìm hiểu một số mẫu thiết kế" là chính xác - phần còn lại của câu trả lời của bạn vi phạm đề xuất của riêng bạn rằng một kích thước không phù hợp với tất cả.
Joris Timmermans

0

Các giao diện có thể có các cài đặt mặc định trong C ++. Không có gì nói rằng việc triển khai mặc định của hàm không chỉ phụ thuộc vào các thành viên ảo (và đối số) khác, do đó không làm tăng bất kỳ loại khớp nối nào.

Đối với trường hợp 2, DRY đang thay thế ở đây. Đóng gói tồn tại để bảo vệ chương trình của bạn khỏi sự thay đổi, từ các triển khai khác nhau, nhưng trong trường hợp này, bạn không có các triển khai khác nhau. Vì vậy, đóng gói YAGNI.

Trong thực tế, giao diện thời gian chạy thường được coi là kém hơn so với tương đương thời gian biên dịch của chúng. Trong trường hợp biên dịch thời gian, bạn có thể có cả trường hợp 1 trường hợp 2 trong cùng một gói - chưa kể đó là vô số lợi thế khác. Hoặc thậm chí vào thời gian chạy, bạn chỉ có thể làm Employee : public IEmployeecho lợi thế tương tự. Có rất nhiều cách để đối phó với những điều như vậy.

Case 3: (best of both worlds?)

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

Tôi ngừng đọc. YAGNI. C ++ là C ++ là gì, và ủy ban Tiêu chuẩn sẽ không bao giờ thực hiện thay đổi như vậy, vì những lý do tuyệt vời.


Bạn nói "bạn không có cách thực hiện nào khác". Nhưng tôi làm. Tôi có Y tá thực hiện Nhân viên và sau này tôi có thể có các triển khai khác (Bác sĩ, Người báo cáo, v.v.). Tôi đã cập nhật Trường hợp 3 để làm rõ hơn ý của tôi.
MustafaM

@illmath: Nhưng bạn không có cách triển khai nào khác get_name. Tất cả các triển khai đề xuất của bạn sẽ chia sẻ cùng thực hiện get_name. Bên cạnh đó, như tôi đã nói, không có lý do để lựa chọn, bạn có thể có cả hai. Ngoài ra, Trường hợp 3 là hoàn toàn vô giá trị. Bạn có thể ghi đè các ảo không thuần túy, vì vậy hãy quên đi một thiết kế mà bạn không thể.
DeadMG

Các giao diện không chỉ có thể có các cài đặt mặc định trong C ++, chúng có thể có các cài đặt mặc định và vẫn còn trừu tượng! tức là khoảng trống ảo IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans

@MadKeithV: Tôi không tin rằng bạn có thể định nghĩa chúng nội tuyến, nhưng quan điểm vẫn giống nhau.
DeadMG

@MadKeith: Như thể Visual Studio đã từng là một đại diện đặc biệt chính xác của Standard C ++.
DeadMG

0

Trường hợp 3 có vượt trội hơn Trường hợp 1 không? Có phải vì nó là 'đóng gói tốt hơn' hay 'kết nối lỏng lẻo hơn', hay một số lý do khác?

Từ những gì tôi thấy trong triển khai của bạn, việc triển khai Trường hợp 3 của bạn yêu cầu một lớp trừu tượng có thể thực hiện các phương thức ảo thuần túy mà sau đó có thể được thay đổi trong lớp dẫn xuất. Trường hợp 3 sẽ tốt hơn vì lớp dẫn xuất có thể thay đổi việc triển khai do_work và khi được yêu cầu và tất cả các trường hợp dẫn xuất về cơ bản sẽ thuộc về kiểu trừu tượng cơ sở.

Trường hợp nào là tốt nhất cho một dự án phần mềm có thể có 40.000 dòng mã.

Tôi muốn nói rằng nó hoàn toàn phụ thuộc vào thiết kế thực hiện của bạn và mục tiêu mà bạn muốn đạt được. Lớp trừu tượng và Giao diện được triển khai dựa trên vấn đề phải giải quyết.

Chỉnh sửa câu hỏi

Điều gì sẽ xảy ra nếu 3 tháng kể từ bây giờ, Joe Coder tạo ra một Nhân viên Bác sĩ, nhưng anh ta quên gọi tăng_money_earned () trong do_work ()?

Các bài kiểm tra đơn vị có thể được thực hiện để kiểm tra xem mỗi lớp có xác nhận hành vi dự kiến ​​hay không. Vì vậy, nếu các bài kiểm tra đơn vị thích hợp được áp dụng, lỗi có thể được ngăn chặn khi Joe Coder thực hiện lớp mới.


0

Việc sử dụng các giao diện chỉ phá vỡ DRY nếu mỗi lần thực hiện là một bản sao của nhau. Bạn có thể giải quyết vấn đề nan giải này bằng cách áp dụng cả giao diện kế thừa, tuy nhiên vẫn có một số trường hợp bạn có thể muốn thực hiện cùng một giao diện trên một số lớp, nhưng thay đổi hành vi trong từng lớp và điều này vẫn tuân theo nguyên tắc của KHÔ. Cho dù bạn chọn sử dụng bất kỳ phương pháp nào trong 3 cách tiếp cận mà bạn đã mô tả đều tùy thuộc vào các lựa chọn bạn cần thực hiện để áp dụng kỹ thuật tốt nhất để phù hợp với một tình huống nhất định. Mặt khác, có thể bạn sẽ thấy rằng theo thời gian, bạn sử dụng Giao diện nhiều hơn và chỉ áp dụng quyền thừa kế khi bạn muốn xóa lặp lại. Điều đó không có nghĩa là đây là duy nhất lý do thừa kế, nhưng tốt hơn hết là giảm thiểu việc sử dụng quyền thừa kế để cho phép bạn giữ các tùy chọn của mình mở nếu bạn thấy thiết kế của mình cần thay đổi sau này và nếu bạn muốn giảm thiểu tác động lên các lớp con cháu khỏi các hiệu ứng thay đổi sẽ giới thiệu trong một lớp cha mẹ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.