Bir yöntemin geçersiz sayılabileceğini, bir yöntemin çağrılabileceğini tanımlamaktan daha güçlü bir bağlılık olarak tanımlamak nasıl olabilir?


36

Gönderen: http://www.artima.com/lejava/articles/designprinciples4.html

Erich Gamma: Hala on yıl sonra bile doğru olduğunu düşünüyorum. Kalıtım, davranışı değiştirmek için harika bir yoldur. Ancak bunun kırılgan olduğunu biliyoruz, çünkü alt sınıf, geçersiz kıldığı bir yöntemin çağrıldığı bağlam hakkında kolayca varsayımlarda bulunabilir. Taktığım alt sınıf kodunun çağrılacağı dolaylı bağlam nedeniyle, temel sınıf ve alt sınıf arasında sıkı bir bağlantı var. Kompozisyon daha güzel bir özelliğe sahiptir. Kaplin, daha büyük bir şeye taktığınız küçük şeylere sahip olmakla azalır ve daha büyük olan nesne daha küçük olan nesneyi geri çağırır. Bir API açısından, bir yöntemin geçersiz kılınabileceğini tanımlamak, bir yöntemin çağrılabileceğini tanımlamaktan daha güçlü bir taahhüttür.

Ne demek istediğini anlamadım. Birisi lütfen açıklayabilir mi?

Yanıtlar:


63

Bir taahhüt gelecekteki seçeneklerinizi azaltan bir şeydir. Bir yöntem yayınlamak, kullanıcıların arayacağı anlamına gelir, bu nedenle bu yöntemi uyumluluktan kaçmadan kaldıramazsınız. Eğer onu tutsaydın private, direkt olarak arayamazlardı ve bir gün problemsizce yeniden ateşlendirebilirdin. Bu nedenle, bir yöntemi yayınlamak, yayınlamamaktan daha güçlü bir taahhüttür. Bir Yayıncılık geçersiz kılınabilir yöntemini daha da güçlü bir taahhüttür. Kullanıcılarınız arayabilir ve yöntemin sizin yaptığınızı düşündüğü şeyi yapmadığı yeni sınıflar oluşturabilirler!

Örneğin, bir temizleme yöntemi yayınlarsanız, kullanıcılar bu yöntemi en son yaptıkları şey olarak adlandırdıklarını hatırladıkları sürece kaynakların uygun şekilde dağıtılmasını sağlayabilirsiniz. Ancak yöntem geçersiz kılınabilirse, birisi onu alt sınıfta geçersiz kılabilir ve çağrı yapamazsuper . Sonuç olarak, üçüncü bir kullanıcı, bu sınıfı kullanabilir ve en sonunda dürüstçe çağırılmış olsalar bilecleanup() bir kaynak sızıntısına neden olabilir ! Bu, kodunuzun anlamını artık garanti edemeyeceğiniz anlamına gelir, bu çok kötü bir şeydir.

Temel olarak, artık kullanıcı tarafından geçersiz kılınan yöntemlerle çalışan hiçbir koda güvenemezsiniz, çünkü bazı aracılar onu geçersiz kılabilir. Bu, temizleme yordamınızı private, kullanıcının yardımı olmadan, tamamen yöntemlerle uygulamanız gerektiği anlamına gelir . Bu nedenle final, açıkça açıkça, API kullanıcıları tarafından geçersiz kılmaya yönelik olmadıkça, yalnızca öğeleri yayınlamak iyi bir fikirdir .


11
Bu muhtemelen şimdiye kadar okuduğum kalıtımla ilgili en iyi argüman. Karşılaştığım aleyhte bütün nedenlerden dolayı, daha önce hiç bu iki argümanla karşılaşmamıştım.
David Arno,

5
@DavidArno Mirasa karşı bir argüman olduğunu sanmıyorum. Bence "varsayılan olarak her şeyi geçersiz kılabilir yap" a karşı bir argüman. Kalıtım kendi başına tehlikeli değildir, düşüncesiz kullanmaktır.
svick 19 Aralık'ta

15
Bu iyi bir nokta gibi görünse de, "bir kullanıcının kendi buggy kodunu ekleyebilir" in nasıl bir argüman olduğunu gerçekten göremiyorum. Kalıtımın etkinleştirilmesi, kullanıcıların hataları önleyebilecek ve düzeltebilecek bir ölçü olan güncellenebilirliği kaybetmeden işlevsellik eklemelerine izin verir. Bir kullanıcı API'nizin üstündeki bir kodun kırılması durumunda, bir API hatası değildir.
Sebb

