Có ổn không khi có các đối tượng tự đúc, ngay cả khi nó làm ô nhiễm API của các lớp con của chúng?


33

Tôi có một lớp cơ sở Base,. Nó có hai lớp con, Sub1Sub2. Mỗi lớp con có một số phương thức bổ sung. Ví dụ, Sub1Sandwich makeASandwich(Ingredients... ingredients), và Sub2boolean contactAliens(Frequency onFrequency).

Vì các phương thức này có các tham số khác nhau và thực hiện những điều hoàn toàn khác nhau, nên chúng hoàn toàn không tương thích và tôi không thể chỉ sử dụng đa hình để giải quyết vấn đề này.

Basecung cấp hầu hết các chức năng và tôi có một bộ sưu tập lớn các Baseđối tượng. Tuy nhiên, tất cả Basecác đối tượng là a Sub1hoặc a Sub2, và đôi khi tôi cần biết chúng là gì.

Có vẻ như một ý tưởng tồi để làm như sau:

for (Base base : bases) {
    if (base instanceof Sub1) {
        ((Sub1) base).makeASandwich(getRandomIngredients());
        // ... etc.
    } else { // must be Sub2
        ((Sub2) base).contactAliens(getFrequency());
        // ... etc.
    }
}

Vì vậy, tôi đã đưa ra một chiến lược để tránh điều này mà không cần đúc. Basebây giờ có các phương thức sau:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

Và tất nhiên, Sub1thực hiện các phương pháp này như

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

Sub2thực hiện chúng theo cách ngược lại.

Thật không may, bây giờ Sub1Sub2có các phương thức này trong API riêng của họ. Vì vậy, tôi có thể làm điều này, ví dụ, trên Sub1.

/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1();   { return this; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2();   { throw new IllegalStateException(); }

Theo cách này, nếu đối tượng chỉ được biết là a Base, các phương thức này không bị phản đối và có thể được sử dụng để "tự" chuyển sang một loại khác để tôi có thể gọi các phương thức của lớp con trên nó. Theo một cách nào đó, điều này có vẻ thanh lịch đối với tôi, nhưng mặt khác, tôi lại lạm dụng các chú thích không được chấp nhận như một cách để "loại bỏ" các phương thức khỏi một lớp.

Vì một Sub1thể hiện thực sự là một Base, nên sử dụng tính kế thừa thay vì đóng gói. Là những gì tôi đang làm tốt? Có cách nào tốt hơn để giải quyết vấn đề này?


12
Cả 3 lớp giờ phải biết về nhau. Thêm Sub3 sẽ liên quan đến rất nhiều thay đổi mã và thêm Sub10 sẽ hết sức đau đớn
Dan Pichelman

15
Nó sẽ giúp rất nhiều nếu bạn cho chúng tôi mã thật. Có những tình huống phù hợp để đưa ra quyết định dựa trên loại cụ thể của một thứ gì đó, nhưng không thể biết liệu bạn có hợp lý trong những gì bạn đang làm với các ví dụ giả định như vậy không. Đối với bất cứ điều gì nó có giá trị, những gì bạn muốn là một Khách truy cập hoặc liên minh được gắn thẻ .
Doval

1
Xin lỗi, tôi nghĩ rằng nó sẽ làm cho mọi thứ dễ dàng hơn để đưa ra các ví dụ đơn giản. Có lẽ tôi sẽ đăng câu hỏi mới với phạm vi rộng hơn về những gì tôi muốn làm.
codebreaker

12
Bạn chỉ đang thực hiện việc truyền và instanceof, theo cách đòi hỏi nhiều thao tác gõ, dễ bị lỗi và khiến cho việc thêm nhiều lớp con trở nên khó khăn hơn.
dùng253751

5
Nếu Sub1Sub2không thể được sử dụng thay thế cho nhau, thì tại sao bạn lại đối xử với chúng như vậy? Tại sao không theo dõi riêng các 'nhà sản xuất bánh sandwich' và 'người tiếp xúc với người ngoài hành tinh' của bạn?
Pieter Witvoet

Câu trả lời:


27

Không phải lúc nào cũng có nghĩa khi thêm các hàm vào lớp cơ sở, như được đề xuất trong một số câu trả lời khác. Thêm quá nhiều hàm trường hợp đặc biệt có thể dẫn đến ràng buộc các thành phần không liên quan với nhau.

Ví dụ tôi có thể có một Animallớp, với CatDogcác thành phần. Nếu tôi muốn có thể in chúng hoặc hiển thị chúng trong GUI, có thể tôi sẽ quá mức cần thiết để thêm renderToGUI(...)sendToPrinter(...)vào lớp cơ sở.

Cách tiếp cận bạn đang sử dụng, sử dụng kiểm tra loại và phôi, rất mong manh - nhưng ít nhất không làm cho các mối quan tâm tách biệt.

Tuy nhiên, nếu bạn thấy mình thường xuyên thực hiện các loại kiểm tra / phôi này thì một tùy chọn là triển khai mẫu khách truy cập / gửi kép cho nó. Nó trông giống như thế này:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Bây giờ mã của bạn trở thành

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

Lợi ích chính là nếu bạn thêm một lớp con, bạn sẽ nhận được các lỗi biên dịch thay vì các trường hợp bị thiếu âm thầm. Lớp khách truy cập mới cũng trở thành một mục tiêu tốt đẹp để kéo các chức năng vào. Ví dụ, nó có thể có ý nghĩa để di chuyển getRandomIngredients()vào ActOnBase.

Bạn cũng có thể trích xuất logic lặp: Ví dụ đoạn trên có thể trở thành

BaseVisitor.applyToArray(bases, new ActOnBase() );

Mát xa hơn một chút và sử dụng lambdas và phát trực tuyến của Java 8 sẽ cho phép bạn truy cập

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Mà IMO là khá nhiều tìm kiếm gọn gàng và cô đọng như bạn có thể nhận được.

Dưới đây là một ví dụ Java 8 đầy đủ hơn:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));

