Alt sınıflarının API'sini kirletse bile, kendilerini yaratan nesnelere sahip olmak uygun mudur?


33

Benim bir temel sınıfım var Base. İki alt sınıfı var Sub1ve Sub2. Her alt sınıfın bazı ek yöntemleri vardır. Örneğin, Sub1yer alır Sandwich makeASandwich(Ingredients... ingredients), ve Sub2yer alır boolean contactAliens(Frequency onFrequency).

Bu yöntemler farklı parametreler alıp tamamen farklı şeyler yaptıkları için tamamen uyumsuzlar ve bu sorunu çözmek için sadece polimorfizm kullanamıyorum.

Baseİşlevlerin çoğunu sağlar ve geniş bir Basenesne koleksiyonuna sahibim . Ancak, tüm Basenesneler ya a Sub1ya da Sub2a'dır ve bazen onların ne olduğunu bilmem gerekir.

Aşağıdakileri yapmak kötü bir fikir gibi görünüyor:

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

Bu yüzden oyuncu seçmeden bunu önlemek için bir strateji ile geldi. Baseşimdi bu yöntemlere sahip:

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

Ve tabii ki, Sub1bu yöntemleri uygular.

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

Ve Sub2onları tam tersi şekilde uygular.

Ne yazık ki, şimdi Sub1ve Sub2bu yöntemleri kendi API'lerinde bulundurun. Böylece, örneğin bunu yapabilirim 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(); }

Bu şekilde, eğer nesne sadece a olarak bilinirse Base, bu yöntemler kullanımdan kaldırılmıştır ve alt sınıfın metotlarını çağırmak için kendisini farklı bir türe dökmek için kullanılabilir. Bu bana bir şekilde zarif görünüyor, ancak diğer taraftan, bir sınıftaki yöntemleri “kaldırmanın” bir yolu olarak kullanımdan kaldırılmış açıklamaları kötüye kullanıyorum.

Bir Sub1örnek gerçekten bir Temel olduğu için , kapsülleme yerine kalıtımı kullanmak mantıklıdır. İyi yaptığım şey mi? Bu sorunu çözmenin daha iyi bir yolu var mı?


12
Artık her 3 sınıf da birbirini tanımak zorunda. Sub3 ekleme kod bir çok değişiklik yer alacağı ve ekleme Sub10 düpedüz acı verici olurdu
Dan Pichelman

15
Bize gerçek kod verirseniz çok yardımcı olur. Bir şeyin belirli sınıfına dayanarak karar almanın uygun olduğu durumlar vardır, ancak bu gibi örneklerle yaptığınız işte haklı olup olmadığınızı söylemek imkansızdır. Ne pahasına olursa olsun, istediğiniz şey bir Ziyaretçi veya etiketli bir birlikteliktir .
Doval

1
Üzgünüm, basitleştirilmiş örnekler vermenin işleri kolaylaştıracağını düşündüm. Belki ne yapmak istediğimi daha geniş bir kapsamıyla yeni bir soru gönderirim.
codebreaker

12
Siz sadece oyuncu kadrosunu yeniliyorsunuz ve instanceofçok fazla yazmaya ihtiyaç duyan bir şekilde hataya açık ve daha fazla alt sınıf eklemeyi zorlaştırıyorsunuz.
kullanıcı253751

5
Eğer Sub1ve Sub2birbirlerinin yerine kullanılamazlar, o zaman neden bunları özel olarak muamele yapmak? Neden 'sandviç üreticileri' ve 'yabancı-temasçılar'ı ayrı ayrı takip etmiyorsunuz?
Pieter Witvoet

Yanıtlar:


27

Diğer cevapların bazılarında önerildiği gibi, temel sınıfa işlevler eklemek her zaman anlamlı değildir. Çok fazla özel durum işlevi eklemek, ilgisiz bileşenlerin birbirine bağlanmasına neden olabilir.

Mesela ben ve bileşenleri Animalile bir sınıf olabilir . Ben bunları yazmak veya GUI bunları göstermek mümkün olmasını istiyorsanız benim eklemek için overkill olabilir ve temel sınıf için.CatDogrenderToGUI(...)sendToPrinter(...)

