Có một ngôn ngữ hoặc mẫu thiết kế nào cho phép * loại bỏ * các hành vi hoặc thuộc tính của đối tượng trong hệ thống phân cấp lớp không?


28

Một thiếu sót nổi tiếng của hệ thống phân cấp lớp truyền thống là chúng rất tệ khi mô hình hóa thế giới thực. Ví dụ, cố gắng đại diện cho các loài động vật với các lớp. Thực tế có một số vấn đề khi làm điều đó, nhưng một vấn đề mà tôi chưa bao giờ thấy giải pháp là khi một lớp con "mất" một hành vi hoặc tài sản được xác định trong một siêu hạng, giống như một con chim cánh cụt không thể bay (ở đó có lẽ là những ví dụ tốt hơn, nhưng đó là ví dụ đầu tiên xuất hiện trong đầu tôi).

Một mặt, bạn không muốn xác định, đối với mọi thuộc tính và hành vi, một số cờ chỉ định nếu nó có ở tất cả và kiểm tra nó mỗi lần trước khi truy cập vào hành vi hoặc tài sản đó. Bạn chỉ muốn nói rằng chim có thể bay, đơn giản và rõ ràng, trong lớp Chim. Nhưng sau đó, thật tuyệt nếu người ta có thể định nghĩa "ngoại lệ" sau đó, mà không phải sử dụng một số hack khủng khiếp ở khắp mọi nơi. Điều này thường xảy ra khi một hệ thống đã hoạt động được một thời gian. Bạn đột nhiên tìm thấy một "ngoại lệ" hoàn toàn không phù hợp với thiết kế ban đầu và bạn không muốn thay đổi một phần lớn mã của mình để phù hợp với nó.

Vì vậy, có một số ngôn ngữ hoặc mẫu thiết kế có thể xử lý vấn đề này một cách sạch sẽ, mà không yêu cầu thay đổi lớn đối với "siêu hạng" và tất cả các mã sử dụng nó? Ngay cả khi một giải pháp chỉ xử lý một trường hợp cụ thể, một số giải pháp có thể cùng nhau tạo thành một chiến lược hoàn chỉnh.

Sau khi suy nghĩ nhiều hơn, tôi nhận ra mình đã quên Nguyên tắc thay thế Liskov. Đó là lý do tại sao bạn không thể làm điều đó. Giả sử bạn xác định "đặc điểm / giao diện" cho tất cả các "nhóm tính năng" chính, bạn có thể tự do triển khai các đặc điểm trong các nhánh khác nhau của hệ thống phân cấp, như đặc điểm Bay có thể được thực hiện bởi Chim và một số loại sóc và cá đặc biệt.

Vì vậy, câu hỏi của tôi có thể lên tới "Làm thế nào tôi có thể không thực hiện một đặc điểm?" Nếu siêu hạng của bạn là một Tuần tự hóa Java, bạn cũng phải là một, ngay cả khi không có cách nào để bạn tuần tự hóa trạng thái của mình, ví dụ nếu bạn có chứa "Ổ cắm".

Một cách để làm điều đó là luôn xác định tất cả các đặc điểm của bạn theo cặp ngay từ đầu: Bay và Không theo dõi (sẽ ném UnsupportedOperationException, nếu không được kiểm tra). Không đặc điểm sẽ không xác định bất kỳ giao diện mới nào và có thể được kiểm tra đơn giản. Âm thanh như một giải pháp "giá rẻ", đặc biệt nếu được sử dụng từ đầu.


3
'mà không phải sử dụng một số hack khủng khiếp ở khắp mọi nơi': vô hiệu hóa một hành vi là một vụ hack khủng khiếp: nó sẽ ám chỉ điều đó function save_yourself_from_crashing_airplane(Bird b) { f.fly() }sẽ phức tạp hơn rất nhiều. (như Peter Török đã nói, nó vi phạm LSP)
keppla

Một sự kết hợp giữa mẫu Chiến lược và kế thừa có thể cho phép bạn "soạn thảo" hành vi được kế thừa cho các loại siêu cụ thể? Khi bạn nói: " it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"bạn có xem xét một phương pháp nhà máy kiểm soát hành vi hacky không?
StuperUser

1
Một người tất nhiên có thể chỉ cần ném một NotSupportedExceptiontừ Penguin.fly().
Felix Dombek

Theo như ngôn ngữ, bạn chắc chắn có thể hủy triển khai một phương thức trong một lớp con. Chẳng hạn, trong Ruby : class Penguin < Bird; undef fly; end;. Cho dù bạn nên là một câu hỏi khác.
Nathan Long

Điều này sẽ phá vỡ nguyên tắc liskov và được cho là toàn bộ quan điểm của OOP.
deadalnix