+1: Loại điều này thường được sử dụng trong các ngôn ngữ có hỗ trợ hạng nhất cho loại tổng hợp và khớp mẫu và nó là một thiết kế hợp lệ trong Java, mặc dù về mặt cú pháp rất xấu.
Mankude

@Mankude Một chút đường cú pháp có thể làm cho nó rất xa xấu xí - Tôi đã cập nhật nó với một ví dụ.
Michael Anderson

Giả sử OP giả định thay đổi ý định và quyết định thêm a Sub3. Không phải khách truy cập có cùng một vấn đề như instanceof? Bây giờ bạn cần thêm onSub3vào khách truy cập và nó bị hỏng nếu bạn quên.
Radiodef

1
@Radiodef Một lợi thế của việc sử dụng mẫu khách truy cập so với chuyển đổi trực tiếp trên loại là khi bạn đã thêm vào onSub3giao diện khách truy cập, bạn sẽ gặp lỗi biên dịch ở mọi vị trí bạn tạo Khách truy cập mới chưa được cập nhật. Ngược lại, việc chuyển đổi theo kiểu sẽ tạo ra lỗi thời gian chạy tốt nhất - có thể gây khó khăn hơn để kích hoạt.
Michael Anderson

1
@FiveNine, nếu bạn vui lòng thêm ví dụ hoàn chỉnh đó vào cuối câu trả lời có thể giúp người khác.
Michael Anderson

83

Từ quan điểm của tôi: thiết kế của bạn là sai .

Được dịch sang ngôn ngữ tự nhiên, bạn đang nói như sau:

Cho chúng tôi có animals, có catsfish. animalscó các thuộc tính, là phổ biến cho catsfish. Nhưng điều đó là không đủ: có một số thuộc tính, khác biệt catvới fish, do đó bạn cần phải phân lớp.

Bây giờ bạn có vấn đề, rằng bạn quên mô hình chuyển động . Đuợc. Điều đó tương đối dễ dàng:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Nhưng đó là một thiết kế sai. Cách chính xác sẽ là:

for(Animal a : animals){
    animal.move()
}

Trường hợp movesẽ được chia sẻ hành vi được thực hiện khác nhau bởi mỗi động vật.

Vì các phương thức này có các tham số khác nhau và thực hiện những điều hoàn toàn khác nhau, nên chúng hoàn toàn không tương thích và tôi không thể chỉ sử dụng đa hình để giải quyết vấn đề này.

Điều này có nghĩa là: thiết kế của bạn bị hỏng.

Đề nghị của tôi: refactor Base, Sub1Sub2.


8
Nếu bạn sẽ cung cấp mã thực tế, tôi có thể đưa ra khuyến nghị.
Thomas Junk

9
@codebreaker Nếu bạn đồng ý rằng ví dụ của bạn không tốt, tôi khuyên bạn nên chấp nhận câu trả lời này và sau đó viết một câu hỏi mới. Đừng sửa lại câu hỏi để có một ví dụ khác hoặc các câu trả lời hiện tại sẽ không có ý nghĩa.
Đĩa Moby