Kullandığınız yaklaşım, tür kontrolleri ve yayınları kullanmak kırılgan - ancak en azından endişeleri ayrı tutuyor.

Ancak, sık sık bu tür kontrolleri / yayınları yaparken kendinizi bulursanız, bunun için bir seçenek ziyaretçi / çift gönderme modelini uygulamaktır. Bu gibi görünüyor:

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

Şimdi kodun olur

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

Asıl yarar, bir alt sınıf eklerseniz, sessizce eksik vakalardan ziyade derleme hataları elde etmenizdir. Yeni ziyaretçi sınıfı aynı zamanda fonksiyonları içine çekmek için iyi bir hedef haline geldi. Örneğin, getRandomIngredients()içine taşınmak mantıklı olabilir ActOnBase.

Döngü mantığını da çıkarabilirsiniz: Örneğin, yukarıdaki parça olabilir

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

Java 8'in lambdalarını ve akışını kullanarak biraz daha masaj yapmak ve kullanmanız size

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

Hangi IMO, alabildiğiniz kadar temiz ve özlü bir görünüm.

İşte daha eksiksiz bir Java 8 örneği:

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: Bu tür şeyler, toplam türler ve desen eşleştirme için birinci sınıf desteği olan dillerde yaygın olarak kullanılır ve sözdizimsel olarak çok çirkin olsa da, Java'da geçerli bir tasarımdır.
Mankarse

@ Mayankse Küçük bir sözdizimsel şeker çirkinlikten çok uzaklaştırabilir - Bir örnekle güncelledim.
Michael Anderson,

Varsayımsal olarak, OP fikrini değiştirir ve bir tane eklemeye karar verir Sub3. Ziyaretçilerin de aynı sorunu yok instanceofmu? Şimdi onSub3ziyaretçiye eklemelisiniz ve unutursanız kırılır.
Radiodef

1
@Radiodef Doğrudan yazıyı açmak için ziyaretçi şablonunu kullanmanın bir avantajı onSub3, ziyaretçi arayüzüne ekledikten sonra, henüz güncellenmemiş yeni bir Ziyaretçi oluşturduğunuz her yerde bir derleme hatası elde etmenizdir . Buna karşılık, yazıyı açmak en iyi ihtimalle bir çalışma zamanı hatası üretecektir - bu tetiklenmesi zor olabilir.
Michael Anderson

1
@FiveNine, bu tam örneği başkalarına yardımcı olabilecek cevabın sonuna eklemek isterseniz,
Michael Anderson,

83

Benim açımdan: tasarımın yanlış .

Doğal dile çevrilmiş, aşağıdakileri söylüyorsunuz:

Sahip olduğumuz göz önüne alındığında animals, catsve vardır fish. ve animalsortak özellikleri var . Ancak bu yeterli değil: bazı farklılıklar var , bundan farklı olan , bu nedenle alt sınıfa ihtiyacınız var.catsfishcatfish

Şimdi problemin var, hareketi modellemeyi unuttun . Tamam. Bu nispeten kolay:

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

Ancak bu yanlış bir tasarım. Doğru yol şöyle olurdu:

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

moveHer hayvan tarafından farklı şekilde uygulanan davranış nerede paylaşılır.

Bu yöntemler farklı parametreler alıp tamamen farklı şeyler yaptıkları için tamamen uyumsuzlar ve bu sorunu çözmek için sadece polimorfizm kullanamıyorum.

Bunun anlamı: tasarımınız bozuldu.

Benim tavsiye: refactor Base, Sub1ve Sub2.


8
Eğer gerçek bir kod önerecekseniz, önerilerde bulunabilirim.
Thomas Junk

9
@codebreaker Örneğinizin iyi olmadığını kabul ediyorsanız, bu cevabı kabul etmenizi ve ardından yeni bir soru yazmanızı öneririz. Soruyu farklı bir örnekle tekrar gözden geçirmeyin yoksa mevcut cevaplar bir anlam ifade etmeyecektir.
Moby Disk

16
@codebreaker Bunun asıl soruyu yanıtlayarak sorunu "çözdüğünü" düşünüyorum . Asıl soru şu: Sorunumu nasıl çözerim? Kodunuzu doğru şekilde düzeltin. Refactor o yüzden .move()ya .attack()ve düzgün soyut thatkısmı - yerine sahip .swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home(), vb nasıl düzgün planı ayrı mı? Daha iyi örnekler olmadan bilinmeyen ... ama yine de genelde doğru cevap - örnek kod ve daha fazla ayrıntı aksini önermedikçe.
WernerCD