Câu trả lời:


17

Như những người khác đã đề cập, bạn sẽ phải chống lại LSP.

Tuy nhiên, có thể lập luận rằng một lớp con chỉ là một phần mở rộng tùy tiện của một siêu lớp. Đó là một đối tượng mới theo đúng nghĩa của nó và mối quan hệ duy nhất với siêu hạng là nó được sử dụng một nền tảng.

Điều này có thể có ý nghĩa logic, thay vì nói Penguin một con chim. Câu nói của bạn Penguin thừa hưởng một số tập hợp hành vi từ Bird.

Nói chung, các ngôn ngữ động cho phép bạn diễn đạt điều này một cách dễ dàng, một ví dụ sử dụng JavaScript như sau:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

Trong trường hợp cụ thể này, Penguinđang tích cực làm mờ Bird.flyphương thức mà nó kế thừa bằng cách viết một thuộc flytính có giá trị undefinedcho đối tượng.

Bây giờ bạn có thể nói rằng Penguinkhông thể được coi là bình thường Birdnữa. Nhưng như đã đề cập, trong thế giới thực, nó đơn giản là không thể. Bởi vì chúng tôi đang làm người mẫu Birdnhư một thực thể bay.

Thay thế là không đưa ra giả định rộng rãi rằng Bird's có thể bay. Sẽ là hợp lý nếu có một sự Birdtrừu tượng cho phép tất cả các loài chim được thừa hưởng từ nó, mà không thất bại. Điều này có nghĩa là chỉ đưa ra các giả định mà tất cả các lớp con có thể giữ.

Nói chung, ý tưởng của Mixin áp dụng độc đáo ở đây. Có một lớp cơ sở rất mỏng, và trộn tất cả các hành vi khác vào nó.

Thí dụ:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Nếu bạn tò mò, tôi có một triển khaiObject.make

Thêm vào:

Vì vậy, câu hỏi của tôi có thể lên tới "Làm thế nào tôi có thể không thực hiện một đặc điểm?" Nếu siêu hạng của bạn là một Tuần tự hóa Java, bạn cũng phải là một, ngay cả khi không có cách nào để bạn tuần tự hóa trạng thái của mình, ví dụ nếu bạn có chứa "Ổ cắm".

Bạn không "không thực hiện" một đặc điểm. Bạn chỉ cần sửa chữa hierachy thừa kế của bạn. Hoặc bạn có thể thực hiện hợp đồng siêu lớp của mình hoặc bạn không nên giả vờ bạn thuộc loại đó.

Đây là nơi thành phần đối tượng tỏa sáng.

Bên cạnh đó, serializable không có nghĩa là mọi thứ nên được tuần tự hóa, nó chỉ có nghĩa là "trạng thái bạn quan tâm" nên được nối tiếp.

Bạn không nên sử dụng đặc điểm "NotX". Đó chỉ là sự phình to mã khủng khiếp. Nếu một chức năng mong đợi một vật thể bay, nó sẽ sụp đổ và cháy khi bạn cho nó một con voi ma mút.


10
"trong thế giới thực, nó đơn giản là không thể." Vâng, nó có thể. Một con chim cánh cụt là một con chim. Khả năng bay không phải là một tài sản của chim, nó chỉ đơn giản là một tài sản ngẫu nhiên của hầu hết các loài chim. Các thuộc tính xác định các loài chim là "lông vũ, có cánh, hai chân, động vật nhiệt, động vật có trứng, động vật có xương sống" (Wikipedia) - không có gì về việc bay ở đó.
pdr

2
@pdr một lần nữa nó phụ thuộc vào định nghĩa của bạn về chim. Khi tôi đang sử dụng thuật ngữ "Bird", tôi có nghĩa là sự trừu tượng của lớp mà chúng ta sử dụng để đại diện cho các loài chim bao gồm cả phương pháp bay. Tôi cũng đề cập rằng bạn có thể làm cho lớp trừu tượng của bạn ít cụ thể hơn. Ngoài ra một con chim cánh cụt không có lông.
Raynos

2
@Raynos: Chim cánh cụt thực sự có lông. Lông của chúng khá ngắn và rậm, tất nhiên.
Jon Purdy

@JonPurdy đủ công bằng, tôi luôn tưởng tượng họ có lông.
Raynos

+1 nói chung và đặc biệt cho "voi ma mút". LOL!
Sebastien Diot

28

AFAIK tất cả các ngôn ngữ dựa trên kế thừa được xây dựng trên Nguyên tắc thay thế Liskov . Xóa / vô hiệu hóa một thuộc tính lớp cơ sở trong một lớp con rõ ràng sẽ vi phạm LSP, vì vậy tôi không nghĩ rằng khả năng đó được thực hiện ở bất cứ đâu. Thế giới thực rất lộn xộn và không thể được mô hình hóa chính xác bằng những trừu tượng toán học.

