Apakah saya boleh memiliki objek yang melemparkan diri mereka sendiri, bahkan jika itu mencemari API subkelas mereka?


33

Saya memiliki kelas dasar Base,. Ini memiliki dua subclass, Sub1dan Sub2. Setiap subkelas memiliki beberapa metode tambahan. Misalnya, Sub1memiliki Sandwich makeASandwich(Ingredients... ingredients), dan Sub2memilikiboolean contactAliens(Frequency onFrequency) .

Karena metode ini mengambil parameter yang berbeda dan melakukan hal yang sama sekali berbeda, mereka sepenuhnya tidak kompatibel, dan saya tidak bisa menggunakan polimorfisme untuk menyelesaikan masalah ini.

Basemenyediakan sebagian besar fungsionalitas, dan saya memiliki banyak koleksi Baseobjek. Namun, semua Basebenda adalah a Sub1atau a Sub2, dan kadang-kadang saya perlu tahu yang mana.

Sepertinya ide yang buruk untuk melakukan hal berikut:

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

Jadi saya datang dengan strategi untuk menghindari ini tanpa casting. Basesekarang memiliki metode ini:

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

Dan tentu saja, Sub1menerapkan metode ini sebagai

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

Dan Sub2 mengimplementasikannya dengan cara yang berlawanan.

Sayangnya, sekarang Sub1dan Sub2memiliki metode ini di API mereka sendiri. Jadi saya bisa melakukan ini, misalnya, pada 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(); }

Dengan cara ini, jika objek diketahui hanya a Base , metode ini tidak ditinggalkan, dan dapat digunakan untuk "melemparkan" dirinya ke jenis yang berbeda sehingga saya dapat memanggil metode subclass di atasnya. Ini tampaknya elegan bagi saya, tetapi di sisi lain, saya agak menyalahgunakan anotasi usang sebagai cara untuk "menghapus" metode dari kelas.

Karena sebuah Sub1instance benar - benar sebuah Base, masuk akal untuk menggunakan warisan daripada enkapsulasi. Apakah yang saya lakukan baik? Apakah ada cara yang lebih baik untuk menyelesaikan masalah ini?


12
Semua 3 kelas sekarang harus saling mengenal. Menambahkan Sub3 akan melibatkan banyak perubahan kode, dan menambahkan Sub10 akan sangat menyakitkan
Dan Pichelman

15
Akan sangat membantu jika Anda memberi kami kode nyata. Ada situasi di mana pantas untuk mengambil keputusan berdasarkan kelas tertentu dari sesuatu, tetapi tidak mungkin untuk mengatakan apakah Anda dibenarkan dalam apa yang Anda lakukan dengan contoh-contoh yang dibuat-buat. Untuk apa pun nilainya, yang Anda inginkan adalah Pengunjung atau tagged union .
Doval

1
Maaf, saya pikir akan lebih mudah untuk memberikan contoh yang disederhanakan. Mungkin saya akan mengirim pertanyaan baru dengan ruang lingkup yang lebih luas tentang apa yang ingin saya lakukan.
codebreaker

12
Anda hanya menerapkan kembali casting dan instanceof, dengan cara yang membutuhkan banyak pengetikan, rentan kesalahan, dan membuatnya sulit untuk menambahkan lebih banyak subkelas.
user253751

5
Jika Sub1dan Sub2tidak dapat digunakan secara bergantian, mengapa Anda memperlakukannya seperti itu? Mengapa tidak melacak 'pembuat sandwich' dan 'alien-contacters' Anda secara terpisah?
Pieter Witvoet

Jawaban:


27

Tidak selalu masuk akal untuk menambahkan fungsi ke kelas dasar, seperti yang disarankan dalam beberapa jawaban lainnya. Menambahkan terlalu banyak fungsi kasus khusus dapat mengakibatkan pengikatan komponen yang tidak terkait.

Misalnya saya mungkin punya Animalkelas, dengan Catdan Dogkomponen. Jika saya ingin dapat mencetaknya, atau menunjukkannya di GUI, mungkin saya harus menambah renderToGUI(...)dan menambah sendToPrinter(...)kelas dasar.

Pendekatan yang Anda gunakan, menggunakan pemeriksaan tipe dan gips, rapuh - tetapi setidaknya tidak membuat keprihatinan dipisahkan.