11
@codebreaker Sınıf hiyerarşiniz sizi bu gibi durumlara yönlendirirse, tanım gereği mantıklı değil
Patrick Collins

7
@codebreaker Crisfole'nin son yorumuyla ilgili olarak, ona yardımcı olabilecek başka bir bakış açısı var. Örneğin, movevs fly/ run/ etc ile bir cümleyle şöyle açıklanabilir: Nesneye ne yapacağını değil, ne yapacağını söylemelisin. Girişimciler açısından, bahsettiğiniz gibi, burada bir yorumdaki gerçek durum gibi, nesnel soruları sormalısınız, durumunu kontrol etmeyin.
Izkata

9

Bir grup şeyinizin olduğu ve bir sandviç yapmalarını veya uzaylılarla iletişim kurmalarını istediğiniz bir durumu hayal etmek biraz zor. Böyle bir döküm bulduğunuz çoğu durumda tek bir türle çalışacaksınız - örneğin, clang'da , listedeki her düğüm için farklı bir şey yapmak yerine, getAsFunction öğesinin boş olmayan döndürdüğü bildirimler için bir düğüm grubunu filtrelersiniz .

Bir dizi eylem gerçekleştirmeniz gerekebilir ve eylemi gerçekleştiren nesnelerin ilişkili olmasıyla ilgili değildir.

Yani bir liste yerine Base, eylemler listesinde çalışın

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

nerede

interface RandomAction {
    void act(Context context);
} 

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

Uygunsa, Base'in eylemi geri döndürmek için bir yöntem kullanmasını veya eylemi temel listenizdeki örneklerden seçmeniz gereken başka bir yolun olmasını sağlayabilirsiniz (polimorfizmi kullanamayacağınızı söylediğiniz için sınıfın bir fonksiyonu değil, üslerin başka bir özelliğidir, aksi takdirde sadece Base (hareket) yöntemini verirsiniz.


2
Benim tür absürt yöntemimin, alt sınıfları bilindiğinde sınıfların neler yapabileceğini (ve birbirleri ile ilgisi olmadığını) göstermek için tasarlandığını anlıyorsunuz, ancak bir Contextnesneye sahip olmanın işleri daha da kötüleştirdiğini düşünüyorum. Ben sadece Objects yöntemlerine geçerek , onları döküm ve doğru tür olduklarını umarak olabilir.
codebreaker

1
@codebreaker, o zaman sorunuzu netleştirmeniz gerekir - çoğu sistemde yaptıkları işlere karşı bir dizi işlevi çağırmak için geçerli bir neden olabilir; örneğin bir olayla ilgili kuralları işlemek veya bir simülasyonda bir adım gerçekleştirmek için. Genellikle bu tür sistemler eylemlerin gerçekleştiği bir içeriğe sahiptir. Eğer 'getRandomIngredients' ve 'getFrequency' ile ilgili değilse, o zaman aynı nesnede olmamalısınız ve eylemlerin bileşenlerin veya sıklığın kaynağını yakalaması gibi farklı bir yaklaşıma ihtiyacınız var.
Pete Kirkham,

1
Haklısın ve bu soruyu kurtarabileceğimi sanmıyorum. Kötü bir örnek seçerek bitirdim, bu yüzden muhtemelen başka bir tane göndereceğim. GetFrequency () ve getIngredients () yalnızca yer tutucuydu. Muhtemelen ne olduğunu bilmeyeceğimi daha açık hale getirmek için argüman olarak "..." koymalıydım.
codebreaker

Bu IMO'nun doğru cevabı. Bunun Contextyeni bir sınıf, hatta bir arayüz olması gerekmediğini anlayın . Genellikle bağlam sadece arayandır, bu yüzden çağrılar formdadır action.act(this). Eğer getFrequency()ve getRandomIngredients()statik yöntemlerdir, hatta bir bağlam gerekmeyebilir. Demek istediğim, şöyle derlerdim, problemin çok fazla gibi olduğu bir iş sırası.
QuestionC