Một số ngôn ngữ cung cấp các đặc điểm hoặc mixin, chính xác để xử lý các vấn đề như vậy một cách linh hoạt hơn.


1
LSP dành cho các loại , không dành cho các lớp .
Jörg W Mittag

2
@ PéterTörök: Câu hỏi này sẽ không tồn tại nếu không :-) Tôi có thể nghĩ về hai ví dụ từ Ruby. Classlà một lớp con của Modulemặc dù ClassIS-KHÔNG-A Module. Nhưng nó vẫn có ý nghĩa là một lớp con, vì nó sử dụng lại rất nhiều mã. OTOH, StringIOIS-A IO, nhưng cả hai không có bất kỳ mối quan hệ thừa kế nào (tất nhiên ngoài việc cả hai đều được thừa kế từ Object, tất nhiên), vì họ không chia sẻ bất kỳ mã nào. Các lớp học để chia sẻ mã, các loại để mô tả các giao thức. IOStringIOcó cùng một giao thức, do đó cùng loại, nhưng các lớp của chúng không liên quan.
Jörg W Mittag

1
@ JörgWMittag, OK, bây giờ tôi hiểu rõ hơn ý của bạn. Tuy nhiên, với tôi ví dụ đầu tiên của bạn nghe có vẻ giống như lạm dụng quyền thừa kế hơn là biểu hiện của một số vấn đề cơ bản mà bạn dường như đề xuất. IMO kế thừa công cộng không nên được sử dụng để sử dụng lại việc thực hiện, chỉ để thể hiện các mối quan hệ phụ (is-a). Và thực tế là nó có thể bị lạm dụng không đủ tiêu chuẩn - tôi không thể tưởng tượng bất kỳ công cụ có thể sử dụng nào từ bất kỳ miền nào không thể sử dụng sai.
Péter Török

2
Để mọi người nâng cao câu trả lời này: lưu ý rằng điều này không thực sự trả lời câu hỏi, đặc biệt là sau khi làm rõ chỉnh sửa. Tôi không nghĩ câu trả lời này xứng đáng được đánh giá thấp, bởi vì những gì anh ấy nói là rất đúng và quan trọng cần biết, nhưng anh ấy đã không thực sự trả lời câu hỏi.
jhocking

1
Hãy tưởng tượng một Java trong đó chỉ có các giao diện là các loại, các lớp không và các lớp con có thể "không thực hiện" các giao diện của siêu lớp của chúng và tôi nghĩ rằng bạn có một ý tưởng sơ bộ.
Jörg W Mittag

15

Fly()là trong ví dụ đầu tiên trong: Các mẫu thiết kế đầu tiên cho Mẫu chiến lược và đây là một tình huống tốt về lý do tại sao bạn nên "Ưu tiên sáng tác hơn kế thừa." .

Bạn có thể kết hợp thành phần và kế thừa bằng cách có các siêu kiểu FlyingBird, FlightlessBirdcó hành vi đúng được Nhà máy đưa vào, rằng các kiểu con có liên quan, ví dụ như Penguin : FlightlessBirdtự động, và tất cả mọi thứ khác thực sự được Nhà máy xử lý là chuyện đương nhiên.


1
Tôi đã đề cập đến mẫu Trang trí trong câu trả lời của mình, nhưng mẫu Chiến lược cũng hoạt động khá tốt.
jhocking

1
+1 cho "Thành phần ưu tiên hơn thừa kế." Tuy nhiên, sự cần thiết của các mẫu thiết kế đặc biệt để triển khai bố cục trong các ngôn ngữ được nhập tĩnh giúp củng cố sự thiên vị của tôi đối với các ngôn ngữ động như Ruby.
Roy Tinker

11

Không phải vấn đề thực sự mà bạn cho là BirdFlyphương pháp sao? Tại sao không:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Bây giờ vấn đề rõ ràng là nhiều kế thừa ( Duck), vì vậy điều bạn thực sự cần là các giao diện:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}

3
Vấn đề là sự tiến hóa không tuân theo Nguyên tắc thay thế Liskov và kế thừa với việc loại bỏ tính năng.
Donal Fellows

7

Đầu tiên, CÓ, bất kỳ ngôn ngữ nào cho phép sửa đổi đối tượng dễ dàng sẽ cho phép bạn làm điều đó. Trong Ruby, ví dụ, bạn có thể dễ dàng loại bỏ một phương thức.

Nhưng như Péter Török nói, nó sẽ vi phạm LSP .