Namun jika Anda mendapati diri Anda sering melakukan pemeriksaan semacam ini maka salah satu opsi adalah menerapkan pola pengunjung / pengiriman ganda untuknya. Ini terlihat seperti ini:

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

Sekarang kode Anda menjadi

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

Manfaat utama adalah bahwa jika Anda menambahkan subkelas, Anda akan mendapatkan kesalahan kompilasi daripada kasus yang hilang secara diam-diam. Kelas pengunjung baru juga menjadi target yang bagus untuk menarik fungsi. Misalnya mungkin masuk akal untuk pindah getRandomIngredients()keActOnBase .

Anda juga dapat mengekstrak logika perulangan: Misalnya fragmen di atas mungkin menjadi

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

Sedikit memijat lebih lanjut dan menggunakan lambdas dan streaming Java 8 akan membuat Anda bisa

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

IMO mana yang cukup rapi dan ringkas yang bisa Anda dapatkan.

Berikut adalah contoh Java 8 yang lebih lengkap:

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: Hal semacam ini biasa digunakan dalam bahasa yang memiliki dukungan kelas satu untuk jumlah-jenis dan pencocokan pola, dan ini adalah desain yang valid di Jawa, meskipun secara sintaksis sangat jelek.
Mankarse

@ Mankarse Sedikit gula sintaksis bisa membuatnya sangat jauh dari jelek - saya sudah memperbaruinya dengan contoh.
Michael Anderson

Katakanlah secara hipotetis OP berubah pikiran dan memutuskan untuk menambahkan Sub3. Bukankah pengunjung memiliki masalah yang sama persis instanceof? Sekarang Anda perlu menambah onSub3pengunjung dan itu rusak jika Anda lupa.
Radiodef

1
@Radiodef Keuntungan menggunakan pola pengunjung alih-alih mengaktifkan tipe secara langsung adalah bahwa setelah Anda menambahkan onSub3ke antarmuka pengunjung, Anda mendapatkan kesalahan kompilasi di setiap lokasi yang Anda buat Pengunjung baru yang belum diperbarui. Sebaliknya beralih pada jenis terbaik akan menghasilkan kesalahan run-time - yang bisa lebih sulit untuk dipicu.
Michael Anderson

1
@FiveNine, jika Anda senang menambahkan contoh lengkap itu ke akhir jawaban yang mungkin bisa membantu orang lain.
Michael Anderson

83

Dari sudut pandang saya: desain Anda salah .

Diterjemahkan ke bahasa alami, Anda mengatakan yang berikut:

Mengingat yang kita miliki animals, ada catsdan fish. animalsmemiliki sifat, yang umum untuk catsdan fish. Tapi itu tidak cukup: ada beberapa sifat, yang membedakan catdarifish , oleh karena itu Anda perlu subkelas.

Sekarang Anda memiliki masalah, bahwa Anda lupa untuk memodelkan gerakan . Baik. Itu relatif mudah:

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

Tapi itu desain yang salah. Cara yang benar adalah:

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

Di mana moveperilaku bersama akan diimplementasikan secara berbeda oleh masing-masing hewan.

Karena metode ini mengambil parameter yang berbeda dan melakukan hal yang sama sekali berbeda, mereka sepenuhnya tidak kompatibel, dan saya tidak bisa menggunakan polimorfisme untuk menyelesaikan masalah ini.

Ini berarti: desain Anda rusak.

Rekomendasi saya: refactor Base, Sub1dan Sub2.


8
Jika Anda menawarkan kode aktual, saya dapat membuat rekomendasi.
Thomas Junk

9
@codebreaker Jika Anda setuju bahwa contoh Anda tidak baik, saya sarankan menerima jawaban ini dan kemudian menulis pertanyaan baru. Jangan merevisi pertanyaan untuk memiliki contoh yang berbeda atau jawaban yang ada tidak masuk akal.
Moby Disk