Ayrıca bu çoğunlukla ziyaretçi kalıbı çözümüyle aynıdır. Aradaki fark, Visitor :: OnSub1 () / Visitor :: OnSub2 () yöntemlerini Sub1 :: act () / Sub2 :: act ()
QuestionC

4

Alt sınıflarınızın ne yapabileceklerini tanımlayan bir veya daha fazla arayüz oluşturmasına ne dersiniz? Bunun gibi bir şey:

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

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

O zaman sınıfların şöyle görünecek:

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

Ve for-loop'ınız:

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

Bu yaklaşımın iki büyük avantajı:

  • oyuncu seçimi yok
  • Her bir alt sınıfın, istediğiniz değişiklikler için biraz esneklik sağlayan istediğiniz kadar arabirim oluşturmasını sağlayabilirsiniz.

Not: Arayüzlerin isimleri için üzgünüm, o an için daha serin bir şey düşünemedim: D.


3
Bu aslında sorunu çözmüyor. For döngüsü içinde oyuncu kadrosuna ihtiyacınız var. (Eğer yoksa, OP'nin örneğindeki oyuncu kadrosuna da ihtiyacınız yok).
Taemyr

2

Yaklaşım neredeyse bir aile içinde her tür edecek durumlarda iyi biri olabilir ya bazı kriterini karşılayan bazı arayüzünün bir uygulama olarak doğrudan kullanılabilir olması veya bu arabirimi uygulaması oluşturmak için kullanılabilir. Yerleşik koleksiyon türleri, IMHO'nun bu düzenden faydalanacağını, ancak örneğin amaçlarına uygun olmadığından bir koleksiyon arayüzü icat edeceğim BunchOfThings<T>.

Bazı uygulamaları BunchOfThingsdeğiştirilebilir; bazıları değil. Çoğu durumda, bir nesne Fred'in kullanabileceği bir şeyi tutmak isteyebilir BunchOfThingsve Fred'den başka hiçbir şeyin onu değiştiremeyeceğini biliyor. Bu gereksinim iki şekilde karşılanabilir:

  1. Fred bunun sadece referansları tuttuğunu ve iç dünyasının hiçbir yerinde BunchOfThingsbulunmadığına dair hiçbir dış referans olmadığını biliyor BunchOfThings. Başka hiç kimsenin BunchOfThingsya da onun içlerine referansı yoksa, başka hiç kimse onu değiştiremeyecektir, bu nedenle sınırlama sağlanacaktır.

  2. Ne BunchOfThingsdış referansların var olduğu ne de iç kısımlarından herhangi biri hiçbir şekilde değiştirilemez. Kesinlikle kimse bir BunchOfThingsşeyi değiştiremezse , sınırlama sağlanacaktır.

Kısıtlamayı yerine getirmenin bir yolu, alınan herhangi bir nesneyi koşulsuz olarak kopyalamaktır (iç içe geçmiş bileşenleri yinelemeli olarak işlemek). Bir diğeri, alınan bir nesnenin değişmezlik vaat edip etmediğini test etmek ve olmasa da bir kopyasını çıkarmak ve aynı şekilde iç içe geçmiş bileşenlerle yapmak olacaktır. İkinciden daha temiz ve birinciden daha hızlı olması uygun olan bir alternatif, AsImmutablebir nesnenin değişmeyen bir kopyasını yapmasını isteyen bir yöntem sunmaktır ( AsImmutableonu destekleyen herhangi bir yuvalanmış bileşeni kullanarak ).

İlgili yöntemler ayrıca sağlanabilir asDetached(kod bir nesneyi aldığında ve onu mutasyona geçirmek isteyip istemediğini bilmediğinde kullanılır; bu durumda, değiştirilebilir bir nesne yeni bir değiştirilebilir nesne ile değiştirilmelidir, ancak değişmez bir nesne tutulabilir) olduğu gibi), asMutable(bir nesnenin daha önce geri döndürülen bir nesneyi tutacağını bildiği durumlarda asDetached, yani değişken bir nesneye paylaşılmayan bir referans ya da değiştirilebilen bir nesneye paylaşılabilir bir referans) ya da asNewMutable(kodun dışarıdan aldığı durumlarda buradaki verinin bir kopyasını değiştirmek isteyip istemediğini bilecektir - eğer gelen veriler değişken ise, derhal değiştirilebilen bir kopya oluşturmak için kullanılacak ve sonra bırakılacak olan değiştirilemez bir kopya yaparak başlamak için hiçbir neden yoktur).