Trong phần này, tôi sẽ quên LSP và cho rằng:

  • Bird là một lớp với phương thức fly ()
  • Chim cánh cụt phải thừa hưởng từ Bird
  • Chim cánh cụt không thể bay ()
  • Tôi không quan tâm nếu nó là một thiết kế tốt hoặc nếu nó phù hợp với thế giới thực, vì đó là ví dụ được cung cấp trong câu hỏi này.

Bạn đã nói :

Một mặt, bạn không muốn xác định cho mọi thuộc tính và hành vi một số cờ chỉ định nếu nó có ở tất cả và kiểm tra nó mỗi lần trước khi truy cập vào hành vi hoặc thuộc tính đó

Dường như điều bạn muốn là " yêu cầu sự tha thứ thay vì sự cho phép " của Python

Chỉ cần làm cho Penguin của bạn ném một ngoại lệ hoặc kế thừa từ một lớp NonFellingBird sẽ ném một ngoại lệ (mã giả):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

Nhân tiện, bất cứ điều gì bạn chọn: đưa ra một ngoại lệ hoặc xóa một phương thức, cuối cùng, đoạn mã sau (giả sử rằng ngôn ngữ của bạn hỗ trợ loại bỏ phương thức):

var bird:Bird = new Penguin();
bird.fly();

sẽ ném một ngoại lệ thời gian chạy.


"Chỉ cần làm cho Chim cánh cụt của bạn ném một ngoại lệ hoặc thừa kế từ một lớp NonFellingBird sẽ ném một ngoại lệ" Điều đó vẫn vi phạm LSP. Nó vẫn gợi ý rằng một chú chim cánh cụt có thể bay, mặc dù việc thực hiện bay của nó là thất bại. Không bao giờ nên có một phương pháp bay trên Penguin.
pdr

@pdr: không có nghĩa là chim cánh cụt có thể bay, nhưng nó sẽ bay (đó là hợp đồng). Ngoại lệ sẽ cho bạn biết rằng nó không thể . Nhân tiện, tôi không khẳng định đó là một thực hành OOP tốt, tôi chỉ đưa ra câu trả lời cho một phần của câu hỏi
David

Điểm đáng chú ý là chim cánh cụt không nên bay chỉ vì nó là Chim. Nếu tôi muốn viết mã có nội dung "Nếu x có thể bay, hãy làm điều này; người khác làm điều đó." Tôi phải sử dụng thử / bắt trong phiên bản của bạn, nơi tôi chỉ có thể hỏi đối tượng nếu nó có thể bay (phương thức đúc hoặc kiểm tra tồn tại). Nó có thể chỉ là từ ngữ, nhưng câu trả lời của bạn ngụ ý rằng việc ném một ngoại lệ là tuân thủ LSP.
pdr

@pdr "Tôi phải sử dụng thử / bắt trong phiên bản của bạn" -> đó là toàn bộ vấn đề xin tha thứ thay vì sự cho phép (vì ngay cả một con Vịt cũng có thể bị gãy cánh và không thể bay). Tôi sẽ sửa từ ngữ.
David

"đó là toàn bộ vấn đề yêu cầu sự tha thứ hơn là sự cho phép." Có, ngoại trừ việc nó cho phép khung công tác ném cùng một loại ngoại lệ cho bất kỳ phương thức bị thiếu nào, do đó, "try: trừ AttributionError:" của Python tương đương chính xác với C # 's "if (X là Y) {} khác {}" và có thể nhận ra ngay lập tức như vậy. Nhưng nếu bạn cố tình ném một UnknownFlyException để ghi đè chức năng fly () mặc định trong Bird thì nó sẽ trở nên ít nhận ra hơn.
pdr

7

Như ai đó đã chỉ ra ở trên trong các bình luận, chim cánh cụt là chim, chim cánh cụt không bay, ergo không phải con chim nào cũng có thể bay.

Vì vậy, Bird.fly () không nên tồn tại hoặc được phép không hoạt động. Tôi thích cái trước.

Có FlyingBird kéo dài Bird có phương thức .fly () sẽ đúng, tất nhiên.


Tôi đồng ý, Fly nên là một giao diện mà chim có thể thực hiện. Nó có thể được thực hiện như một phương thức cũng như với hành vi mặc định có thể được ghi đè, nhưng một cách tiếp cận sạch hơn là sử dụng một giao diện.
Jon Raynor

6

Vấn đề thực sự với ví dụ fly () là đầu vào và đầu ra của hoạt động không được xác định đúng. Những gì cần thiết cho một con chim bay? Và điều gì xảy ra sau khi bay thành công? Các kiểu tham số và kiểu trả về cho hàm fly () phải có thông tin đó. Nếu không, thiết kế của bạn phụ thuộc vào tác dụng phụ ngẫu nhiên và bất cứ điều gì có thể xảy ra. Phần bất cứ điều gì là nguyên nhân gây ra toàn bộ vấn đề, giao diện không được xác định đúng và tất cả các loại thực hiện được cho phép.