16
@ codebreaker Saya pikir ini "memecahkan" masalah dengan menjawab pertanyaan sebenarnya . Pertanyaan sesungguhnya adalah: Bagaimana cara saya mengatasi masalah saya? Perbaiki kode Anda dengan benar. Refactor sehingga Anda .move()atau .attack()dan benar abstrak thatbagian - bukan memiliki .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home(), dll Bagaimana Anda refactor benar? Tidak dikenal tanpa contoh yang lebih baik ... tetapi umumnya masih merupakan jawaban yang benar - kecuali kode contoh dan detail lainnya menyarankan sebaliknya.
WernerCD

11
@codebreaker Jika hierarki kelas Anda mengarahkan Anda ke situasi seperti ini, maka menurut definisi itu tidak masuk akal
Patrick Collins

7
@codebreaker Terkait dengan komentar terakhir Crisfole, ada cara lain untuk melihatnya yang mungkin bisa membantu. Misalnya, dengan movevs fly/ run/ etc, yang dapat dijelaskan dalam satu kalimat sebagai: Anda harus memberi tahu objek apa yang harus dilakukan, bukan bagaimana melakukannya. Dalam hal aksesor, seperti yang Anda sebutkan lebih seperti kasus nyata dalam komentar di sini, Anda harus mengajukan pertanyaan objek, bukan memeriksa kondisinya.
Izkata

9

Agak sulit untuk membayangkan keadaan di mana Anda memiliki sekelompok hal dan ingin mereka membuat sandwich atau menghubungi alien. Dalam kebanyakan kasus di mana Anda menemukan casting seperti itu Anda akan beroperasi dengan satu jenis - misalnya di dentang Anda menyaring satu set node untuk deklarasi di mana getAsFunction mengembalikan non-null, daripada melakukan sesuatu yang berbeda untuk setiap node dalam daftar.

Mungkin Anda perlu melakukan urutan tindakan dan sebenarnya tidak relevan bahwa objek yang melakukan tindakan terkait.

Jadi, alih-alih daftar Base, bekerja pada daftar tindakan

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

dimana

interface RandomAction {
    void act(Context context);
} 

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

Anda dapat, jika sesuai, meminta Base menerapkan metode untuk mengembalikan aksi, atau apa pun cara lain yang Anda perlukan untuk memilih aksi dari instance dalam daftar base Anda (karena Anda mengatakan Anda tidak dapat menggunakan polimorfisme, jadi mungkin tindakan yang harus diambil bukan fungsi dari kelas tetapi beberapa properti lain dari basis, jika tidak, Anda hanya akan memberikan metode Base the act (Context))


2
Anda mengetahui bahwa metode absurd saya dirancang untuk menunjukkan perbedaan dalam apa yang dapat dilakukan kelas setelah subkelas mereka diketahui (dan bahwa mereka tidak ada hubungannya satu sama lain), tetapi saya pikir memiliki Contextobjek hanya akan memperburuk keadaan. Saya mungkin juga hanya meneruskan Objectmetode, casting mereka, dan berharap mereka adalah tipe yang tepat.
codebreaker

1
@codebreaker Anda benar-benar perlu mengklarifikasi pertanyaan Anda - di sebagian besar sistem akan ada alasan yang sah untuk memanggil banyak fungsi agnostik dari apa yang mereka lakukan; misalnya untuk memproses aturan pada suatu acara atau melakukan langkah dalam simulasi. Biasanya sistem seperti itu memiliki konteks di mana tindakan terjadi. Jika 'getRandomIngredients' dan 'getFrequency' tidak terkait, maka tidak boleh berada di objek yang sama, dan Anda memerlukan pendekatan yang berbeda, seperti meminta tindakan menangkap sumber bahan atau frekuensi.
Pete Kirkham

1
Anda benar, dan saya pikir saya tidak bisa menyelamatkan pertanyaan ini. Saya akhirnya memilih contoh yang buruk, jadi saya mungkin akan memposting yang lain lagi. GetFrequency () dan getIngredients () hanyalah placeholder. Saya mungkin seharusnya hanya menempatkan "..." sebagai argumen untuk membuatnya lebih jelas bahwa saya tidak tahu apa itu.
codebreaker

Ini IMO jawaban yang tepat. Memahami bahwa Contexttidak perlu menjadi kelas baru, atau bahkan sebuah antarmuka. Biasanya konteksnya hanya penelepon, jadi panggilan ada dalam formulir action.act(this). Jika getFrequency()dan getRandomIngredients()merupakan metode statis, Anda bahkan mungkin tidak memerlukan konteks. Ini adalah bagaimana saya akan menerapkan katakan, antrian pekerjaan, yang masalah Anda terdengar sangat mirip.
QuestionC