asXXYöntemlerin biraz farklı türler getirmesine rağmen, asıl rollerinin döndürülen nesnelerin program gereksinimlerini karşılamasını sağlamak olduğunu unutmayın.


0

İyi bir tasarıma sahip olup olmadığınız konusunu görmezden gelip, iyi ya da en azından kabul edilebilir olduğunu varsayarak, alt sınıfların yeteneklerini dikkate almak isterim, türünü değil.

Bu nedenle, ya:


Sandviçlerin ve uzaylıların varlığına ilişkin bazı bilgileri temel sınıfa taşıyın, üs sınıfın bazı örneklerinin yapamayacağını bilseniz bile. İstisnaları atmak için temel sınıfta uygulayın ve kodu şu şekilde değiştirin:

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

Sonra bir veya her iki alt sınıfları geçersiz kılar canMakeASandwich()ve yalnızca bir uygular her makeASandwich()ve contactAliens().


Bir türün hangi özelliklere sahip olduğunu tespit etmek için somut alt sınıflar yerine arayüzleri kullanın. Temel sınıfı tek başına bırakın ve kodu şu şekilde değiştirin:

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

veya muhtemelen (ve tarzınıza uymuyorsa veya mantıklı olduğunu düşündüğünüz herhangi bir Java stili için bu seçeneği görmezden gelmekten çekinmeyin):

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

Şahsen , YMMV'den bir gelen veya uygunsuz bir şekilde yakalanma riski nedeniyle, genellikle beklenen bir istisna yakalama tarzını sevmem .ClassCastExceptiongetRandomIngredientsmakeASandwich


2
ClassCastException'ı yakalamak yalnızca yenidir. Bu ani bütün amacıdır.
Radiodef

@Radiodef: doğru, ancak amaçlardan biri <yakalamaktan kaçınmak ArrayIndexOutOfBoundsExceptionve bazı insanlar da bunu yapmaya karar veriyor. Tadı yok ;-) Temelde benim "sorunum" Python'da çalışıyorum, burada tercih edilen stil genellikle istisna olup olmayacağını, önceden ne kadar basit olursa olsun herhangi bir istisna yakalamanın önceden test edilmesinin tercih edilmesidir. . Belki de olasılık, Java’da bahsetmeye değmez.
Steve Jessop,

@SteveJessop »Affetmeyi istemek izin almaktan daha kolaydır« sloganı;)
Thomas Junk

0

Burada, kendisini kendi türetilmiş sınıflarına indirgeyen temel bir sınıf vakası var. Çok iyi olduğunu biliyoruz ki bu normalde kötüdür, ancak iyi bir sebep bulduğumuzu söylemek istersen, bunun üzerindeki kısıtlamalara bakalım ve görelim:

  1. Temel sınıftaki tüm davaları ele alabiliriz.
  2. Hiçbir yabancı türetilmiş sınıf, hiç yeni bir vaka eklemek zorunda kalmaz.
  3. Temel sınıfın kontrolü altında olmak için çağrı yapma politikasını yaşayabiliriz.
  4. Ancak bilimimizden, temel sınıfta olmayan bir yöntem için türetilmiş bir sınıfta ne yapılacağına ilişkin politikanın, temel sınıftan değil, türetilmiş sınıftan olduğunu biliyoruz.

Eğer 4 ise, bizde: 5. Türetilmiş sınıfın politikası her zaman temel sınıfın politikası ile aynı siyasi kontrol altındadır.

Her ikisi de 2 ve 5 doğrudan türetilmiş tüm sınıfları numaralandırabileceğimizi ima eder; bu, dışarıdan türetilmiş herhangi bir sınıf olmaması gerektiği anlamına gelir.

Ama işte olay bu. Eğer hepsi seninse if'i sanal yöntem çağrısı olan soyutlama ile değiştirebilirsin (saçmalık olsa bile) ve if ve self-cast oyunundan kurtulabilirsin. Bu yüzden yapma. Daha iyi bir tasarım var.