Vì vậy, thay vì điều này:

class Bird {
public:
   virtual void fly()=0;
};

Cậu nên có vài thứ như thế này:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

Bây giờ nó xác định rõ ràng các giới hạn của chức năng - hành vi bay của bạn chỉ có một lần nổi duy nhất để quyết định - khoảng cách từ mặt đất, khi được đưa ra vị trí. Bây giờ toàn bộ vấn đề tự động giải quyết. Một con chim không thể bay chỉ cần trả về 0,0 từ chức năng đó, nó không bao giờ rời khỏi mặt đất. Đó là hành vi đúng cho điều đó, và một khi quyết định thả nổi đó, bạn biết rằng bạn đã thực hiện đầy đủ giao diện.

Hành vi thực tế có thể khó mã hóa thành các loại, nhưng đó là cách duy nhất để xác định đúng giao diện của bạn.

Chỉnh sửa: Tôi muốn làm rõ một khía cạnh. Phiên bản float-> float này của hàm fly () cũng quan trọng vì nó xác định đường dẫn. Phiên bản này có nghĩa là một con chim không thể tự nhân đôi một cách kỳ diệu khi nó đang bay. Đây là lý do tại sao tham số là một lần nổi - đó là vị trí trong con đường mà con chim đi. Nếu bạn muốn các đường dẫn phức tạp hơn, thì pos2path Point2d (float x); trong đó sử dụng cùng x với hàm fly ().


1
Tôi khá thích câu trả lời của bạn. Tôi nghĩ rằng nó xứng đáng nhiều phiếu hơn.
Sebastien Diot

2
Câu trả lời tuyệt vời. Vấn đề là câu hỏi chỉ vẫy tay như những gì fly () thực sự làm. Bất kỳ triển khai thực sự nào của ruồi đều sẽ có, ít nhất là một đích đến - bay (đích phối hợp) mà trong trường hợp của chim cánh cụt, có thể bị ghi đè để thực hiện {return currentPocation)}
Chris Cudmore

4

Về mặt kỹ thuật, bạn có thể thực hiện điều này bằng bất kỳ ngôn ngữ gõ động / vịt nào (JavaScript, Ruby, Lua, v.v.) nhưng nó hầu như luôn là một ý tưởng thực sự tồi. Loại bỏ các phương thức khỏi một lớp là một cơn ác mộng bảo trì, giống như sử dụng các biến toàn cục (nghĩa là bạn không thể nói trong một mô-đun rằng trạng thái toàn cầu chưa được sửa đổi ở nơi khác).

Các mô hình tốt cho vấn đề bạn mô tả là Trang trí hoặc Chiến lược, thiết kế kiến ​​trúc thành phần. Về cơ bản, thay vì loại bỏ các hành vi không cần thiết khỏi các lớp con, bạn xây dựng các đối tượng bằng cách thêm các hành vi cần thiết. Vì vậy, để xây dựng hầu hết các loài chim bạn sẽ thêm thành phần bay, nhưng đừng thêm thành phần đó vào chim cánh cụt của bạn.


3

Peter đã đề cập đến Nguyên tắc thay thế Liskov, nhưng tôi cảm thấy cần phải giải thích.

Đặt q (x) là một thuộc tính có thể chứng minh được về các đối tượng x thuộc loại T. Sau đó, q (y) có thể chứng minh được đối với các đối tượng y loại S trong đó S là một kiểu con của T.

Do đó, nếu một Bird (đối tượng x loại T) có thể bay (q (x)) thì Penguin (đối tượng y loại S) có thể bay (q (y)), theo định nghĩa. Nhưng đó rõ ràng không phải là trường hợp. Ngoài ra còn có những sinh vật khác có thể bay nhưng không thuộc loại Bird.

Làm thế nào bạn đối phó với điều này phụ thuộc vào ngôn ngữ. Nếu một ngôn ngữ hỗ trợ nhiều kế thừa thì bạn nên sử dụng một lớp trừu tượng cho các sinh vật có thể bay; nếu một ngôn ngữ thích các giao diện thì đó là giải pháp (và việc thực hiện bay nên được gói gọn hơn là kế thừa); hoặc, nếu một ngôn ngữ hỗ trợ Duck Typing (không có ý định chơi chữ) thì bạn chỉ có thể thực hiện một phương thức bay trên các lớp có thể và gọi nó nếu nó ở đó.