16
@codebreaker Tôi nghĩ rằng điều này "giải quyết" vấn đề bằng cách trả lời câu hỏi thực sự . Câu hỏi thực sự là: Làm thế nào để tôi giải quyết vấn đề của mình? Tái cấu trúc mã của bạn đúng cách. Refactor để bạn .move()hay .attack()và đúng cách trừu tượng thatphần - thay vì phải .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home(), vv Làm thế nào bạn Refactor đúng cách? Không biết nếu không có ví dụ tốt hơn ... nhưng nói chung vẫn là câu trả lời đúng - trừ khi mã ví dụ và nhiều chi tiết khác đề xuất khác.
WernerCD

11
@codebreaker Nếu hệ thống phân cấp lớp của bạn dẫn bạn đến những tình huống như thế này, thì theo định nghĩa, nó không có ý nghĩa
Patrick Collins

7
@codebreaker Liên quan đến bình luận cuối cùng của Crisfole, có một cách khác để xem xét nó có thể giúp ích. Ví dụ, với movevs fly/ run/ etc, điều đó có thể được giải thích trong một câu như: Bạn nên nói cho đối tượng biết phải làm gì chứ không phải làm thế nào. Về mặt người truy cập, như bạn đề cập giống như trường hợp thực tế trong một bình luận ở đây, bạn nên đặt câu hỏi cho đối tượng, không kiểm tra trạng thái của nó.
Izkata

9

Thật khó để tưởng tượng một tình huống mà bạn có một nhóm các thứ và muốn chúng làm bánh sandwich hoặc liên lạc với người ngoài hành tinh. Trong hầu hết các trường hợp khi bạn tìm thấy việc truyền như vậy, bạn sẽ hoạt động với một loại - ví dụ: trong clang, bạn lọc một tập hợp các nút để khai báo trong đó getAsFunction trả về giá trị khác, thay vì thực hiện một cái gì đó khác nhau cho mỗi nút trong danh sách.

Có thể là bạn cần thực hiện một chuỗi hành động và thực sự không liên quan đến việc các đối tượng thực hiện hành động có liên quan.

Vì vậy, thay vì một danh sách Base, hãy làm việc với danh sách các hành động

for (RandomAction action : actions)
   action.act(context);

Ở đâu

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

Nếu thích hợp, bạn có thể yêu cầu Base thực hiện một phương thức để trả về hành động hoặc bất kỳ phương tiện nào khác mà bạn cần để chọn hành động từ các thể hiện trong danh sách cơ sở của bạn (vì bạn nói rằng bạn không thể sử dụng đa hình, nên có lẽ là hành động để thực hiện không phải là một chức năng của lớp mà là một số thuộc tính khác của các cơ sở, nếu không, bạn chỉ cần cung cấp cho phương thức hành động (Ngữ cảnh) cơ sở)


2
Bạn hiểu rằng các phương thức vô lý của tôi được thiết kế để cho thấy sự khác biệt trong những gì các lớp có thể làm khi lớp con của chúng được biết (và chúng không liên quan gì đến nhau), nhưng tôi nghĩ rằng có một Contextđối tượng chỉ làm cho vấn đề tồi tệ hơn. Tôi cũng có thể chuyển qua Objectcác phương thức, truyền chúng và hy vọng chúng là loại phù hợp.
codebreaker

1
@codebreaker bạn thực sự cần phải làm rõ câu hỏi của mình sau đó - trong hầu hết các hệ thống sẽ có một lý do hợp lệ để gọi một loạt các chức năng không biết về những gì họ làm; ví dụ để xử lý các quy tắc về một sự kiện hoặc thực hiện một bước trong mô phỏng. Thông thường các hệ thống như vậy có một bối cảnh trong đó các hành động xảy ra. Nếu 'getRandomIngredrons' và 'getFrequency' không liên quan, thì bạn không nên ở cùng một đối tượng và bạn cần một cách tiếp cận khác, chẳng hạn như có các hành động nắm bắt nguồn nguyên liệu hoặc tần suất.
Pete Kirkham

1
Bạn nói đúng, và tôi không nghĩ mình có thể cứu vãn được câu hỏi này. Cuối cùng tôi đã chọn một ví dụ tồi, vì vậy có lẽ tôi sẽ chỉ đăng một cái khác. GetFrequency () và getIngredrons () chỉ là các trình giữ chỗ. Có lẽ tôi nên đặt "..." làm lý lẽ để làm rõ hơn rằng tôi sẽ không biết chúng là gì.
codebreaker

