2010-11-12 39 views
5

Tôi đang cố gắng sử dụng lớp cơ sở trừu tượng C++ theo cách tương tự với giao diện Java. Cho rằng chúng tôi đã theo các lớp giao diện với các chức năng ảo chỉ thuần túy:Thừa kế song song giữa các lớp giao diện và lớp triển khai trong C++

class Shape { virtual double area()=0; }; 
class Square : public Shape { virtual void setLength(double length)=0; }; 
class Rectangle : public Square { virtual void setWidth(double width)=0; }; 

và tôi cố gắng thực hiện Square và Rectangle theo cách sau:

class SquareImpl : public Square { /*implementation*/ }; 
class RectangleImpl : public SquareImpl, Rectangle { /*implementation*/ }; 

đâu RectangleImpl thừa hưởng cả SquareImplRectangle để tái sử dụng, nói , SquareImpl::area(). Tuy nhiên khi tôi cố gắng biên dịch, hai vấn đề phát sinh: Thứ nhất, tất cả các phương thức trong SquareImpl không được kế thừa đúng cách và tôi phải thực hiện lại thủ công RectangleImpl::area()RectangleImpl::setLength(). Thứ hai, điều này vẫn giới thiệu vấn đề kim cương mà Shape là cơ sở mơ hồ của RectangleImpl.

Tôi có thể biên dịch mã nếu tôi hầu như kế thừa Square từ Shape, nhưng tôi không nghĩ rằng hiệu suất sẽ mở rộng với nhiều giao diện xuất phát hơn được thêm vào. Ngoài ra, RectangleImpl vẫn không kế thừa SquareImpl::setLength() mặc dù SquareImpl::area() được kế thừa tốt. (bỏ qua tính thực tiễn tại đây)

Một giải pháp khác có thể là làm cho giao diện độc lập với nhau, tức là làm cho Square không được kế thừa từ Shape. Nhưng làm như vậy sẽ làm cho tôi mất quyền truy cập vào các phương thức trong Shape nếu tôi định nghĩa các hàm lấy con trỏ Square*. Nó cũng sẽ làm cho static_cast không thể giữa ShapeSquare. Vì vậy, câu hỏi của tôi là, có bất kỳ mẫu thiết kế nào khác trong C++ để giải quyết loại thừa kế song song này giữa các lớp giao diện và các lớp thực hiện, mà không yêu cầu thừa kế ảo không? Không.

(chỉnh sửa làm rõ: mã ví dụ trên chỉ là minh họa hình nộm của tôi về thừa kế song song giữa các giao diện và hiện thực Tôi hiểu rằng có những cách tốt hơn để thực hiện hình dạng nhưng vấn đề của tôi không phải là làm thế nào để thực hiện hình dạng..)

+0

"Nơi RectangleImpl kế thừa cả SquareImpl và Rectangle để tái sử dụng, SquareImpl :: area()" => kế thừa để được sử dụng lại, không sử dụng lại. – icecrime

+0

dupe của http://stackoverflow.com/questions/249500/looking-for-a-better-way-than-virtual-inheritance-in-c? –

+0

Việc thừa kế không công khai của Hình chữ nhật trong RectangleImpl có dự định không? – daramarak

Trả lời

3

Điều bạn có ở đây là trường hợp của Diamond Problem, điều này có thể xảy ra trong bất kỳ ngôn ngữ OO nào cho phép đa kế thừa. Điều này, bằng cách này, là một trong những lý do tại sao các nhà thiết kế của Java quyết định không có nhiều thừa kế, và đã đưa ra khái niệm về một giao diện.

Cách giao dịch C++ với vấn đề kim cương là Virtual Inheritance.

Và, như codymanix chỉ ra, hình vuông và hình chữ nhật là một ví dụ nổi tiếng xấu cho thiết kế hướng đối tượng, bởi vì như xa như OO là có liên quan a square is not a rectangle.