Nhưng mọi thuộc tính của siêu lớp nên áp dụng cho tất cả các lớp con của nó.

[Đáp lại để chỉnh sửa]

Áp dụng một "đặc điểm" của CanFly cho Bird là không tốt hơn. Nó vẫn gợi ý để gọi mã rằng tất cả các loài chim có thể bay.

Một đặc điểm trong các điều khoản bạn xác định đó chính xác là ý nghĩa của Liskov khi cô nói "tài sản".


2

Hãy để tôi bắt đầu bằng cách đề cập (như mọi người khác) Nguyên tắc thay thế Liskov, giải thích lý do tại sao bạn không nên làm điều này. Tuy nhiên, vấn đề của những gì bạn nên làm là một trong thiết kế. Trong một số trường hợp, Penguin có thể không thực sự bay. Có lẽ bạn có thể yêu cầu Penguin ném Insu enoughWingsException khi được yêu cầu bay, miễn là bạn rõ ràng trong tài liệu của Bird :: fly () rằng nó có thể ném nó cho những con chim không thể bay. Có một thử nghiệm để xem nếu nó thực sự có thể bay, mặc dù điều đó làm mờ giao diện.

Thay thế là cơ cấu lại các lớp học của bạn. Hãy tạo lớp "FlyingCreature" (hoặc tốt hơn là một giao diện, nếu bạn đang xử lý ngôn ngữ cho phép nó). "Bird" không kế thừa từ FlyingCreature, nhưng bạn có thể tạo "FlyingBird". Lark, Kền kền và Đại bàng đều thừa hưởng từ FlyingBird. Chim cánh cụt không. Nó chỉ thừa hưởng từ Bird.

Nó phức tạp hơn một chút so với cấu trúc ngây thơ, nhưng nó có lợi thế là chính xác. Bạn sẽ lưu ý rằng tất cả các lớp dự kiến ​​đều ở đó (Bird) và người dùng thường có thể bỏ qua các lớp 'được phát minh' (FlyingCreature) nếu việc sinh vật của bạn có thể bay hay không không quan trọng.


0

Cách điển hình để xử lý một tình huống như vậy là ném một cái gì đó giống như một sự tôn trọng UnsupportedOperationException(Java). NotImplementedException(C #).


Miễn là bạn ghi lại khả năng này trong Bird.
DJClayworth

0

Nhiều câu trả lời hay với nhiều bình luận, nhưng tất cả đều không đồng ý và tôi chỉ có thể chọn một câu duy nhất, vì vậy tôi sẽ tóm tắt ở đây tất cả các quan điểm tôi đồng ý.

0) Đừng giả sử "gõ tĩnh" (Tôi đã làm khi tôi hỏi, vì tôi hầu như chỉ làm Java). Về cơ bản, vấn đề rất phụ thuộc vào loại ngôn ngữ mà người ta sử dụng.

1) Người ta nên tách hệ thống phân cấp loại khỏi hệ thống phân cấp tái sử dụng mã trong thiết kế và trong đầu của một người, ngay cả khi chúng chủ yếu trùng nhau. Nói chung, sử dụng các lớp để tái sử dụng và giao diện cho các loại.

2) Lý do tại sao bình thường Bird IS-A Fly là vì hầu hết các loài chim đều có thể bay, do đó, nó thực tế theo quan điểm tái sử dụng mã, nhưng nói rằng Bird IS-A Fly thực sự sai vì có ít nhất một ngoại lệ (Chim cánh cụt).

3) Trong cả hai ngôn ngữ tĩnh và động, bạn chỉ có thể ném một ngoại lệ. Nhưng điều này chỉ nên được sử dụng nếu nó được tuyên bố rõ ràng trong "hợp đồng" của lớp / giao diện khai báo chức năng, nếu không thì đó là "vi phạm hợp đồng". Điều đó cũng có nghĩa là bây giờ bạn phải chuẩn bị để bắt ngoại lệ ở mọi nơi, vì vậy bạn viết thêm mã tại trang web cuộc gọi và đó là mã xấu.

4) Trong một số ngôn ngữ động, thực sự có thể "loại bỏ / ẩn" chức năng của một siêu hạng. Nếu kiểm tra sự hiện diện của chức năng là cách bạn kiểm tra "IS-A" trong ngôn ngữ đó, thì đây là một giải pháp đầy đủ và hợp lý. Mặt khác, hoạt động "IS-A" là một thứ khác vẫn nói rằng đối tượng của bạn "nên" thực hiện chức năng hiện đang thiếu, thì mã gọi của bạn sẽ cho rằng chức năng này hiện diện và gọi nó và gặp sự cố, vì vậy đó là số tiền để ném một ngoại lệ.