-1

Birini Sub1'de, diğeri Sub2'de yapan bir soyut metodu koyun ve bu özet metodu karma döngülerinizde kullanı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();
}

Bunun, getRandomIngredients ve getFrequency'ın nasıl tanımlandığına bağlı olarak başka değişiklikler gerektirebileceğini unutmayın. Ancak, dürüst olmak gerekirse, yabancılar ile temasa geçen sınıfın içinde muhtemelen getFrequency'yi tanımlamak için daha iyi bir yer var.

Bu arada, asSub1 ve asSub2 yöntemleriniz kesinlikle kötü bir uygulamadır. Bunu yapacaksan, oyuncu seçerek yap. Bu yöntemlerin size dökümde vermeyeceği hiçbir şey yok.


2
Cevabınız soruyu tamamen okumadığınızı gösteriyor: polimorfizm burada kesmiyor. Sub1 ve Sub2'nin her birinde birkaç yöntem vardır, bunlar sadece örneklerdir ve "doAThing" gerçekten bir şey ifade etmez. Yapacağım hiçbir şeye karşılık gelmiyor. Ek olarak, son cümlenizde olduğu gibi: garantili tip güvenliği?
codebreaker

@codebreaker - Örnekteki döngüde ne yaptığınıza tam olarak karşılık gelir. Bunun bir anlamı yoksa, döngüyü yazmayın. Eğer bir yöntem olarak bir anlam ifade etmiyorsa, bir döngü gövdesi olarak bir anlam ifade etmez.
Random832

1
Ve yöntemleriniz tip güvenliğini "döküm" yöntemiyle "garanti eder": nesne yanlış tip ise bir istisna atarak.
Random832

Kötü bir örnek seçtiğimin farkındayım. Sorun, alt sınıflardaki yöntemlerin çoğu, mutasyonel yöntemlerden ziyade alıcılar gibidir. Bu sınıflar işleri kendileri yapmazlar, sadece veri sağlarlar. Polimorfizm bana orada yardımcı olmaz. Ben üreticilerle ilgileniyorum, tüketicilerle değil. Ayrıca: bir istisna atmak, derleme zamanı kadar iyi olmayan bir çalışma zamanı garantisidir.
codebreaker

1
Demek istediğim , kodunuz yalnızca çalışma zamanı garantisi de veriyor. AsSub2'yi aramadan önce birinin isSub2'yi kontrol etmesini engelleyen hiçbir şey yok.
Random832

-2

Hem itmek olabilir makeASandwichve contactAlienstemel sınıf içine ve daha sonra kukla uygulamaları ile bunları uygulamak. Çünkü return türleri / argümanlar ortak bir temel sınıfa çözülemez.

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

Yöntemin sözleşmesi için ne anlama geldiği ve sonucun bağlamı hakkında çıkarılamadığınız ve yapamayacağınız şeyler gibi bu tür şeyler için belirgin dezavantajlar vardır. Bir sandviç yapamadığım için malzemenin teşebbüsünde kullanılmış vb.


1
Bunu yapmayı düşündüm, ama Liskov Değişim İlkesini ihlal ediyor ve örneğin, "alt1" yazdığınızda çalışmak kadar hoş değil. ve otomatik tamamlama gelirse, makeASandwich'i görürsünüz ama ContactAliens ile görüşmezsiniz.
codebreaker

-5

Yaptıkların tamamen meşru. Sadece dogmayı yineleyen naytarlara dikkat etmeyin, çünkü bazı kitaplarda okurlar. Dogma'nın mühendislikte yeri yok.

Aynı mekanizmayı birkaç kez kullandım ve java çalışma zamanının aynı şeyi en az bir yerde de yapabileceğini ve böylece kodun performansını, kullanılabilirliğini ve okunabilirliğini artırabileceğini güvenle söyleyebilirim. kullanır.

Örneğin java.lang.reflect.Member, hangi java.lang.reflect.Fieldvejava.lang.reflect.Method . (Gerçek hiyerarşi, bundan biraz daha karmaşıktır, ancak bu alakasızdır.) Alanlar ve yöntemler çok farklı hayvanlardır: birinin alabileceği ya da ayarlayabileceği bir değeri vardır, diğeri böyle bir şeye sahip değildir, ancak çağrılabilir. bir dizi parametre ve bir değer döndürebilir. Dolayısıyla, alanlar ve yöntemler her ikisi de üyelerdir, ancak onlarla yapabileceğiniz şeyler, sandviç yapmak ve uzaylılarla iletişim kurmak kadar birbirinden farklıdır.