Thêm vài điểm nữa. Thứ nhất, Thuật ngữ cho những gì bạn đang làm ở đây là nhiều thừa kế, không phải "thừa kế song song". Thứ hai, trong trường hợp đặc biệt này, nó thực sự không có ý nghĩa gì khi có một số class Square và một số class SquareImpl. Nếu bạn nghĩ rằng bạn có thể có các triển khai khác nhau của Square, bạn chỉ nên có một lớp cơ sở cung cấp triển khai mặc định và các chức năng ảo có thể bị ghi đè bởi lớp dẫn xuất nếu cần. Nói cách khác, bạn nên cuộn SquareSquareImpl vào một lớp học với các chức năng ảo.

Bạn chắc chắn có thể sử dụng lớp C++ trừu tượng như giao diện Java, nhưng phần lớn thời gian không có lý do gì cho nó. Giao diện đã được thêm vào Java một cách chính xác như một cách để giải quyết vấn đề thiếu nhiều thừa kế. Trong C++, bạn có thể tiếp tục và sử dụng nhiều thừa kế, mặc dù bạn nên luôn luôn làm điều đó một cách thận trọng.

+1

Tôi đã sử dụng các hình dạng như một ví dụ về việc triển khai một cái gì đó có khả năng phức tạp đằng sau một lớp giao diện C++. Trong trường hợp này, 'Square' đủ đơn giản để có thể thực hiện và kết hợp giao diện, có thể không phải là trường hợp cho các tình huống khác. Dù sao, xin lưu ý rằng hình dạng là một ví dụ nổi tiếng xấu. Tôi đoán tôi sẽ sử dụng FooBar trong các ví dụ của tôi lần sau. – Soares

+0

Đề xuất của bạn để cuộn Square và SquareImpl thành một lớp đơn hoạt động trong trường hợp này, nhưng vấn đề của Soares xảy ra thường xuyên khi viết mã để tiêm phụ thuộc. Đối với DI, bạn sẽ luôn có 2 lớp SquareImpl có các triển khai rất khác nhau (một là thực, một là một đối tượng giả đơn giản). Không có mã số để chia sẻ, do đó, nó làm cho cảm giác hoàn hảo để làm cho họ kế thừa từ cùng một giao diện. FYI giải pháp tôi đã đi với DI là chỉ cần làm thừa kế ảo từ ngăn xếp kế thừa giao diện. Một chút trùng lặp, nhưng ít nhất nó hoạt động: / – Weston

0

Hình vuông không phải là Hình chữ nhật và Hình chữ nhật không phải là Hình vuông. Điều duy nhất họ có điểm chung là họ là Shapes. Vì vậy:

class Square : public Shape {...}; 
class Rectangle : public Shape {...}; 

chức năng khởi tạo của họ là khác nhau, Square::setSide(double)Rectangle::setLengthAndWidth(double, double). Bạn không cần * Các lớp học Impl. Làm công cụ của bạn trong Quảng trường và Hình chữ nhật.

+0

Ví dụ của tôi chỉ là minh họa cho vấn đề. Tôi hiểu rằng có những cách tốt hơn để thực hiện hình dạng nhưng tôi chỉ tạo nên các lớp để thể hiện quan điểm của tôi về sự kế thừa song song. – Soares

+1

Ah, minh hoạ không tốt. Hmm, tôi nghĩ bạn nên xóa 'public SquareImpl' khỏi' class RectangleImpl' và đặt mã chung vào một số hàm tĩnh mà cả hai 'SquareImpl' và' RectangleImpl' đều có thể gọi. – Dialecticus

+1

-1 Tôi cảm thấy bạn đã không thực sự trả lời vấn đề của người hỏi, tôi nghĩ rõ ràng là anh ấy có những điều cơ bản ở đây – Elemental

0

Tôi nghĩ bạn nên tìm kiếm thừa kế ảo ở đây để bạn chỉ có một thể hiện hình dạng bên dưới tất cả - điều này dường như rõ ràng từ quan điểm ngữ nghĩa - tức là hình vuông và hình chữ nhật rõ ràng là cùng một hình dạng .