Ini juga sebagian besar identik dengan solusi pola pengunjung. Perbedaannya adalah Anda menerapkan metode Pengunjung :: OnSub1 () / Pengunjung :: OnSub2 () sebagai Sub1 :: act () / Sub2 :: act ()
QuestionC

4

Bagaimana jika Anda memiliki subclass Anda mengimplementasikan satu atau lebih antarmuka yang menentukan apa yang bisa mereka lakukan? Sesuatu seperti ini:

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

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Maka kelas Anda akan terlihat seperti ini:

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
    }
}

Dan for-loop Anda akan menjadi:

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

Dua keuntungan utama dari pendekatan ini:

  • tidak ada casting yang terlibat
  • Anda dapat meminta setiap subclass mengimplementasikan antarmuka sebanyak yang Anda inginkan, yang memang memberikan fleksibilitas untuk perubahan di masa mendatang.

PS: Maaf untuk nama-nama antarmuka, saya tidak bisa memikirkan sesuatu yang lebih keren pada saat itu: D.


3
Ini sebenarnya tidak menyelesaikan masalah. Anda masih membutuhkan para pemain di dalam for loop. (Jika Anda tidak maka Anda tidak perlu pemain dalam contoh OP juga).
Taemyr

2

Pendekatan ini dapat menjadi salah satu yang baik dalam kasus di mana hampir semua jenis dalam sebuah keluarga akan baik langsung digunakan sebagai implementasi beberapa antarmuka yang memenuhi beberapa kriteria atau dapat digunakan untuk membuat sebuah implementasi dari antarmuka tersebut. Jenis-jenis koleksi bawaan IMHO akan mendapat manfaat dari pola ini, tetapi karena mereka tidak untuk tujuan contoh saya akan menciptakan antarmuka koleksiBunchOfThings<T> .

Beberapa implementasi BunchOfThingsbisa berubah; beberapa tidak. Dalam banyak kasus, sebuah objek yang mungkin ingin dimiliki Fred untuk menyimpan sesuatu yang dapat digunakan sebagai BunchOfThingsdan tahu bahwa selain Fred akan dapat memodifikasinya. Persyaratan ini dapat dipenuhi dengan dua cara:

  1. Fred tahu bahwa itu memegang satu-satunya referensi untuk itu BunchOfThings, dan tidak ada referensi luar untuk itu BunchOfThingsir internal yang ada di mana pun di alam semesta. Jika tidak ada orang lain yang memiliki referensi ke BunchOfThingsatau internal, tidak ada orang lain akan dapat memodifikasinya, sehingga kendala akan terpenuhi.

  2. Baik BunchOfThingsinternal maupun internal yang tidak ada referensi luarnya, dapat dimodifikasi melalui cara apa pun. Jika benar-benar tidak ada yang dapat memodifikasi BunchOfThings, maka batasannya akan terpenuhi.

Salah satu cara untuk memenuhi kendala adalah dengan menyalin tanpa syarat objek apa pun yang diterima (secara rekursif memproses komponen bersarang). Lain akan menguji apakah objek yang diterima menjanjikan kekekalan dan, jika tidak, membuat salinannya, dan melakukan hal yang sama dengan komponen bersarang. Alternatif, yang cenderung lebih bersih daripada yang kedua dan lebih cepat dari yang pertama, adalah menawarkan AsImmutablemetode yang meminta objek untuk membuat salinan dirinya yang tidak dapat diubah (menggunakan AsImmutablekomponen bersarang yang mendukungnya).

Metode terkait juga dapat disediakan untuk asDetached(untuk digunakan ketika kode menerima objek dan tidak tahu apakah ia ingin mengubahnya, dalam hal ini objek yang dapat diubah harus diganti dengan objek yang dapat diubah baru, tetapi objek yang tidak dapat diubah dapat disimpan) as-is), asMutable(dalam kasus-kasus di mana suatu objek tahu bahwa ia akan menyimpan objek yang sebelumnya kembali dari asDetached, yaitu referensi yang tidak digunakan bersama untuk objek yang bisa berubah atau referensi yang dapat dibagi ke objek yang bisa berubah), dan asNewMutable(dalam kasus-kasus di mana kode menerima bagian luar referensi dan tahu itu akan ingin bermutasi salinan data di dalamnya - jika data yang masuk bisa berubah tidak ada alasan untuk memulai dengan membuat salinan yang tidak dapat diubah yang akan segera digunakan untuk membuat salinan yang dapat diubah dan kemudian ditinggalkan).