Şimdi, yansıma kullanan kod yazarken sık sık Memberbizim elimizde ve bir Methodya da bir Field(veya nadiren başka bir şey) olduğunu biliyoruz ve yine de instanceofkesin olarak anlamak için tüm sıkıcıları yapmamız gerektiğini biliyoruz. ne olduğu ve ona uygun bir referans almak için onu atmamız gerekiyor. (Ve bu sadece sıkıcı değil, aynı zamanda çok iyi bir performans sergiliyor.)Method Sınıf, tarif ettiğiniz modeli çok kolay bir şekilde uygulayabilir ve böylece binlerce programcının hayatını kolaylaştırabilirdi.

Tabii ki, bu teknik sadece yakından bağlı kaynak sınıflarının kontrolüne sahip olduğunuz (ve her zaman olacaktır) yakın, eşleşmiş sınıfların küçük, iyi tanımlanmış hiyerarşileri için geçerlidir: Sınıf hiyerarşiniz varsa, böyle bir şey yapmak istemezsiniz. temel sınıfı yeniden düzenlemek için özgür olmayan insanlar tarafından genişletilebilir.

İşte yaptığım şey, yaptığınız şeyden nasıl farklıydı:

  • Temel sınıf, asDerivedClass()her birinin geri dönmesiyle , tüm yöntemler ailesi için varsayılan bir uygulama sağlar null.

  • Her türetilmiş sınıf asDerivedClass(), thisyerine dönen yöntemlerden yalnızca birini geçersiz kılar null. Geri kalanların hiçbirini geçersiz kılmaz ve onlar hakkında hiçbir şey bilmek istemez. Yani, hiç IllegalStateExceptionatılmadı.

  • Temel sınıf ayrıca, aşağıdaki gibi kodlanan finaltüm isDerivedClass()yöntem ailesi için uygulamalar sağlar : return asDerivedClass() != null; Bu şekilde, türetilmiş sınıflar tarafından geçersiz kılınması gereken yöntem sayısı en aza indirilir.

  • Ben kullanıyorum henüz @Deprecatedbunu düşünmedim çünkü bu mekanizmada. Şimdi bana fikri verdiğine göre, onu kullanacağım, teşekkürler!

C # asanahtar sözcüğün kullanımıyla ilgili bir mekanizmaya sahiptir . C # 'da söyleyebilirsiniz DerivedClass derivedInstance = baseInstance as DerivedClassve DerivedClasseğer baseInstanceo sınıftan biriyseniz ya da nullolmasaydı bir referans alacaksınız . Bu (teorik olarak) isdökümden daha iyi performans gösterir , ( iskuşkusuz C # anahtar kelimesi olarak adlandırılır instanceof), ancak el işçiliği yaptığımız özel mekanizma daha da iyi bir performans sergiler: instanceofJava’nın döküm işlemlerinin çifti olarak asC # operatörü bizim özel yaklaşımının tek sanal yöntem çağrısı kadar hızlı yapmazlar.

Bu tekniğin bir kalıp olarak ilan edilmesi gerektiği ve bunun için güzel bir adın bulunması gerektiği önerisini ortaya koyuyorum .

Gee, oyların için teşekkürler!

Söylemenin bir özeti, sizi yorumları okuma zorluğundan kurtarmak için:

İnsanların itirazı orijinal tasarımın yanlış olduğu, yani hiçbir zaman ortak bir temel sınıftan türeyen çok farklı sınıflara sahip olmamanız gerektiği ya da böyle bir hiyerarşiyi kullanan kodun asla sahip olma konumunda olmaması gerektiği anlamına gelir. türetilmiş sınıfı bulmak için bir temel referans ve ihtiyaç. Bu nedenle, bu sorunun ve özgün tasarımın kullanımını iyileştiren cevabımın önerdiği kendi kendini ifade etme mekanizmasının, hiçbir zaman, ilk önce gerekli olmadıklarını söylüyorlar. (Kendi kendini döküm mekanizmasının kendisi hakkında gerçekten hiçbir şey söylemezler, yalnızca mekanizmanın uygulanması gereken tasarımların doğasından şikayet ederler.)