Không thực sự chắc chắn về vấn đề hiệu suất mà bạn đề cập - có vẻ như không có vấn đề gì với tôi (theo nghĩa là không phải trả thêm phí nào ngoài việc gọi bất kỳ hàm v nào). Tôi không thấy lý do tại sao bạn không thể truy cập setLength từ hình vuông - rất khó để xem tại sao bạn có thể gặp phải điều này và bạn không có nguồn cho việc triển khai.

0

Vấn đề của bạn là Rectangle-> Square-> Hình dạng không biết gì về SquareImpl nên nó không thể sử dụng các hàm này để thỏa mãn các yêu cầu chức năng trừu tượng của nó.

Cách dễ nhất là không kết hợp giao diện và triển khai kế thừa khi chúng bị ràng buộc chặt chẽ như thế này. Làm cho RectangleImpl kế thừa từ các giao diện Square và Rectangle. Nếu SquareImpl và RectangleImpl đang sao chép quá nhiều mã của nhau, hãy sử dụng một lớp làm tất cả công việc này và có chức năng thành viên trong mỗi lần triển khai.

0

Sau khi xem xét lại một đêm và đề cập đến sean giải pháp được cung cấp tại Looking for a better way than virtual inheritance in C++, tôi đã đưa ra giải pháp sau.

Ở đây tôi xác định lại vấn đề trừu tượng hơn để tránh nhầm lẫn chúng tôi có trên hình dạng. Chúng ta có một giao diện Ball có thể cuộn, một giao diện FooBall chứa các phương thức cụ thể của Foo và giao diện FooBarBall cũng là một số FooBall và chứa cả hai phương thức cụ thể của Bar và Foo cụ thể. Tương tự như vấn đề ban đầu, chúng tôi có một triển khai thực hiện FooBall và chúng tôi muốn lấy nó để bao gồm các phương thức cụ thể của Bar. nhưng kế thừa cả giao diện và triển khai sẽ giới thiệu thừa kế kim cương.

Để giải quyết vấn đề, thay vì đặt trực tiếp Foo và Bar phương pháp cụ thể vào nguồn gốc Ball giao diện, tôi đặt một phương pháp duy nhất vào một FooBall giao diện nguồn gốc có thể chuyển đổi các đối tượng vào một đối tượng Foo thông qua phương pháp toFoo(). Bằng cách này, việc triển khai có thể kết hợp trong giao diện độc lập FooBar mà không cần đưa vào thừa kế kim cương.

Tuy nhiên, không phải tất cả các mã đều có thể được loại bỏ để lấy tất cả các Thanh từ Foos một cách tự do. Chúng tôi vẫn phải viết các triển khai độc lập của Ball, FooBallFooBarBall mà không kế thừa từ mỗi người khác.Nhưng chúng ta có thể sử dụng mẫu tổng hợp để bọc các đối tượng FooBar thực được thực hiện khác nhau. Bằng cách này, chúng tôi vẫn có thể loại bỏ khá nhiều mã nếu chúng tôi có nhiều triển khai của Foo và Bar.

#include <stdio.h> 

class Ball { 
    public: 
    // All balls can roll. 
    virtual void roll() = 0; 

    // Ball has many other methods that are not 
    // covered here. 

    virtual inline ~Ball() { 
     printf("deleting Ball\n"); 
    }; 
}; 

class Foo { 
    public: 
    virtual void doFoo() = 0; 

    // do some very complicated stuff. 
    virtual void complexFoo() = 0; 

    virtual inline ~Foo() {}; 
}; 

/** 
* We assume that classes that implement Bar also 
* implement the Foo interface. The Bar interface 
* specification failed to enforce this constraint 
* by inheriting from Foo because it will introduce 
* diamond inheritance into the implementation. 
**/ 
class Bar { 
    public: 
    virtual void doBar() = 0; 
    virtual void complicatedBar() = 0; 

    virtual inline ~Bar() {}; 
}; 

class FooBall : public Ball { 
    public: 
    virtual Foo* toFoo() = 0; 

    virtual inline ~FooBall() {}; 
}; 

/** 
* A BarBall is always also a FooBall and support 
* both Foo and Bar methods. 
**/ 
class FooBarBall : public FooBall { 
    public: 
    virtual Bar* toBar() = 0; 