4
Bu argümanı kolayca etrafına çevirebilirsiniz: ilk kodlayıcı temizleme argümanı yapar, ancak hata yapar ve her şeyi temizlemez. İkinci kodlayıcı temizleme yöntemini geçersiz kılar ve iyi bir iş çıkarır ve kodlayıcı # 3 sınıfı kullanır ve kodlayıcı # 1 karışıklık çıkarsa da herhangi bir kaynak sızıntısı yaşamaz.
Pieter B,

6
@Doval Gerçekten. Bu nedenle mirasın hemen hemen her tanıtıcı OOP kitabı ve sınıfında bir numaralı ders olması bir travesti.
Kevin Krumwiede

30

Normal bir işlev yayınlarsanız, tek taraflı bir sözleşme yaparsınız :
İşlev çağrıldığında ne yapar?

Bir geri arama yayınlarsanız, tek taraflı bir sözleşme de yaparsınız:
Ne zaman ve nasıl aranır?

Ve eğer geçersiz bir işlev yayınlarsanız, her ikisi de bir kerede gerçekleşir, bu yüzden iki taraflı bir sözleşme yaparsınız:
Ne zaman çağrılacak ve çağrıldığında ne yapmalı?

Kullanıcılarınız API’nizi kötüye kullanmasalar bile ( sözleşmenin bir bölümünü kırarak , algılaması çok pahalı olabilir), ikincisinin çok daha fazla belgeye ihtiyacı olduğunu ve belgelendirdiğiniz her şeyin bir sınırlama olduğunu kolayca görebilirsiniz. Diğer seçenekleriniz.

Böyle bir iki taraflı sözleşme dönmekle bir örnek ile ilgili bir hareket showve hidehiç setVisible(boolean)de java.awt.Component .


+1. Diğer cevabın neden kabul edildiğinden emin değilim; bazı ilginç noktalara değiniyor, ancak bu sorunun kesinlikle doğru cevabı değil, bu nedenle alıntı olarak geçenlerin kesinlikle ne anlama geldiği değil.
18'de

Bu doğru cevap, ancak örneği anlamıyorum. Şovu değiştirmek ve setVisible (boolean) ile gizlemek miras almayan kodu da bozuyor. Bir şey mi eksik?
eigensheep

3
@ eigensheep: showve hidehala var, onlar sadece @Deprecated. Bu nedenle, değişiklik yalnızca onları çağıran hiçbir kodu kırmaz. Ancak, onları geçersiz kılarsanız, geçersiz kılmalar yeni 'setVisible'a geçen müşteriler tarafından aranmaz. (Swing'i hiç kullanmadım, bu yüzden bunları geçersiz kılmanın ne kadar yaygın olduğunu bilmiyorum; ancak çok uzun zaman önce gerçekleştiğinden Deduplicator'ın onu acı çektiğini hatırlamasının nedenini hayal ediyorum.)
ruakh

12

Kilian Foth'un cevabı mükemmel. Bunun neden bir sorun olduğuna ilişkin kanonik bir örnek * eklemek isterim. Bir tamsayı Point sınıfı düşünün:

class Point2D {
    public int x;
    public int y;

    // constructor
    public Point2D(int theX, int theY) { x = theX; y = theY; }

    public int hashCode() { return x + y; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point2D) ) { return false; }

        Point2D that = (Point2D) o;

        return (x == that.x) &&
               (y == that.y);
    }
}

Şimdi onu 3B nokta olarak sınıflandıralım.

class Point3D extends Point2D {
    public int z;

    // constructor
    public Point3D(int theX, int theY, int theZ) {
        super(x, y); z = theZ;
    }

    public int hashCode() { return super.hashCode() + z; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point3D) ) { return false; }

        Point3D that = (Point3D) o;

        return super.equals(that) &&
               (z == that.z);
    }
}

Süper basit! Puanlarımızı kullanalım:

Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);

p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false

Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);

p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false

Muhtemelen neden bu kadar kolay bir örnek gönderdiğimi merak ediyorsundur. İşte yakalamak:

p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!

2B noktasını eşdeğer 3B noktayla karşılaştırdığımızda, gerçek oluyoruz, ancak karşılaştırmayı tersine çevirdiğimizde, yanlış yapıyoruz (çünkü p2a başarısız oluyor instanceof Point3D).

Sonuç

  1. Bir yöntemi, bir alt sınıfta, süper sınıfın çalışmasını beklediği zamanla artık uyumlu olmayacak şekilde uygulamak mümkündür.

  2. Eşit () 'i, üst sınıfıyla uyumlu bir şekilde, çok farklı bir alt sınıf üzerine uygulamak genellikle imkansızdır.