5) Cách thay thế tốt hơn là thực sự tách tính trạng Fly ra khỏi tính trạng Bird. Vì vậy, một con chim bay phải mở rộng / thực hiện rõ ràng cả Chim và Bay / Bay. Đây có lẽ là thiết kế sạch nhất, vì bạn không phải "loại bỏ" bất cứ thứ gì. Một nhược điểm hiện nay là hầu như mọi con chim đều phải thực hiện cả Bird và Fly, vì vậy bạn viết thêm mã. Cách xung quanh là có một lớp trung gian FlyingBird, thực hiện cả Bird và Fly, và đại diện cho trường hợp phổ biến, nhưng công việc này có thể được sử dụng hạn chế mà không cần thừa kế nhiều lần.

6) Một lựa chọn khác không yêu cầu nhiều kế thừa là sử dụng thành phần thay vì kế thừa. Mỗi khía cạnh của một con vật được mô hình hóa bởi một lớp độc lập và Chim cụ thể là một thành phần của Chim, và có thể là Bay hoặc Bơi, ... Bạn có thể sử dụng lại mã đầy đủ, nhưng phải thực hiện một hoặc nhiều bước bổ sung để có được chức năng Bay, khi bạn có một tài liệu tham khảo về một chú chim cụ thể. Ngoài ra, ngôn ngữ tự nhiên "đối tượng IS-A Fly" và "đối tượng AS-A (diễn viên) Fly" sẽ không hoạt động nữa, vì vậy bạn phải phát minh ra cú pháp của riêng bạn (một số ngôn ngữ động có thể có cách này). Điều này có thể làm cho mã của bạn cồng kềnh hơn.

7) Xác định đặc điểm bay của bạn sao cho nó có lối thoát rõ ràng cho thứ không thể bay. Fly.getNumberOfWings () có thể trả về 0. Nếu Fly.fly (hướng, currentPotinion) sẽ trả lại vị trí mới sau chuyến bay, thì Penguin.fly () chỉ có thể trả về currentPocation mà không thay đổi. Bạn có thể kết thúc với mã hoạt động về mặt kỹ thuật, nhưng có một số cảnh báo. Thứ nhất, một số mã có thể không có hành vi "không làm gì" rõ ràng. Ngoài ra, nếu ai đó gọi x.fly (), họ sẽ mong đợi nó làm điều gì đó , ngay cả khi nhận xét nói fly () được phép không làm gì cả . Cuối cùng, chim cánh cụt IS-A Flying vẫn sẽ trở lại thành sự thật, điều này có thể gây nhầm lẫn cho lập trình viên.

8) Làm như 5), nhưng sử dụng thành phần để giải quyết các trường hợp cần nhiều kế thừa. Đây là tùy chọn tôi thích cho một ngôn ngữ tĩnh, vì 6) có vẻ cồng kềnh hơn (và có lẽ cần nhiều bộ nhớ hơn vì chúng ta có nhiều đối tượng hơn). Một ngôn ngữ động có thể làm cho 6) ít cồng kềnh hơn, nhưng tôi nghi ngờ nó sẽ trở nên ít cồng kềnh hơn 5).


0

Xác định một hành vi mặc định (đánh dấu nó là ảo) trong lớp cơ sở và ghi đè nó là cần thiết. Bằng cách đó, mọi con chim đều có thể "bay".

Ngay cả chim cánh cụt cũng bay, nó lướt trên băng ở độ cao không!

Hành vi bay có thể được ghi đè khi cần thiết.

Một khả năng khác là có Giao diện Fly. Không phải tất cả các loài chim sẽ thực hiện giao diện đó.

class eagle : bird, IFly
class penguin : bird

Các thuộc tính không thể được loại bỏ, vì vậy đó là lý do tại sao điều quan trọng là phải biết các thuộc tính nào là phổ biến trên tất cả các loài chim. Tôi nghĩ rằng đó là một vấn đề thiết kế để đảm bảo các thuộc tính chung được thực hiện ở cấp cơ sở.


-1

Tôi nghĩ rằng mô hình bạn đang tìm kiếm là đa hình cũ tốt. Mặc dù bạn có thể xóa giao diện khỏi một lớp bằng một số ngôn ngữ, nhưng có lẽ đó không phải là ý kiến ​​hay vì những lý do được đưa ra bởi Péter Török. Tuy nhiên, trong bất kỳ ngôn ngữ OO nào, bạn có thể ghi đè một phương thức để thay đổi hành vi của nó và điều đó bao gồm không làm gì cả. Để mượn ví dụ của bạn, bạn có thể cung cấp phương thức Penguin :: fly () thực hiện bất kỳ thao tác nào sau đây:

  • không có gì
  • ném một ngoại lệ
  • thay vào đó gọi phương thức Penguin :: bơi ()
  • khẳng định rằng chim cánh cụt đang ở dưới nước (chúng thực hiện kiểu "bay" qua nước)