    virtual inline ~FooBarBall() {}; 
}; 


/* Composite Implementation */ 

class FooImpl_A : public Foo { 
    public: 
    virtual void doFoo() { 
     printf("FooImpl_A::doFoo()\n"); 
    }; 

    virtual void complexFoo() { 
     printf("FooImpl_A::complexFoo()\n"); 
    } 

    virtual inline ~FooImpl_A() { 
     printf("deleting FooImpl_A\n"); 
    } 
}; 

class FooBarImpl_A : public FooImpl_A, public Bar { 
    public: 
    virtual void doBar() { 
     printf("BarImpl_A::doBar()\n"); 
    } 

    virtual void complicatedBar() {; 
     printf("BarImpl_A::complicatedBar()\n"); 
    } 

    virtual inline ~FooBarImpl_A() { 
     printf("deleting FooBarImpl_A\n"); 
    } 
}; 

/* Composite Pattern */ 
class FooBarBallContainer : public FooBarBall { 
    public: 

    /* FooBarBallImpl_A can take any class that 
    * implements both the Foo and Bar interface, 
    * including classes that inherit FooBarImpl_A 
    * and other different implementations. 
    * 
    * We'll assume that realFoo and realBar are 
    * actually the same object as Foo methods have 
    * side effect on Bar methods. If they are not 
    * the same object, a third argument with false 
    * value need to be supplied. 
    */ 
    FooBarBallContainer(Foo* realFoo, Bar* realBar, bool sameObject=true) : 
    _realFoo(realFoo), _realBar(realBar), _sameObject(sameObject) {} 

    virtual void roll() { 
     // roll makes use of FooBar methods 
     _realBar->doBar(); 
     _realFoo->complexFoo(); 
    } 

    virtual Foo* toFoo() { 
     return _realFoo; 
    } 

    virtual Bar* toBar() { 
     return _realBar; 
    } 

    virtual ~FooBarBallContainer() { 
     delete _realFoo; 

     // Check if realFoo and realBar are 
     // not the same object to avoid deleting 
     // it twice. 
     if(!_sameObject) { 
      delete _realBar; 
     } 
    } 

    private: 
    Foo* _realFoo; 
    Bar* _realBar; 
    bool _sameObject; 
}; 


/* Monolithic Implmentation */ 

class FooBarBallImpl_B : public FooBarBall, 
    public Foo, public Bar { 

    public: 
    virtual void roll() { 
     complicatedBar(); 
     doFoo(); 
    } 

    virtual Foo* toFoo() { 
     return (Foo*) this; 
    } 

    virtual Bar* toBar() { 
     return (Bar*) this; 
    } 

    virtual void doFoo() { 
     printf("FooBarBallImpl_B::doFoo()\n"); 
    } 

    virtual void complexFoo() { 
     printf("FooBarBallImpl_B::complexFoo()\n"); 
    } 

    virtual void doBar() { 
     printf("FooBarBallImpl_B::doBar()\n"); 
    } 

    virtual void complicatedBar() { 
     printf("FooBarBallImpl_B::complicatedBar()\n"); 
    } 

}; 

/* Example usage of FooBarBall */ 
void processFooBarBall(FooBarBall *ball) { 

    Foo *foo = ball->toFoo(); 
    foo->doFoo(); 

    ball->roll(); 

    Bar *bar = ball->toBar(); 
    bar->complicatedBar(); 
} 

main() { 

    FooBarImpl_A *fooBar = new FooBarImpl_A(); 
    FooBarBall *container = new FooBarBallContainer(fooBar, fooBar); 

    printf 
    processFooBarBall(container); 
    delete container; 

    FooBarBallImpl_B *ball = new FooBarBallImpl_B(); 
    processFooBarBall(ball); 

    // we can even wrap FooBarBallImpl_B into the container 
    // but the behavior of roll() will become different 
    container = new FooBarBallContainer(ball, ball); 
    processFooBarBall(container); 

    delete container; 

} 
Các vấn đề liên quan