Đây là IMO câu trả lời đúng. Hiểu rằng Contextkhông cần phải là một lớp mới, hoặc thậm chí là một giao diện. Thông thường bối cảnh chỉ là người gọi, vì vậy các cuộc gọi ở dạng action.act(this). Nếu getFrequency()getRandomIngredients()là các phương thức tĩnh, bạn thậm chí có thể không cần một bối cảnh. Đây là cách tôi sẽ thực hiện nói, một hàng đợi công việc, mà vấn đề của bạn nghe có vẻ rất khủng khiếp.
Câu hỏi

Ngoài ra, điều này hầu hết là giống hệt với giải pháp mô hình khách truy cập. Sự khác biệt là bạn đang triển khai các phương thức của Khách hàng :: OnSub1 () / Khách hàng :: OnSub2 () dưới dạng Sub1 :: act () / Sub2 :: act ()
Câu hỏi

4

Sẽ thế nào nếu bạn có các lớp con thực hiện một hoặc nhiều giao diện xác định những gì chúng có thể làm? Một cái gì đó như thế này:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Sau đó, các lớp học của bạn sẽ trông như thế này:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

Và vòng lặp for của bạn sẽ trở thành:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Hai ưu điểm chính của phương pháp này:

  • không tham gia casting
  • bạn có thể yêu cầu mỗi lớp con thực hiện nhiều giao diện như bạn muốn, điều này mang lại sự linh hoạt cho những thay đổi trong tương lai.

PS: Xin lỗi vì tên của các giao diện, tôi không thể nghĩ ra bất cứ điều gì tuyệt vời hơn tại thời điểm cụ thể đó: D.


3
Điều này không thực sự giải quyết vấn đề. Bạn vẫn cần diễn viên trong vòng lặp for. (Nếu bạn không thì bạn cũng không cần diễn viên trong ví dụ của OP).
Taemyr

2

Cách tiếp cận có thể là một cách tốt trong trường hợp hầu hết mọi loại trong gia đình sẽ thể được sử dụng trực tiếp như một cách thực hiện một số giao diện đáp ứng một số tiêu chí hoặc có thể được sử dụng để tạo ra việc thực hiện giao diện đó. Các loại bộ sưu tập tích hợp sẽ IMHO được hưởng lợi từ mẫu này, nhưng vì chúng không nhằm mục đích ví dụ nên tôi sẽ phát minh ra giao diện bộ sưu tập BunchOfThings<T>.

Một số thực hiện BunchOfThingslà có thể thay đổi; một số không. Trong nhiều trường hợp, một đối tượng Fred có thể muốn giữ thứ gì đó mà nó có thể sử dụng như một BunchOfThingsvà biết rằng không có gì khác ngoài Fred sẽ có thể sửa đổi nó. Yêu cầu này có thể được thỏa mãn theo hai cách:

  1. Fred biết rằng nó có các tài liệu tham khảo duy nhất về điều đó BunchOfThings, và không có tài liệu tham khảo bên ngoài nào cho rằng BunchOfThingsir bên trong của nó tồn tại ở bất cứ đâu trong vũ trụ. Nếu không ai khác có tham chiếu đến BunchOfThingsnội bộ của nó, thì không ai khác có thể sửa đổi nó, vì vậy các ràng buộc sẽ được thỏa mãn.

  2. Không BunchOfThings, bất kỳ nội bộ nào của nó mà các tài liệu tham khảo bên ngoài tồn tại, có thể được sửa đổi thông qua bất kỳ phương tiện nào. Nếu hoàn toàn không ai có thể sửa đổi a BunchOfThings, thì ràng buộc sẽ được thỏa mãn.

Một cách để thỏa mãn các ràng buộc sẽ là sao chép vô điều kiện bất kỳ đối tượng nào nhận được (xử lý đệ quy bất kỳ thành phần lồng nhau nào). Một cách khác là kiểm tra xem một đối tượng nhận được có hứa hẹn tính bất biến hay không, và nếu không, hãy tạo một bản sao của nó và thực hiện tương tự với bất kỳ thành phần lồng nhau nào. Một cách khác, có khả năng sạch hơn cái thứ hai và nhanh hơn cái thứ nhất, là đưa ra một AsImmutablephương thức yêu cầu một đối tượng tạo một bản sao bất biến của chính nó (sử dụng AsImmutabletrên bất kỳ thành phần lồng nhau nào hỗ trợ nó).