Ancak, I yukarıdaki örnekte var zaten java çalışma zamanı yaratıcıları gerçeği için tam böyle bir tasarım tercih yaptığı göstermiştir java.lang.reflect.Member, Field, Methodhiyerarşi ve yorumlarda aşağıda da C # zamanında yaratıcıları bağımsız bir geldi göstermektedir eşdeğer tasarım System.Reflection.MemberInfo, FieldInfo, MethodInfohiyerarşi. Dolayısıyla, bunlar, herkesin burnunun altında oturan ve tam olarak bu tür tasarımları kullanarak gözle görülür şekilde uygulanabilir çözümler sunan iki farklı gerçek dünya senaryosudur.

Aşağıdaki tüm yorumlar bu kadar kaynıyor. Kendi kendine döküm mekanizmasından pek bahsedilmez.


13
Kendini bir kutuya göre tasarladıysan bu meşru. Bu tür soruların çoğunda sorun budur. İnsanlar, tasarımın diğer alanlarında, asıl çözüm, bu durumda niçin ilk sırada yer aldığınızı bulmak olduğunda bu tür çirkin çözümleri zorlayan kararlar alır. İyi bir tasarım seni buraya getiremez. Şu anda bir 200K SLOC uygulamasına bakıyorum ve bunu yapmak için ihtiyaç duyduğumuz herhangi bir yeri göremiyorum. Yani bu sadece insanların bir kitapta okuduğu bir şey değil. Bu tür durumlara girmemek, iyi uygulamaların izlenmesinin bir sonucudur.
Dunk

3
@Dunk Az önce böyle bir mekanizmaya duyulan ihtiyacın örneğin java.lang.reflection.Memberhiyerarşide var olduğunu gösterdim . Java çalışma zamanının yaratıcıları bu tür bir duruma girdiler, sanırım kendilerini bir kutuya göre tasarladıklarını söyleyemezsiniz ya da bir tür en iyi uygulamayı takip etmedikleri için onları suçlarsınız? Bu yüzden burada yaptığım nokta, bu tür bir duruma sahip olduğunuzda bu mekanizmanın işleri iyileştirmenin iyi bir yoludur.
Mike Nakis

6
@MikeNakis Java'nın yaratıcıları çok fazla kötü kararlar verdiler, bu yüzden tek başına pek bir şey söylemedi. Her halükarda, bir ziyaretçiyi kullanmak, özel bir test-sonra-yayın yapmaktan daha iyi olurdu.
Doval

3
Ek olarak, örnek hatalı. Bir Memberarabirim var, ancak yalnızca erişim niteleyicilerini denetlemeye yarar. Genellikle, yöntemlerin veya özelliklerin bir listesini alırsınız ve doğrudan kullanın (bunları karıştırmadan, bu nedenle List<Member>görünmez), böylece döküm yapmanıza gerek kalmaz. ClassBir getMembers()yöntem sağlamadığını unutmayın . Tabii ki, bilgisiz bir programcı yaratabilir List<Member>, ama bu bir yapım kadar az anlamda olarak yapacak List<Object>ve bazı ekleme Integerveya Stringbuna.
SJuan76

5
@MikeNakis "Bir çok insan bunu yapıyor" ve "Ünlü insan bunu yapıyor" gerçek tartışmalar değil. Antipatterns hem kötü fikirler hem de tanım olarak yaygın şekilde kullanılır, ancak bu bahaneler kullanımlarını haklı çıkarır. Bazı tasarım veya başka kullanmak için gerçek nedenleri verin. Geçici yayınlama ile ilgili sorunum, API'nizin her kullanıcısı, her defasında oyuncu seçimi yapmak zorundadır. Bir ziyaretçinin yalnızca bir kez doğru olması gerekir. Döküm işlemini bazı yöntemlerin arkasına gizlemek ve nullbir istisna yerine geri dönmek çok daha iyi olamaz. Diğer herkes eşit, ziyaretçi aynı işi yaparken daha güvenlidir.
Doval
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.