Perhatikan bahwa walaupun asXXmetode dapat mengembalikan tipe yang sedikit berbeda, peran sebenarnya adalah untuk memastikan bahwa objek yang dikembalikan akan memenuhi kebutuhan program.


0

Mengabaikan masalah apakah Anda memiliki desain yang bagus atau tidak, dan seandainya itu bagus atau setidaknya dapat diterima, saya ingin mempertimbangkan kemampuan subclass, bukan tipe.

Karena itu, baik:


Pindahkan pengetahuan tentang keberadaan sandwich dan alien ke kelas dasar, meskipun Anda tahu beberapa contoh kelas dasar tidak bisa melakukannya. Terapkan di kelas dasar untuk melempar pengecualian, dan ubah kode menjadi:

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

Kemudian satu atau kedua subclass menimpa canMakeASandwich(), dan hanya satu yang mengimplementasikan masing-masing makeASandwich()dan contactAliens().


Gunakan antarmuka, bukan subkelas konkret, untuk mendeteksi kemampuan apa yang dimiliki suatu jenis. Biarkan kelas dasar saja, dan ubah kode ke:

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.
}

atau mungkin (dan merasa bebas untuk mengabaikan opsi ini jika tidak sesuai dengan gaya Anda, atau dalam hal apa pun gaya Java yang Anda anggap masuk akal):

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

Secara pribadi saya biasanya tidak suka gaya terakhir menangkap pengecualian yang diharapkan, karena risiko menangkap secara tidak tepat ClassCastExceptionkeluar dari getRandomIngredientsatau makeASandwich, tapi YMMV.


2
Menangkap ClassCastException hanya ew. Itulah seluruh tujuan instanceof.
Radiodef

@Radiodef: benar, tetapi satu tujuan <adalah untuk menghindari penangkapan ArrayIndexOutOfBoundsException, dan beberapa orang memutuskan untuk melakukannya juga. Tidak ada perhitungan untuk rasa ;-) Pada dasarnya "masalah" saya di sini adalah saya bekerja di Python, di mana gaya yang disukai biasanya adalah bahwa menangkap pengecualian apa pun lebih baik daripada menguji terlebih dahulu apakah pengecualian akan terjadi, tidak peduli betapa sederhananya tes lanjutan akan . Mungkin saja kemungkinan tidak layak disebutkan di Jawa.
Steve Jessop

@SteveJessop »Lebih mudah untuk meminta pengampunan daripada izin« adalah slogan;)
Thomas Junk

0

Di sini kita memiliki kasus menarik tentang kelas dasar yang menurunkan dirinya ke kelas turunannya sendiri. Kami tahu betul bahwa ini biasanya buruk, tetapi jika kami ingin mengatakan bahwa kami menemukan alasan yang bagus, mari kita lihat dan lihat apa kendala yang mungkin terjadi:

  1. Kita bisa membahas semua kasing di kelas dasar.
  2. Tidak ada kelas turunan asing yang harus menambahkan kasus baru, selamanya.
  3. Kita dapat hidup dengan kebijakan yang membuat panggilan untuk berada di bawah kendali kelas dasar.
  4. Tapi kita tahu dari ilmu pengetahuan kita bahwa kebijakan apa yang harus dilakukan di kelas turunan untuk metode yang tidak ada di kelas dasar adalah kelas turunan dan bukan kelas dasar.

Jika 4 maka kita memiliki: 5. Kebijakan kelas turunan selalu di bawah kendali politik yang sama dengan kebijakan kelas dasar.

2 dan 5 keduanya secara langsung menyiratkan bahwa kita dapat menghitung semua kelas turunan, yang berarti bahwa tidak ada kelas turunan luar.