Các phương thức liên quan cũng có thể được cung cấp cho asDetached(để sử dụng khi mã nhận được một đối tượng và không biết liệu nó có muốn biến đổi nó hay không, trong trường hợp đó một đối tượng có thể thay đổi phải được thay thế bằng một đối tượng có thể thay đổi mới, nhưng có thể giữ một đối tượng bất biến as-is), asMutable(trong trường hợp một đối tượng biết rằng nó sẽ giữ một đối tượng được trả về trước đó asDetached, nghĩa là một tham chiếu không chia sẻ đến một đối tượng có thể thay đổi hoặc một tham chiếu có thể chia sẻ đến một đối tượng có thể thay đổi) và asNewMutable(trong trường hợp mã nhận được bên ngoài tham khảo và biết rằng họ sẽ muốn thay đổi một bản sao của dữ liệu trong đó - nếu dữ liệu đến có thể thay đổi được thì không có lý do gì để bắt đầu bằng cách tạo một bản sao bất biến sẽ được sử dụng ngay lập tức để tạo một bản sao có thể thay đổi và sau đó bị bỏ rơi).

Lưu ý rằng trong khi các asXXphương thức có thể trả về các loại hơi khác nhau, vai trò thực sự của chúng là đảm bảo rằng các đối tượng được trả về sẽ đáp ứng nhu cầu của chương trình.


0

Bỏ qua vấn đề liệu bạn có một thiết kế tốt hay không, và giả sử nó tốt hay ít nhất là chấp nhận được, tôi muốn xem xét khả năng của các lớp con, chứ không phải loại.

Do đó, một trong hai:


Chuyển một số kiến ​​thức về sự tồn tại của bánh sandwich và người ngoài hành tinh vào lớp cơ sở, mặc dù bạn biết một số trường hợp của lớp cơ sở không thể làm điều đó. Triển khai nó trong lớp cơ sở để đưa ra các ngoại lệ và thay đổi mã thành:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Sau đó, một hoặc cả hai lớp con ghi đè canMakeASandwich()và chỉ một lớp thực hiện mỗi makeASandwich()contactAliens().


Sử dụng các giao diện, không phải các lớp con cụ thể, để phát hiện các khả năng của một loại. Để lại lớp cơ sở một mình và thay đổi mã thành:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

hoặc có thể (và thoải mái bỏ qua tùy chọn này nếu nó không phù hợp với phong cách của bạn hoặc cho vấn đề đó bất kỳ phong cách Java nào bạn cho là hợp lý):

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Cá nhân tôi thường không thích kiểu bắt một ngoại lệ được mong đợi một nửa, bởi vì nguy cơ bắt không đúng cách ClassCastExceptionsẽ xuất hiện getRandomIngredientshoặc makeASandwich, nhưng YMMV.


2
Bắt ClassCastException chỉ là ew. Đó là toàn bộ mục đích của thể hiện.
Radiodef

@Radiodef: đúng, nhưng một mục đích <là để tránh bị bắt ArrayIndexOutOfBoundsExceptionvà một số người cũng quyết định làm điều đó. Không tính toán theo sở thích ;-) Về cơ bản, "vấn đề" của tôi ở đây là tôi làm việc với Python, trong đó phong cách ưa thích thường là bắt bất kỳ ngoại lệ nào là thích hợp hơn để kiểm tra trước liệu ngoại lệ có xảy ra hay không, cho dù thử nghiệm trước có đơn giản đến đâu . Có lẽ khả năng không đáng nhắc đến trong Java.
Steve Jessop

@SteveJessop »Yêu cầu sự tha thứ dễ dàng hơn là xin phép« là khẩu hiệu;)
Thomas Junk

0

Ở đây chúng ta có một trường hợp thú vị của một lớp cơ sở phát sóng chính nó đến các lớp dẫn xuất của chính nó. Chúng tôi biết rõ điều này thường là xấu, nhưng nếu chúng tôi muốn nói rằng chúng tôi đã tìm thấy một lý do chính đáng, chúng ta hãy xem và xem những hạn chế về điều này có thể là gì:

  1. Chúng tôi có thể bao gồm tất cả các trường hợp trong lớp cơ sở.
  2. Không có lớp dẫn xuất nước ngoài sẽ phải thêm một trường hợp mới, bao giờ.
  3. Chúng ta có thể sống với chính sách mà cuộc gọi thực hiện dưới sự kiểm soát của lớp cơ sở.
  4. Nhưng chúng ta biết từ khoa học của mình rằng chính sách phải làm gì trong một lớp dẫn xuất cho một phương thức không phải trong lớp cơ sở là của lớp dẫn xuất chứ không phải lớp cơ sở.

Nếu 4 thì ta có: 5. Chính sách của lớp dẫn xuất luôn nằm dưới sự kiểm soát chính trị giống như chính sách của lớp cơ sở.