İnsanların alt sınıfa izin vermeyi düşündüğünüz bir sınıf yazdığınızda, her yöntemin nasıl davranması gerektiğine dair bir sözleşme yazmak gerçekten iyi bir fikirdir . Daha da iyisi, insanların sözleşmeyi ihlal etmediklerini kanıtlamak için geçersiz kılınan yöntemlerin uygulanmasına karşı koyabilecekleri bir dizi test olacaktır. Neredeyse kimse bunu yapmaz çünkü çok fazla iş var. Ama umursarsan, yapılacak şey budur.

İyi yazılmış bir sözleşmenin harika bir örneği Karşılaştırıcı'dır . .equals()Yukarıda açıklanan nedenlerden dolayı ne yazdığını görmezden gel . İşte Comparator'un işleri .equals()yapamayacağının bir örneği .

notlar

  1. Josh Bloch'un "Etkili Java" Öğesi 8, bu örneğin kaynağıydı, ancak Bloch, üçüncü bir eksen yerine bir renk ekleyen ve inç yerine çiftler kullanan bir ColorPoint kullanıyor. Bloch'un Java örneği, örneklerini çevrimiçi ortamda kullanıma sunan Odersky / Spoon / Venners tarafından çoğaltılmıştır .

  2. Birkaç kişi bu örneğe itiraz etti çünkü üst sınıfa alt sınıf hakkında bilgi verirseniz, bu sorunu çözebilirsiniz. Yeterince az sayıda alt sınıf varsa ve eğer ebeveyn onları biliyorsa, bu doğrudur. Ancak asıl soru, başka birinin alt sınıflar yazacağı bir API yapmaktı. Bu durumda, genellikle üst uygulamayı alt sınıflarla uyumlu olacak şekilde güncelleyemezsiniz.

Bonus

Kıyaslayıcı da ilginç çünkü eşittir () düzgün bir şekilde uygulama sorunu etrafında çalışır. Daha da iyisi, bu tür bir miras sorununu çözmek için bir kalıp izler: Strateji tasarım kalıbı. Haskell ve Scala halkının heyecanlandığı Tipeclasses da Strateji kalıbı. Kalıtım kötü ya da yanlış değil, sadece zor. Daha fazla okumak için, Philip Wadler'in makalesine bakın. Geçici polimorfizm nasıl daha az geçici hale getirilir ?


1
SortedMap ve SortedSet aslında equalsMap ve Set'in bunu nasıl tanımladığının tanımlarını değiştirmiyor . Eşitlik, emri tamamen görmezden gelir; örneğin, aynı elemanlara sahip iki SortedSet'in ancak farklı sıralama emirlerinin hala eşit olduğunu karşılaştırın.
user2357112 18 Aralık'ta Monica

