Làm thế nào để tránh downcasting?


12

Câu hỏi của tôi là về một trường hợp đặc biệt của Siêu thú.

  1. Tôi Animalcó thể moveForward()eat().
  2. Sealkéo dài Animal.
  3. Dogkéo dài Animal.
  4. Và có một sinh vật đặc biệt cũng Animalđược gọi là Human.
  5. Humanthực hiện cũng là một phương pháp speak()(không được thực hiện bởi Animal).

Khi thực hiện một phương thức trừu tượng chấp nhận Animaltôi muốn sử dụng speak()phương thức đó. Điều đó dường như là không thể nếu không thực hiện một trò hề. Jeremy Miller đã viết trong bài báo của mình rằng một mùi u ám.

Điều gì sẽ là một giải pháp để tránh downcasting trong tình huống này?


6
Sửa mô hình của bạn. Sử dụng hệ thống phân cấp phân loại hiện có để mô hình hóa hệ thống phân cấp lớp thường là ý tưởng sai. Bạn nên lấy các tóm tắt của mình từ mã hiện có, không tạo ra các trừu tượng và sau đó cố gắng khớp mã với nó.
Euphoric

2
Nếu bạn muốn Động vật biết nói, thì khả năng nói là một phần tạo nên thứ gì đó của động vật: artima.com/interfacesesign/PreferPoly.html
JeffO

8
tiến về phía trước? cua thì sao?
Fabio Marcolini

Mục # nào trong Java hiệu quả, BTW?
goldilocks

1
Một số người không thể nói, vì vậy một ngoại lệ sẽ bị ném nếu bạn cố gắng.
Tulains Córdova

Câu trả lời:


12

Nếu bạn có một phương thức cần biết liệu lớp cụ thể có thuộc loại Humanđể làm điều gì đó hay không, thì bạn đang phá vỡ một số nguyên tắc RẮN , đặc biệt:

  • Nguyên tắc mở / đóng - nếu trong tương lai, bạn cần thêm một loại động vật mới có thể nói (ví dụ: một con vẹt) hoặc làm một cái gì đó cụ thể cho loại đó, mã hiện tại của bạn sẽ phải thay đổi
  • Nguyên tắc phân biệt giao diện - có vẻ như bạn đang khái quát quá nhiều. Một động vật có thể bao gồm phổ rộng của các loài.

Theo tôi, nếu phương thức của bạn mong đợi một loại lớp cụ thể, hãy gọi phương thức cụ thể đó, sau đó thay đổi phương thức đó để chỉ chấp nhận lớp đó chứ không phải giao diện.

Một cái gì đó như thế này:

public void MakeItSpeak( Human obj );

và không thích điều này:

public void SpeakIfHuman( Animal obj );

Người ta cũng có thể tạo ra một phương thức trừu tượng trên Animalđược gọi canSpeakvà mỗi triển khai cụ thể phải xác định liệu nó có thể "nói" hay không.
Brandon

Nhưng tôi thích câu trả lời của bạn hơn. Nhiều sự phức tạp đến từ việc cố gắng đối xử với một cái gì đó giống như một cái gì đó không phải là nó.
Brandon

@Brandon Tôi đoán, trong trường hợp đó, nếu nó không thể "nói", thì hãy ném một ngoại lệ. Hoặc không làm gì cả.
BЈовић

Vì vậy, bạn sẽ bị quá tải như thế này: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}public void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber

1
Đi hết con đường - makeItSpeak (Động vật ISpeakingAnimal) - sau đó bạn có thể có ISpeakingAnimal thực hiện Động vật. Bạn cũng có thể có makeItSpeak (Động vật) {nói nếu thể hiện ISpeakingAnimal}, nhưng có mùi khét.
ptyx

6

Vấn đề không phải là bạn đang bị hạ thấp - mà là bạn đang bị hạ thấp Human. Thay vào đó, hãy tạo một giao diện:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

Bằng cách này, điều kiện không phải là con vật Human- điều kiện là nó có thể nói. Điều này có nghĩa là mysteriousMethodcó thể làm việc với các lớp con khác, không phải con người Animalmiễn là họ thực hiện CanSpeak.


trong trường hợp đó, nó nên lấy làm đối số của CanSpeak thay vì Animal
Newtopian

1
@Newtopian Giả sử rằng phương thức đó không cần bất cứ thứ gì từ Animalđó và tất cả người dùng của phương thức đó sẽ giữ đối tượng họ muốn gửi cho nó thông qua CanSpeaktham chiếu loại (hoặc thậm chí Humanloại tham chiếu). Nếu đó là trường hợp, phương pháp đó có thể đã được sử dụng Humanở nơi đầu tiên và chúng tôi sẽ không cần phải giới thiệu CanSpeak.
Idan Arye