Cả 2 và 5 đều ngụ ý trực tiếp rằng chúng ta có thể liệt kê tất cả các lớp dẫn xuất, điều đó có nghĩa là không có bất kỳ lớp dẫn xuất bên ngoài nào.

Nhưng đây là điều. Nếu tất cả đều là của bạn, bạn có thể thay thế if bằng một bản tóm tắt là một cuộc gọi phương thức ảo (ngay cả khi đó là một cuộc gọi vô nghĩa) và loại bỏ if và tự diễn. Do đó, đừng làm điều đó. Một thiết kế tốt hơn có sẵn.


-1

Đặt một phương thức trừu tượng trong lớp cơ sở thực hiện một điều trong Sub1 và điều khác trong Sub2 và gọi phương thức trừu tượng đó trong các vòng lặp hỗn hợp của bạn.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

Lưu ý rằng điều này có thể yêu cầu các thay đổi khác tùy thuộc vào cách xác định getRandomIngredrons và getFrequency. Nhưng, thành thật mà nói, bên trong lớp liên lạc với người ngoài hành tinh có lẽ là nơi tốt hơn để xác định getFrequency.

Ngẫu nhiên, các phương thức asSub1 và asSub2 của bạn chắc chắn thực tiễn tồi. Nếu bạn sẽ làm điều này, hãy làm nó bằng cách đúc. Không có gì các phương pháp này cung cấp cho bạn mà đúc không.


2
Câu trả lời của bạn cho thấy bạn đã không đọc hết câu hỏi: đa hình sẽ không cắt nó ở đây. Có một số phương thức trên mỗi Sub1 và Sub2, đây chỉ là các ví dụ và "doAT Breath" không thực sự có ý nghĩa gì. Nó không tương ứng với bất cứ điều gì tôi đang làm. Ngoài ra, như câu cuối cùng của bạn: làm thế nào về an toàn loại được đảm bảo?
codebreaker

@codebreaker - Nó tương ứng chính xác với những gì bạn đang làm trong vòng lặp trong ví dụ. Nếu điều đó không có ý nghĩa, đừng viết vòng lặp. Nếu nó không có ý nghĩa như một phương pháp thì nó không có ý nghĩa như một cơ thể vòng lặp.
Random832

1
Và các phương thức của bạn "đảm bảo" an toàn kiểu giống như cách truyền: bằng cách ném một ngoại lệ nếu đối tượng là loại sai.
Random832

Tôi nhận ra tôi đã chọn một ví dụ xấu. Vấn đề là hầu hết các phương thức trong các lớp con giống như các getters hơn là các phương thức đột biến. Các lớp này không tự làm mọi thứ, chủ yếu chỉ cung cấp dữ liệu. Đa hình sẽ không giúp tôi ở đó. Tôi đang làm việc với các nhà sản xuất, không phải người tiêu dùng. Ngoài ra: ném một ngoại lệ là một đảm bảo thời gian chạy, không tốt như thời gian biên dịch.
codebreaker

1
Quan điểm của tôi là mã của bạn chỉ cung cấp một đảm bảo thời gian chạy là tốt. Không có gì ngăn ai đó không kiểm tra isSub2 trước khi gọi asSub2.
Random832

-2

Bạn có thể đẩy cả hai makeASandwichcontactAliensvào lớp cơ sở và sau đó thực hiện chúng với các triển khai giả. Vì các kiểu trả về / đối số không thể được giải quyết thành một lớp cơ sở chung.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

Có những nhược điểm rõ ràng đối với loại điều này, như điều đó có nghĩa gì đối với hợp đồng của phương pháp và những gì bạn có thể và không thể suy ra về bối cảnh của kết quả. Bạn không thể nghĩ rằng vì tôi đã thất bại trong việc làm một chiếc bánh sandwich, các thành phần đã được sử dụng trong nỗ lực, v.v.


1
Tôi đã nghĩ về việc làm này, nhưng nó vi phạm Nguyên tắc thay thế Liskov và không tốt khi làm việc với, ví dụ như khi bạn gõ "sub1". và tự động hoàn tất xuất hiện, bạn thấy makeASandwich nhưng không liên lạc vớiAliens.
codebreaker

-5

Những gì bạn đang làm là hoàn toàn hợp pháp. Đừng chú ý đến những người không tán thành, những người chỉ nhắc lại giáo điều bởi vì họ đọc nó trong một số cuốn sách. Giáo điều không có chỗ trong kỹ thuật.

Tôi đã sử dụng cùng một cơ chế một vài lần và tôi có thể tự tin nói rằng thời gian chạy java cũng có thể đã làm điều tương tự ở ít nhất một nơi mà tôi có thể nghĩ ra, do đó cải thiện hiệu năng, tính khả dụng và khả năng đọc mã sử dụng nó