Các thuộc tính có thể dễ dàng hơn để thêm và xóa nếu bạn có kế hoạch trước. Bạn có thể lưu trữ các thuộc tính trong một mảng bản đồ / từ điển / liên kết thay vì sử dụng các biến thể hiện. Bạn có thể sử dụng mẫu Factory để tạo các phiên bản tiêu chuẩn của các cấu trúc như vậy, do đó, Bird đến từ BirdFactory sẽ luôn bắt đầu với cùng một tập các thuộc tính. Mã hóa giá trị chính của Objective-C là một ví dụ điển hình cho loại điều này.

Lưu ý: Bài học nghiêm túc từ các bình luận bên dưới là trong khi ghi đè để loại bỏ một hành vi có thể hoạt động, nó không phải luôn luôn là giải pháp tốt nhất. Nếu bạn thấy mình cần phải làm điều này theo bất kỳ cách quan trọng nào, bạn nên xem xét rằng một tín hiệu mạnh mẽ cho thấy biểu đồ thừa kế của bạn là thiếu sót. Không phải lúc nào cũng có thể cấu trúc lại các lớp mà bạn kế thừa, nhưng khi đó thường là giải pháp tốt hơn.

Sử dụng ví dụ Penguin của bạn, một cách để tái cấu trúc sẽ là tách khả năng bay khỏi lớp Bird. Vì không phải tất cả các loài chim đều có thể bay, bao gồm cả phương thức fly () trong Bird là không phù hợp và dẫn trực tiếp đến loại vấn đề bạn đang hỏi về. Vì vậy, hãy di chuyển phương thức fly () (và có lẽ cất cánh () và hạ cánh ()) sang một lớp Aviator hoặc giao diện (tùy thuộc vào ngôn ngữ). Điều này cho phép bạn tạo một lớp FlyingBird kế thừa từ cả Bird và Aviator (hoặc kế thừa từ Bird và thực hiện Aviator). Chim cánh cụt có thể tiếp tục kế thừa trực tiếp từ Bird nhưng không phải Aviator, do đó tránh được vấn đề. Cách sắp xếp như vậy cũng có thể giúp bạn dễ dàng tạo các lớp cho các vật thể bay khác: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect, v.v.


2
-1 cho thậm chí đề nghị gọi Penguin :: bơi (). Điều đó vi phạm nguyên tắc ít gây ngạc nhiên nhất và sẽ khiến các lập trình viên bảo trì ở khắp mọi nơi nguyền rủa tên của bạn.
DJClayworth

1
@DJClayworth Như ví dụ về mặt lố bịch ở nơi đầu tiên, việc hạ thấp vi phạm hành vi suy luận của ruồi () và bơi () có vẻ hơi nhiều. Nhưng nếu bạn thực sự muốn xem xét vấn đề này một cách nghiêm túc, tôi đồng ý rằng nhiều khả năng bạn sẽ đi theo con đường khác và thực hiện bơi () về mặt bay (). Vịt bơi bằng cách chèo chân; chim cánh cụt bơi bằng cách vỗ cánh.
Caleb

1
Tôi đồng ý câu hỏi thật ngớ ngẩn, nhưng rắc rối là tôi đã thấy mọi người thực hiện điều này trong cuộc sống thực - sử dụng các cuộc gọi hiện tại "không thực sự làm gì" để thực hiện chức năng hiếm. Nó thực sự làm hỏng mã và thường kết thúc bằng việc phải viết "if (! (MyBird instanceof Penguin)) fly ();" ở nhiều nơi, trong khi hy vọng không ai tạo ra một lớp đà điểu.
DJClayworth

Sự khẳng định thậm chí còn tồi tệ hơn. Nếu tôi có một loạt Chim, tất cả đều có phương thức fly (), tôi không muốn một lỗi xác nhận khi tôi gọi fly () trên chúng.
DJClayworth

1
Tôi đã không đọc tài liệu Penguin , bởi vì tôi đã được trao một mảng Chim và tôi không biết Chim cánh cụt sẽ ở trong mảng. Tôi đã đọc tài liệu Bird nói rằng khi tôi gọi fly () thì chim bay. Nếu tài liệu đó đã tuyên bố rõ ràng rằng một ngoại lệ có thể bị ném nếu con chim không bay, tôi sẽ cho phép điều đó. Nếu nó nói rằng việc gọi fly () đôi khi sẽ khiến nó bơi, tôi đã đổi sang sử dụng một thư viện lớp khác. Hoặc đi uống rất lớn.
DJClayworth
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.