Loại bỏ giao diện không được liên kết với tính cụ thể trong chữ ký phương thức, bạn có thể thoát khỏi giao diện nếu và chỉ khi chỉ có Con người và sẽ chỉ là con người "CanSpeak".
Newtopian

1
@Newtopian Lý do chúng tôi giới thiệu CanSpeakở nơi đầu tiên không phải là chúng tôi có một cái gì đó thực hiện nó ( Human) mà là chúng tôi có một cái gì đó sử dụng nó (phương thức). Điểm quan trọng CanSpeaklà tách rời phương thức đó khỏi lớp cụ thể Human. Nếu chúng ta không có phương pháp để đối xử với những thứ CanSpeakkhác biệt, sẽ không có điểm nào để phân biệt những thứ đó CanSpeak. Chúng tôi không tạo giao diện chỉ vì chúng tôi có phương pháp ...
Idan Arye

2

Trong trường hợp này, việc triển khai mặc định speak()trong AbstractAnimallớp sẽ là:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

Tại thời điểm đó, bạn đã có một triển khai mặc định trong lớp Trừu tượng - và nó hoạt động chính xác.

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

Vâng, điều này có nghĩa là bạn đã có các lần thử bắt rải rác trong mã để xử lý mọi vấn đề speak, nhưng thay thế cho việc này là if(thingy is Human)gói tất cả các phát ngôn thay thế.

Ưu điểm của ngoại lệ là nếu bạn có một loại điều khác tại một thời điểm nào đó nói (một con vẹt), bạn sẽ không cần phải thực hiện lại tất cả các bài kiểm tra của mình.


1
Tôi không nghĩ rằng đây là một lý do tốt để sử dụng ngoại lệ (trừ khi đó là mã python).
Bryan Chen

1
Tôi sẽ không bỏ phiếu vì điều này sẽ hiệu quả về mặt kỹ thuật, nhưng tôi thực sự không thích kiểu thiết kế này. Tại sao định nghĩa một phương thức ở cấp độ cha mẹ mà cha mẹ không thể thực hiện? Trừ khi bạn biết loại đối tượng là gì (và nếu bạn đã làm - bạn sẽ không gặp phải vấn đề này), bạn cần luôn luôn sử dụng thử / bắt để kiểm tra ngoại lệ hoàn toàn có thể tránh được. Bạn thậm chí có thể sử dụng một canSpeak()phương pháp để xử lý việc này tốt hơn.
Brandon

2
Tôi đã làm việc trong một dự án có rất nhiều khiếm khuyết bắt nguồn từ quyết định của ai đó để viết lại một loạt các phương pháp làm việc để đưa ra một ngoại lệ vì họ đang sử dụng một triển khai lỗi thời. Anh ta có thể đã sửa lỗi thực hiện, nhưng thay vào đó chọn cách ném ngoại lệ ra mọi nơi. Đương nhiên, chúng tôi đã nhập QA với hàng trăm lỗi. Vì vậy, tôi thiên vị chống lại việc ném ngoại lệ trong các phương pháp không được gọi là. Nếu họ không được gọi, hãy xóa chúng .
Brandon

2
Thay thế cho việc ném một ngoại lệ trong trường hợp này có thể không làm gì cả. Rốt cuộc, họ không thể nói được :)
Bовић

@Brandon có một sự khác biệt giữa việc thiết kế nó ngay từ đầu với ngoại lệ so với trang bị thêm sau này. Người ta cũng có thể nhìn vào một giao diện với một phương thức mặc định trong Java8 hoặc không ném ngoại lệ và chỉ im lặng. Điểm mấu chốt là để tránh cần phải downcast, hàm cần được xác định trong kiểu được truyền.

1

Bạn có thể thêm Giao tiếp với Động vật. Tiếng chó sủa, Con người biết nói, Con dấu .. uhh .. Tôi không biết con dấu làm gì.

Nhưng có vẻ như phương pháp của bạn được thiết kế để nếu (Động vật là con người) Nói ();

Câu hỏi bạn có thể muốn hỏi là, sự thay thế là gì? Thật khó để đưa ra một gợi ý vì tôi không biết chính xác những gì bạn muốn đạt được. Có những tình huống lý thuyết trong đó downcasting / upcasting là cách tiếp cận tốt nhất.


15
Con dấu đi ow ow ow , nhưng con cáo không được xác định.

1