Lấy ví dụ java.lang.reflect.Member, đó là cơ sở củajava.lang.reflect.Fieldjava.lang.reflect.Method . . một số tham số và nó có thể trả về một giá trị. Vì vậy, các trường và phương thức đều là thành viên, nhưng những điều bạn có thể làm với chúng khác nhau như làm bánh sandwich so với tiếp xúc với người ngoài hành tinh.

Bây giờ, khi viết mã sử dụng sự phản chiếu, chúng ta rất thường có Membertrong tay và chúng ta biết rằng đó là một Methodhoặc một Field, (hoặc, hiếm khi, một cái gì đó khác), nhưng chúng ta phải làm tất cả những điều tẻ nhạtinstanceof để tìm ra chính xác nó là gì và sau đó chúng ta phải bỏ nó để có được một tài liệu tham khảo đúng đắn về nó. (Và điều này không chỉ tẻ nhạt, mà còn hoạt động không tốt lắm.)Method Lớp có thể dễ dàng thực hiện mô hình mà bạn đang mô tả, do đó làm cho cuộc sống của hàng ngàn lập trình viên dễ dàng hơn.

Tất nhiên, kỹ thuật này chỉ khả thi trong các hệ thống phân cấp nhỏ, được xác định rõ ràng của các lớp được kết hợp chặt chẽ mà bạn có (và sẽ luôn có) kiểm soát mức nguồn: bạn không muốn làm điều đó nếu hệ thống phân cấp lớp của bạn là chịu trách nhiệm được gia hạn bởi những người không có quyền tự do tái cấu trúc lớp cơ sở.

Đây là cách tôi đã làm khác với những gì bạn đã làm:

  • Lớp cơ sở cung cấp một triển khai mặc định cho toàn bộ asDerivedClass()họ phương thức, có mỗi phương thức trả về null.

  • Mỗi lớp dẫn xuất chỉ ghi đè một trong các asDerivedClass()phương thức, trả về thisthay vì null. Nó không ghi đè lên bất kỳ phần còn lại, cũng không muốn biết bất cứ điều gì về họ. Vì vậy, không có IllegalStateExceptions được ném.

  • Lớp cơ sở cũng cung cấp các finaltriển khai cho toàn bộ isDerivedClass()họ các phương thức, được mã hóa như sau: return asDerivedClass() != null; Bằng cách này, số lượng các phương thức cần được ghi đè bởi các lớp dẫn xuất được giảm thiểu.

  • Tôi đã không được sử dụng @Deprecatedtrong cơ chế này bởi vì tôi đã không nghĩ về nó. Bây giờ bạn đã cho tôi ý tưởng, tôi sẽ đưa nó vào sử dụng, cảm ơn!

C # có một cơ chế liên quan được tích hợp sẵn thông qua việc sử dụng astừ khóa. Trong C # bạn có thể nói DerivedClass derivedInstance = baseInstance as DerivedClassvà bạn sẽ nhận được một tham chiếu đến DerivedClassnếu bạn baseInstancethuộc lớp đó hoặc nullnếu không. Điều này (về mặt lý thuyết) hoạt động tốt hơn so với istheo sau, ( islà từ khóa C # được đặt tên tốt hơn được thừa nhận cho instanceof), nhưng cơ chế tùy chỉnh mà chúng tôi đã tạo thủ công thực hiện thậm chí còn tốt hơn: cả hai instanceofhoạt động đúc và của Java nhưas toán tử của C # không thực hiện nhanh như lệnh gọi phương thức ảo duy nhất của phương pháp tùy chỉnh của chúng tôi.

Tôi xin đưa ra đề xuất rằng kỹ thuật này nên được tuyên bố là một mô hình và nên tìm một cái tên đẹp cho nó.

Gee, cảm ơn cho các downvote!

Tóm tắt các tranh cãi, để cứu bạn khỏi những rắc rối khi đọc các bình luận:

Sự phản đối của mọi người dường như là thiết kế ban đầu đã sai, có nghĩa là bạn không bao giờ nên có nhiều lớp khác nhau xuất phát từ một lớp cơ sở chung, hoặc ngay cả khi bạn làm, mã sử dụng hệ thống phân cấp như vậy không bao giờ nên ở vị trí có một tham chiếu cơ sở và cần tìm ra lớp dẫn xuất. Do đó, họ nói, cơ chế tự đúc được đề xuất bởi câu hỏi này và bằng câu trả lời của tôi, giúp cải thiện việc sử dụng thiết kế ban đầu, không bao giờ cần thiết ở nơi đầu tiên. (Họ không thực sự nói bất cứ điều gì về chính cơ chế tự đúc, họ chỉ phàn nàn về bản chất của các thiết kế mà cơ chế này được áp dụng.)