1
@ user2357112 Haklısınız ve bu örneği kaldırdım. SortedMap.equals () 'in Harita ile uyumlu olması, şikayet etmeye devam edeceğim ayrı bir konudur. SortedMap genellikle O (log2 n) ve HashMap (haritanın kurallı uygulaması) O (1) 'dir. Bu nedenle, bir SortedMap’i yalnızca sipariş vermeyi gerçekten önemsiyorsanız kullanırsınız. Bu nedenle, sıranın SortedMap uygulamalarındaki equals () testinin kritik bir bileşeni olacak kadar önemli olduğuna inanıyorum. Eşit () bir uygulamayı Map ile paylaşmamalıdırlar (Java'daki AbstractMap ile yaparlar).
GlenPeterson

3
"Miras fena ya da yanlış değil, sadece zor." Ne dediğini anlıyorum, ama zor şeyler genellikle hatalara, hatalara ve sorunlara yol açar. Aynı şeyleri (veya hemen hemen aynı şeyleri) daha güvenilir bir şekilde gerçekleştirebildiğiniz zaman, daha zor olan da kötüdür.
jpmc26

7
Bu çok kötü bir örnek Glen. Mirası, kullanılmaması gereken şekilde kullandınız, sınıfların istediğiniz şekilde çalışmamasına şaşmamalı. Yanlış bir soyutlama (2B noktası) sağlayarak Liskov'un ikame prensibini bozdunuz, ancak sadece kalıtımın yanlış örneğinizde kötü olması, genel olarak kötü olduğu anlamına gelmez. Her ne kadar bu cevap makul görünse de, sadece en temel kalıtım kurallarını ihlal ettiğini anlamayan insanları şaşırtacaktır.
Andy

3
Liskov'un İkame İlkesi'nin ELI5'i şöyle diyor: Bir sınıf bir sınıfın BçocuğuABB olsaydı ve bir sınıfın nesnesini başlatırsanız , sınıf nesnesini ebeveyni için kullanabilmeli ve herhangi bir uygulama detayını kaybetmeden döküm değişkeninin API'sini kullanabilmelisiniz. çocuk. Üçüncü özelliği sağlayarak kuralı çiğnediniz. Temel sınıfın böyle bir özelliği olduğu hakkında hiçbir fikri olmadığı zaman değişkeni zyayınladıktan sonra koordinatlara nasıl erişmeyi planlıyorsunuz ? Bir alt sınıfa temeli atmak, herkese açık API'yi çiğnemeniz durumunda, soyutlamanız yanlıştır. Point3DPoint2D
Andy

4

Kalıtım Kapsüllemeyi Zayıflatır

Miras alınmasına izin verilen bir arabirim yayınladığınızda, arabiriminizin boyutunu büyük ölçüde artırırsınız. Her geçersiz kılınabilir yöntem değiştirilebilir ve bu nedenle yapıcıya verilen geri arama olarak düşünülmelidir. Sınıfınız tarafından sağlanan uygulama sadece geri çağırın varsayılan değeridir. Bu nedenle, yöntemle ilgili beklentilerin ne olduğunu gösteren bir tür sözleşme yapılmalıdır. Bu nadiren olur ve nesneye yönelik kodun kırılgan olarak adlandırılmasının ana nedenidir.

Aşağıda, Peter Norvig'in ( http://norvig.com/java-iaq.html ) izniyle, java koleksiyonları çerçevesinden gerçek (basitleştirilmiş) bir örnek verilmiştir .

Public Class HashTable{
    ...
    Public Object put(K key, V value){
        try{
            //add object to table;
        }catch(TableFullException e){
            increaseTableSize();
            put(key,value);
        }
    }
}

Peki, bunu alt sınıflarsak ne olur?

/** A version of Hashtable that lets you do
 * table.put("dog", "canine");, and then have
 * table.get("dogs") return "canine". **/

public class HashtableWithPlurals extends Hashtable {

    /** Make the table map both key and key + "s" to value. **/
    public Object put(Object key, Object value) {
        super.put(key + "s", value);
        return super.put(key, value);
    }
}

Bir hatamız var: Bazen "köpek" ekliyoruz ve karma "köpekler" için bir giriş alıyor. Bunun nedeni, Hashtable sınıfını tasarlayan kişinin beklemediği bir uygulama ortaya koyan biriydi.

Kalıtım Genişletilebilirliği Kırıyor

Sınıfınızın alt sınıflara alınmasına izin verirseniz, sınıfınıza hiçbir yöntem eklememeyi taahhüt ediyorsunuz. Bu, aksi takdirde hiçbir şey kırılmadan yapılabilir.

Bir arabirime yeni yöntemler eklediğinizde, sınıfınızdan miras alan herkesin bu yöntemleri uygulaması gerekir.


3

Bir yöntemin çağrılması gerekiyorsa, yalnızca doğru şekilde çalıştığından emin olmanız gerekir. Bu kadar. Bitti.

Bir yöntem geçersiz kılmak için tasarlanmışsa, yöntemin kapsamı hakkında da dikkatlice düşünmeniz gerekir: kapsam çok büyükse, alt sınıfın çoğunlukla kopyalanan kodu ana yöntemden kopyalaması gerekir; Çok küçükse, istenen yeni fonksiyonelliğe sahip olmak için birçok yöntemin geçersiz kılınması gerekir - bu karmaşıklık ve gereksiz hat sayısı ekler.

Bu nedenle ebeveyn yönteminin yaratıcısının, sınıfın ve yöntemlerinin gelecekte nasıl geçersiz kılınabileceği konusunda varsayımlarda bulunması gerekir.

Ancak, yazar alıntı metinde farklı bir konu hakkında konuşuyor:

Ancak bunun kırılgan olduğunu biliyoruz, çünkü alt sınıf, geçersiz kıldığı bir yöntemin çağrıldığı bağlam hakkında kolayca varsayımlarda bulunabilir.

aNormalde yöntemden çağrılan b, ancak bazı nadir ve açıkça görülmeyen durumlarda yöntemden çağrılan yöntemi düşünün c. Eğer geçersiz kılma metodunun yazarı cmetodu ve beklentilerini gözardı ederse a, işlerin nasıl ters gidebileceği açıktır.

Bu nedenle a, açıkça ve açıkça tanımlanmış, iyi belgelenmiş, "bir şeyi yapar ve iyi yapar" olarak tanımlanması daha çok önemlidir - daha çok sadece çağrılmak için tasarlanmış bir yöntem olsaydı.

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.