Tapi ini masalahnya. Jika semuanya milik Anda, Anda dapat mengganti if dengan abstraksi yang merupakan panggilan metode virtual (bahkan jika itu omong kosong) dan menyingkirkan if dan self-cast. Karena itu, jangan lakukan itu. Desain yang lebih baik tersedia.


-1

Letakkan metode abstrak di kelas dasar yang melakukan satu hal di Sub1 dan yang lainnya di Sub2, dan memanggil metode abstrak itu di loop campuran Anda.

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

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

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

Perhatikan bahwa ini mungkin memerlukan perubahan lain tergantung pada bagaimana getRandomIngredients dan getFrequency didefinisikan. Tapi, jujur ​​saja, di dalam kelas yang menghubungi alien mungkin adalah tempat yang lebih baik untuk mendefinisikan getFrequency.

Kebetulan, metode asSub1 dan asSub2 Anda jelas praktik yang buruk. Jika Anda akan melakukan ini, lakukan dengan casting. Tidak ada metode ini memberi Anda bahwa casting tidak.


2
Jawaban Anda menunjukkan Anda tidak membaca pertanyaan sepenuhnya: polimorfisme tidak akan memotongnya di sini. Ada beberapa metode pada masing-masing Sub1 dan Sub2, ini hanya contoh, dan "melakukan" tidak benar-benar berarti apa-apa. Itu tidak sesuai dengan apa pun yang akan saya lakukan. Selain itu, untuk kalimat terakhir Anda: bagaimana dengan keamanan jenis yang dijamin?
codebreaker

@codebreaker - Ini sesuai persis dengan apa yang Anda lakukan di loop dalam contoh. Jika itu tidak ada artinya, jangan menulis loop. Jika itu tidak masuk akal sebagai metode, itu tidak masuk akal sebagai loop body.
Random832

1
Dan metode Anda "menjamin" jenis keamanan dengan cara yang sama dilakukan casting: dengan melemparkan pengecualian jika objeknya adalah tipe yang salah.
Acak832

Saya sadar saya telah mengambil contoh yang buruk. Masalahnya adalah sebagian besar metode dalam subclass lebih mirip getter daripada metode mutatif. Kelas-kelas ini tidak melakukan banyak hal sendiri, hanya menyediakan data, kebanyakan. Polimorfisme tidak akan membantu saya di sana. Saya berurusan dengan produsen, bukan konsumen. Juga: melempar pengecualian adalah jaminan runtime, yang tidak sebagus waktu kompilasi.
codebreaker

1
Maksud saya adalah kode Anda hanya memberikan jaminan runtime juga. Tidak ada yang menghentikan seseorang untuk gagal memeriksa isSub2 sebelum memanggil asSub2.
Random832

-2

Anda bisa mendorong keduanya makeASandwichdan contactAlienske kelas dasar dan kemudian mengimplementasikannya dengan implementasi dummy. Karena tipe pengembalian / argumen tidak dapat diselesaikan ke kelas dasar umum.

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
    }
 }

Ada kerugian yang jelas untuk hal semacam ini, seperti apa artinya untuk kontrak metode dan apa yang Anda dapat dan tidak dapat menyimpulkan tentang konteks hasilnya. Anda tidak dapat berpikir karena saya gagal membuat sandwich bahan yang digunakan dalam upaya dll


1
Saya memang berpikir untuk melakukan ini, tetapi itu melanggar Prinsip Substitusi Liskov, dan tidak baik untuk digunakan, misalnya, ketika Anda mengetik "sub1." dan autocomplete muncul, Anda melihat makeASandwich tetapi tidak contactAliens.
codebreaker

-5

Apa yang Anda lakukan benar-benar sah. Jangan memperhatikan para penentang yang hanya mengulangi dogma karena mereka membacanya di beberapa buku. Dogma tidak punya tempat di bidang teknik.

Saya telah menggunakan mekanisme yang sama beberapa kali, dan saya dapat mengatakan dengan keyakinan bahwa java runtime juga dapat melakukan hal yang sama di setidaknya satu tempat yang dapat saya pikirkan, sehingga meningkatkan kinerja, kegunaan, dan keterbacaan kode yang menggunakannya.