Tuy nhiên, trong ví dụ trên tôi đã đã chỉ ra rằng những người tạo ra thời gian chạy java đã làm trong thực tế chọn một thiết kế chính xác như cho java.lang.reflect.Member, Field, Methodhệ thống cấp bậc, và trong các ý kiến dưới đây tôi cũng cho thấy rằng những người sáng tạo của C # runtime đến một cách độc lập tại một thiết kế tương đương cho System.Reflection.MemberInfo, FieldInfo.MethodInfo hệ thống cấp bậc. Vì vậy, đây là hai kịch bản trong thế giới thực khác nhau, nằm ngay dưới mũi của mọi người và có các giải pháp hoàn toàn khả thi khi sử dụng các thiết kế chính xác như vậy.

Đó là những gì tất cả các ý kiến ​​sau đây sôi lên. Cơ chế tự đúc hầu như không được đề cập.


13
Chắc chắn là hợp pháp nếu bạn tự thiết kế thành một chiếc hộp. Đó là vấn đề với rất nhiều loại câu hỏi. Mọi người đưa ra quyết định trong các lĩnh vực khác của thiết kế, điều này buộc các loại giải pháp hackish này khi giải pháp thực sự là tìm ra lý do tại sao bạn lại rơi vào tình huống này ngay từ đầu. Một thiết kế đẹp sẽ không giúp bạn ở đây. Tôi hiện đang xem xét một ứng dụng SLOC 200 nghìn và tôi không thấy bất cứ nơi nào chúng tôi cần để làm điều này. Vì vậy, nó không chỉ là một cái gì đó mọi người đọc trong một cuốn sách. Không tham gia vào các loại tình huống này chỉ đơn giản là kết quả cuối cùng của việc tuân theo các thực hành tốt.
Dunk

3
@Dunk Tôi chỉ cho thấy rằng cần có một cơ chế như vậy tồn tại ví dụ trong java.lang.reflection.Memberhệ thống phân cấp. Những người tạo ra thời gian chạy java gặp phải tình huống này, tôi không cho rằng bạn sẽ nói rằng họ tự thiết kế thành một cái hộp, hoặc đổ lỗi cho họ vì đã không tuân theo một số loại thực hành tốt nhất? Vì vậy, điểm tôi đang làm ở đây là một khi bạn gặp loại tình huống này, cơ chế này là một cách tốt để cải thiện mọi thứ.
Mike Nakis

6
@MikeNakis Những người tạo ra Java đã đưa ra rất nhiều quyết định tồi, vì vậy mà một mình không nói gì nhiều. Ở bất cứ giá nào, sử dụng khách truy cập sẽ tốt hơn so với thực hiện kiểm tra đột xuất.
Doval

3
Ngoài ra, ví dụ là thiếu sót. Có một Membergiao diện, nhưng nó chỉ phục vụ để kiểm tra vòng loại truy cập. Thông thường, bạn nhận được một danh sách các phương thức hoặc thuộc tính và sử dụng trực tiếp (không trộn chúng, vì vậy không List<Member>xuất hiện), vì vậy bạn không cần phải truyền. Lưu ý rằng Classkhông cung cấp một getMembers()phương pháp. Tất nhiên, một lập trình viên không biết gì có thể tạo ra List<Member>, nhưng điều đó sẽ ít có ý nghĩa như làm cho nó trở thành List<Object>và thêm một số Integerhoặc Stringvào nó.
SJuan76

5
@MikeNakis "Rất nhiều người làm điều đó" và "Người nổi tiếng làm điều đó" không phải là những lý lẽ thực sự. Antipotype là cả những ý tưởng tồi và được sử dụng rộng rãi theo định nghĩa, tuy nhiên những lời bào chữa đó sẽ biện minh cho việc sử dụng chúng. Đưa ra lý do thực sự cho việc sử dụng một số thiết kế hoặc khác. Vấn đề của tôi với việc truyền quảng cáo đột xuất là mọi người dùng API của bạn phải thực hiện việc truyền đúng mỗi lần họ thực hiện. Một khách truy cập chỉ cần được chính xác một lần. Ẩn việc đúc đằng sau một số phương thức và quay lại nullthay vì một ngoại lệ không làm cho nó tốt hơn nhiều. Tất cả những thứ khác đều bằng nhau, khách truy cập an toàn hơn trong khi làm cùng một công việc.
Doval
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.