Downcasting đôi khi là cần thiết và thích hợp. Đặc biệt, nó thường thích hợp trong trường hợp một người có các đối tượng có thể có hoặc không có khả năng và người ta muốn sử dụng khả năng đó khi nó tồn tại trong khi xử lý các đối tượng mà không có khả năng đó theo cách mặc định. Như một ví dụ đơn giản, giả sử a Stringđược hỏi liệu nó có bằng một số đối tượng tùy ý khác không. Để một cái Stringbằng nhau String, nó phải kiểm tra độ dài và mảng ký tự lùi của chuỗi khác. Nếu a Stringđược hỏi liệu nó có bằng a hay không Dog, tuy nhiên, nó không thể truy cập vào độ dài của Dognó, nhưng nó không nên; thay vào đó, nếu đối tượng mà một người Stringđược cho là so sánh chính nó không phải là mộtString, so sánh nên sử dụng một hành vi mặc định (báo cáo rằng đối tượng khác không bằng nhau).

Thời điểm khi downcasting nên được coi là đáng ngờ nhất là khi đối tượng được chọn là "được biết" là thuộc loại thích hợp. Nói chung, nếu một đối tượng được biết là a Cat, người ta nên sử dụng một biến kiểu Cat, thay vì biến kiểu Animal, để tham chiếu đến nó. Tuy nhiên, có những lúc điều này không phải lúc nào cũng hoạt động. Ví dụ, một Zoobộ sưu tập có thể giữ các cặp đối tượng trong các khe mảng chẵn / lẻ, với mong muốn các đối tượng trong mỗi cặp sẽ có thể tác động lên nhau, ngay cả khi chúng không thể tác động lên các đối tượng trong các cặp khác. Trong trường hợp như vậy, các đối tượng trong mỗi cặp vẫn sẽ phải chấp nhận một loại tham số không cụ thể để chúng có thể, về mặt cú pháp , được truyền các đối tượng từ bất kỳ cặp nào khác. Vì vậy, ngay cả khi Cat'splayWith(Animal other)phương thức sẽ chỉ hoạt động khi otherlà một Cat, Zoocần phải có thể truyền cho nó một phần tử của một Animal[], vì vậy kiểu tham số của nó sẽ phải Animalthay vì Cat.

Trong trường hợp downcasting là hợp pháp không thể tránh khỏi, người ta nên sử dụng nó mà không cần điều kiện. Câu hỏi quan trọng là xác định khi nào một cách hợp lý có thể tránh bị hạ thấp và tránh nó khi có thể.


Trong trường hợp chuỗi, bạn nên có một phương thức Object.equalToString(String string). Sau đó, bạn có boolean String.equal(Object object) { return object.equalStoString(this); }Vì vậy, không cần downcast: bạn có thể sử dụng điều phối động.
Giorgio

@Giorgio: Công văn động có công dụng của nó, nhưng nói chung thậm chí còn tồi tệ hơn việc hạ thấp.
supercat

cách gửi năng động nói chung thậm chí còn tồi tệ hơn downcasting? tôi mặc dù nó là cách khác
Bryan Chen

@BryanChen: Điều đó phụ thuộc vào thuật ngữ của bạn. Tôi không nghĩ Objectcó bất kỳ equalStoStringphương thức ảo nào và tôi thừa nhận tôi không biết ví dụ được trích dẫn thậm chí sẽ hoạt động như thế nào trong Java, nhưng trong C #, công văn động (khác với công văn ảo) có nghĩa là về cơ bản trình biên dịch có để thực hiện tra cứu tên dựa trên Reflection lần đầu tiên một phương thức được sử dụng trên một lớp, khác với công văn ảo (chỉ đơn giản thực hiện cuộc gọi qua một vị trí trong bảng phương thức ảo được yêu cầu để chứa địa chỉ phương thức hợp lệ).
supercat

Từ quan điểm mô hình hóa, tôi muốn điều phối động nói chung. Nó cũng là cách hướng đối tượng để chọn một thủ tục dựa trên loại (các) tham số đầu vào của nó.
Giorgio

1

Khi thực hiện một phương thức trừu tượng chấp nhận Động vật, tôi muốn sử dụng phương thức speak ().

Bạn có một vài lựa chọn:

  • Sử dụng phản xạ để gọi speak nếu nó tồn tại. Ưu điểm: không phụ thuộc vào Human. Nhược điểm: hiện có một sự phụ thuộc ẩn vào tên "nói".

  • Giới thiệu một giao diện mới Speakervà downcast cho giao diện. Điều này linh hoạt hơn phụ thuộc vào một loại bê tông cụ thể. Nó có nhược điểm là bạn phải sửa đổiHuman để thực hiện Speaker. Điều này sẽ không hoạt động nếu bạn không thể sửa đổiHuman

  • Downcast để Human. Điều này có nhược điểm là bạn sẽ phải sửa đổi mã bất cứ khi nào bạn muốn một lớp con khác nói. Lý tưởng nhất là bạn muốn mở rộng các ứng dụng bằng cách thêm mã mà không cần liên tục quay lại và thay đổi mã cũ.

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.