Ambil contoh java.lang.reflect.Member, yang merupakan dasar dari java.lang.reflect.Fielddan java.lang.reflect.Method. (Hirarki sebenarnya sedikit lebih rumit dari itu, tapi itu tidak relevan.) Bidang dan metode adalah hewan yang sangat berbeda: satu memiliki nilai yang dapat Anda peroleh atau tetapkan, sementara yang lain tidak memiliki hal semacam itu, tetapi dapat digunakan dengan sejumlah parameter dan dapat mengembalikan nilai. Jadi, bidang dan metode sama-sama anggota, tetapi hal-hal yang dapat Anda lakukan dengannya berbeda satu sama lain seperti membuat sandwich vs menghubungi alien.

Sekarang, ketika menulis kode yang menggunakan refleksi kita sering memiliki Memberdi tangan kita, dan kita tahu bahwa itu adalah a Methodatau a Field, (atau, jarang, sesuatu yang lain,) namun kita harus melakukan semua yang membosankan instanceofuntuk mencari tahu dengan tepat apa itu dan kemudian kita harus membuangnya untuk mendapatkan referensi yang tepat untuk itu. (Dan ini tidak hanya membosankan, tetapi juga tidak berkinerja sangat baik.) MethodKelas bisa dengan mudah menerapkan pola yang Anda gambarkan, sehingga membuat kehidupan ribuan programmer lebih mudah.

Tentu saja, teknik ini hanya dapat dijalankan dalam hierarki kecil, yang didefinisikan dengan baik dari kelas yang digabungkan erat yang Anda miliki (dan akan selalu memiliki) kontrol tingkat sumber: Anda tidak ingin melakukan hal seperti itu jika hierarki kelas Anda adalah dapat diperpanjang oleh orang-orang yang tidak bebas untuk memperbaiki kelas dasar.

Berikut adalah bagaimana apa yang telah saya lakukan berbeda dari apa yang telah Anda lakukan:

  • Kelas dasar menyediakan implementasi default untuk seluruh asDerivedClass()keluarga metode, masing-masing kembali null.

  • Setiap kelas turunan hanya menimpa salah satu asDerivedClass()metode, mengembalikan thisbukan null. Itu tidak mengesampingkan sisanya, juga tidak ingin tahu apa-apa tentang mereka. Jadi, tidak ada IllegalStateExceptionyang terlempar.

  • Kelas dasar juga menyediakan finalimplementasi untuk seluruh isDerivedClass()keluarga metode, diberi kode sebagai berikut: return asDerivedClass() != null; Dengan cara ini, jumlah metode yang perlu diganti oleh kelas turunan diminimalkan.

  • Saya belum menggunakan @Deprecatedmekanisme ini karena saya tidak memikirkannya. Sekarang Anda memberi saya ide, saya akan menggunakannya, terima kasih!

C # memiliki mekanisme terkait bawaan melalui penggunaan askata kunci. Dalam C # Anda bisa mengatakan DerivedClass derivedInstance = baseInstance as DerivedClassdan Anda akan mendapatkan referensi ke DerivedClassjika Anda baseInstancedari kelas itu, atau nulljika tidak. Ini (secara teoritis) berkinerja lebih baik daripada isdiikuti oleh para pemeran, ( isadalah kata kunci C # yang diakui lebih baik untuk instanceof,) tetapi mekanisme kustom yang kami hasilkan dengan kerajinan tangan bahkan lebih baik: pasangan instanceofoperasi -dan-pemeran Jawa, juga sebagai asoperator C # tidak melakukan secepat panggilan metode virtual tunggal pendekatan kustom kami.

Saya dengan ini mengemukakan proposisi bahwa teknik ini harus dinyatakan sebagai pola dan bahwa nama yang bagus harus ditemukan untuk itu.

Wah, terima kasih untuk downvotes!

Ringkasan kontroversi, untuk menyelamatkan Anda dari kesulitan membaca komentar:

Orang-orang keberatan karena desain aslinya salah, artinya Anda tidak boleh memiliki kelas yang sangat berbeda yang berasal dari kelas dasar yang sama, atau bahkan jika Anda melakukannya, kode yang menggunakan hierarki seperti itu seharusnya tidak boleh berada dalam posisi memiliki referensi dasar dan perlu mencari tahu kelas turunannya. Karena itu, kata mereka, mekanisme self-casting yang diajukan oleh pertanyaan ini dan oleh jawaban saya, yang meningkatkan penggunaan desain asli, seharusnya tidak pernah diperlukan sejak awal. (Mereka tidak benar-benar mengatakan apa-apa tentang mekanisme self-casting itu sendiri, mereka hanya mengeluh tentang sifat desain yang dimaksudkan untuk diterapkan mekanisme itu.)

Namun, pada contoh di atas saya miliki sudah menunjukkan bahwa pencipta runtime java itu sebenarnya memilih desain tepatnya tersebut untuk java.lang.reflect.Member, Field, Methodhirarki, dan di komentar di bawah ini saya juga menunjukkan bahwa pencipta C # runtime secara independen tiba di setara desain untuk System.Reflection.MemberInfo, FieldInfo, MethodInfohirarki. Jadi, ini adalah dua skenario dunia nyata yang berbeda yang duduk tepat di bawah hidung semua orang dan yang memiliki solusi yang bisa diterapkan dengan menggunakan desain yang tepat seperti itu.

Itulah semua komentar berikut. Mekanisme self-casting hampir tidak disebutkan.


13
Sah-sah saja jika Anda mendesain diri menjadi sebuah kotak. Itulah masalah dengan banyak jenis pertanyaan ini. Orang-orang membuat keputusan dalam bidang desain lain yang memaksa jenis solusi peretasan ini ketika solusi sebenarnya adalah untuk mencari tahu mengapa Anda berakhir dalam situasi ini di tempat pertama. Desain yang layak tidak akan membuat Anda sampai di sini. Saat ini saya sedang melihat aplikasi 200K SLOC dan saya tidak melihat di mana pun kami perlu melakukan ini. Jadi itu bukan hanya sesuatu yang dibaca orang di buku. Tidak masuk dalam situasi semacam ini hanyalah hasil akhir dari mengikuti praktik yang baik.
Dunk

3
@Dunk Saya baru saja menunjukkan bahwa kebutuhan untuk mekanisme seperti itu ada misalnya dalam java.lang.reflection.Memberhierarki. Para pencipta runtime java masuk dalam situasi seperti ini, saya tidak berpikir Anda akan mengatakan mereka merancang diri mereka sendiri menjadi sebuah kotak, atau menyalahkan mereka karena tidak mengikuti semacam praktik terbaik? Jadi, maksud saya di sini adalah bahwa sekali Anda memiliki situasi seperti ini, mekanisme ini adalah cara yang baik untuk memperbaiki keadaan.
Mike Nakis

6
@MikeNakis Pembuat Java telah membuat banyak keputusan buruk, jadi itu saja tidak banyak bicara. Bagaimanapun, menggunakan pengunjung akan lebih baik daripada melakukan uji coba ad-hoc.
Doval

3
Selain itu, contohnya cacat. Ada Memberantarmuka, tetapi hanya berfungsi untuk memeriksa kualifikasi akses. Biasanya, Anda mendapatkan daftar metode atau properti dan menggunakannya secara langsung (tanpa mencampurkannya, jadi tidak List<Member>muncul), sehingga Anda tidak perlu melakukan cast. Catatan yang Classtidak memberikan getMembers()metode. Tentu saja, seorang programmer yang tidak mengerti mungkin menciptakannya List<Member>, tetapi itu akan membuat sedikit masuk akal jika membuatnya List<Object>dan menambahkan beberapa Integeratau Stringke dalamnya.
SJuan76

5
@MikeNakis "Banyak orang melakukannya" dan "Orang terkenal melakukannya" bukan argumen yang sebenarnya. Antipatterns adalah ide yang buruk dan banyak digunakan oleh definisi, namun alasan itu akan membenarkan penggunaannya. Berikan alasan nyata untuk menggunakan beberapa desain atau lainnya. Masalah saya dengan casting ad-hoc adalah bahwa setiap pengguna API Anda harus mendapatkan casting yang tepat setiap kali mereka melakukannya. Pengunjung hanya perlu benar sekali. Menyembunyikan casting di belakang beberapa metode dan kembali nullbukannya pengecualian tidak membuatnya jauh lebih baik. Semua sama sederajat, pengunjung lebih aman saat melakukan pekerjaan yang sama.
Doval
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.