Bir sınıfı kendi listesinin alt sınıfı olarak tanımlamanın dezavantajı ne olabilir?


48

Yakın tarihli bir projemde, aşağıdaki başlığa sahip bir sınıf tanımladım:

public class Node extends ArrayList<Node> {
    ...
}

Bununla birlikte, CS profesörümle konuştuktan sonra, sınıfın "hafıza için korkunç" ve "kötü uygulama" olacağını belirtti. İlkini özellikle doğru, ikincisini öznel olarak bulamadım.

Bu kullanım için gerekçem, bir örneğin davranışının ya özel bir uygulama tarafından ya da etkileşime giren birkaç benzer nesnenin davranışı tarafından tanımlanabileceği, keyfi derinliğe sahip bir şey olarak tanımlanması gereken bir nesne için bir fikrim olduğunu düşünüyorum. . Bu, fiziksel uygulaması etkileşime giren birçok alt-bileşenden oluşan nesnelerin soyutlanmasına izin verecektir.

Öte yandan, bunun nasıl kötü bir uygulama olabileceğini görüyorum. Bir şeyi kendi listesi olarak tanımlama fikri basit veya fiziksel olarak uygulanabilir değildir.

Neden herhangi bir geçerli sebep var mı olmamalı bunun için benim kullanımı dikkate alınarak, benim kodunda kullanacağız?


This Bunu daha fazla açıklamaya ihtiyacım olursa, memnun olurum; Ben sadece bu soruyu kısa tutmaya çalışıyorum.



1
@gnat Bunun, bir sınıfı, liste içeren listeleri içermekten ziyade, kesinlikle bir sınıfı kendi listesi olarak tanımlamakla ilgisi vardır. Bence benzer bir şey varyantı, ama tamamen aynı değil. Bu soru, Robert Harvey'in cevabı boyunca bir şeye işaret ediyor .
Addison Crump

16
C ++ ' da sık kullanılan CRTP deyimini gösterdiği gibi, kendisi tarafından parametrelendirilen bir şeyden devralmak olağandışı değildir . Bununla birlikte, kap söz konusu olduğunda, asıl soru, kompozisyon yerine neden devralma kullanmanız gerektiğinin haklı çıkmasıdır.
Christophe

1
Fikri beğendim. Sonuçta, bir ağaçtaki her düğüm bir (alt) ağaçtır. Genellikle bir düğümün ağacı, bir tip görünümden gizlenir (klasik düğüm bir ağaç değil tek bir nesnedir) ve bunun yerine veri görünümünde ifade edilir (tek düğüm dallara erişime izin verir). İşte tam tersi; Şube verilerini görmeyeceğiz, bu türde. İronik olarak, C ++ 'daki gerçek nesne düzeni büyük olasılıkla çok benzer olacaktır (kalıtım, veri içerenlere çok benzer şekilde uygulanır), bu da aynı şeyi ifade etmenin iki yoluyla karşı karşıya olduğumuzu düşündürüyor.
Peter - Monica

1
Belki biraz eksik ama cidden gerekiyor uzatmak ArrayList<Node> karşılaştırıldığında uygulamak List<Node> ?
Matthias

Yanıtlar:


107

Açıkçası, burada mirasa olan ihtiyacı göremiyorum. Mantıklı değil; Node Bir olan ArrayList bir Node?

Bu sadece özyinelemeli bir veri yapısı ise, basitçe şöyle bir şey yazacaksınız:

public class Node {
    public String item;
    public List<Node> children;
}

Hangi yapar mantıklı; düğümün bir alt veya alt düğüm listesi var.


34
Aynı şey değil. Bir liste ad-infinitum listesi, listede yeni bir listeden başka bir şey kaydetmediğiniz sürece anlamsızdır . Bunun Item için var Itemve listelerden bağımsız olarak çalışıyor.
Robert Harvey

6
Oh, şimdi anlıyorum. Ben yaklaştı vardı Node bir olduğu ArrayList bir Nodeşekilde Node 0 çoğuna içeriyor Node nesneler. İşlevleri temelde aynıdır, ancak benzer nesnelerin bir listesi olmak yerine , benzer nesnelerin bir listesine sahiptir . Yazılan sınıf için özgün tasarım , her ikisinin de uyguladığı Nodebir altkümeler kümesi olarak ifade edilebileceği Node, ancak bunun kesinlikle daha az kafa karıştırıcı olduğu inancıydı. +1
Addison Crump

11
Bir düğüm ile bir liste arasındaki kimliğin, C veya Lisp'te tek başına bağlanan listeler yazdığınızda ya da her neyse: “doğal olarak” ortaya çıkma eğiliminde olduğunu söyleyebilirim: bazı tasarımlarda ana düğüm, tek olma anlamında “liste” dir. bir şey hiç bir listeyi temsil etmek için kullanılır. Bu nedenle, bazı bağlamlarda belki de bir düğümün, "EvilBadWrong'u Java'da hissettiğini kabul etmeme rağmen," bir düğümün "" (sadece "sahip olmadığı") olduğu hissini tamamen ortadan kaldıramam.
Steve Jessop

12
@ nl-x Bir sözdiziminin daha kısa olması, daha iyi olduğu anlamına gelmez. Ayrıca, tesadüfen, böyle bir ağaçtaki öğelere erişmek oldukça nadirdir. Genellikle yapı derleme zamanında bilinmez, bu yüzden sabit ağaçları kullanarak bu ağaçları geçme eğiliminde değilsiniz. Derinlik de sabit değildir, bu yüzden sözdizimini kullanarak tüm değerler üzerinde yineleme yapamazsınız. Kodu geleneksel bir ağaç geçiş problemini kullanmak için uyarladığınızda Children, yalnızca bir veya iki kez yazmaya başlarsınız ve bu gibi durumlarda kodun açık olması için yazması tercih edilir.
Servi


57

“Hafıza için korkunç” argümanı tamamen yanlıştır, ancak nesnel olarak “kötü bir uygulama” dır . Bir sınıftan miras aldığınızda, yalnızca ilgilendiğiniz alanları ve yöntemleri miras almazsınız. Bunun yerine her şeyi miras alırsınız . Sizin için yararlı olmasa bile, ilan ettiği her yöntem. Ve en önemlisi, ayrıca sınıfın sağladığı tüm sözleşmeleri ve garantileri devralırsınız.

SOLID kısaltması, iyi nesne yönelimli tasarım için bazı sezgiler sunar. Burada, ben nterface Ayrışma İlkesi (ISS) ve L iskov değişikliği Pricinple (LSP) bir şey söylemek.

ISS bize arayüzlerimizi mümkün olduğunca küçük tutmamızı söylüyor. Ancak miras alarak ArrayList, birçok, çok yöntem alırsınız. O anlamlı mı get(), remove(), set()(replace) veya add()belirli bir dizinde (insert) bir alt düğümü? ensureCapacity()Altta yatan listeye duyarlı mı ? sort()Bir Düğüm için ne anlama geliyor ? Sınıfınızın kullanıcılarının gerçekten bir alması gerekiyor subList()mu? İstemediğiniz yöntemleri gizleyemediğiniz için, tek çözüm ArrayList'i üye değişken olarak kullanmak ve gerçekte istediğiniz tüm yöntemleri iletmektir:

private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }

Dokümantasyonda gördüğünüz tüm yöntemleri gerçekten istiyorsanız, LSP'ye geçebiliriz. LSP bize alt sınıfı ana sınıfın beklendiği yerde kullanmamız gerektiğini söyler. Eğer bir fonksiyon bir ArrayListparametre alırsa ve Nodebunun yerine geçersek, hiçbir şeyin değişmemesi gerekir.

Alt sınıfların uyumluluğu, tür imzaları gibi basit şeylerle başlar. Bir yöntemi geçersiz kıldığınızda, üst sınıfla yasal olan kullanımları dışlayabileceğinden, parametre türlerini daha katı yapamazsınız. Ancak bu, derleyicinin bizim için Java'da kontrol ettiği bir şeydir.

Fakat LSP çok daha derinlere iniyor: Tüm ebeveyn sınıflarının ve arayüzlerin dokümantasyonu ile vaat edilen her şeyle uyumluluğu korumak zorundayız. In verdikleri yanıta Lynn böyle bir durumda bulmuştur List(eğer aracılığıyla miras arayüzü ArrayList) garanti nasıl equals()ve hashCode()yöntemler işe gerekiyor. Çünkü hashCode()tam olarak uygulanması gereken belirli bir algoritma bile veriliyor. Bunu yazdığınızı varsayalım Node:

public class Node extends ArrayList<Node> {
  public final int value;

  public Node(int value, Node... children) {
    this.value = Value;
    for (Node child : children)
      add(child);
  }

  ...

}

Bunun için value, buna katkıda bulunamama hashCode()ve etkilemememiz gerekir equals(). ListOndan devralan tarafından onuruna söz - - arayüz gerektirir new Node(0).equals(new Node(123))gerçek olamayacak kadar.


Sınıflardan miras almak, bir üst sınıfın verdiği bir sözü yanlışlıkla kırmayı çok kolaylaştırdığından ve genellikle amaçladığınızdan daha fazla yöntem ortaya koyduğundan, genellikle kalıtım yerine kompozisyonu tercih etmeniz önerilir . Bir şeyi miras almanız gerekiyorsa, yalnızca arabirimleri miras almanız önerilir. Belirli bir sınıfın davranışını tekrar kullanmak istiyorsanız, onu bir örnek değişkeninde ayrı bir nesne olarak tutabilirsiniz, böylece tüm sözleri ve gereksinimleri size zorlanmaz.

Bazen, doğal dilimiz miras ilişkisi önerir: Bir otomobil bir araçtır. Motosiklet bir araçtır. Sınıfları tanımlamalı mıyım Carve Motorcyclebunu bir Vehiclesınıftan miras almalı mıyım ? Nesneye yönelik tasarım, gerçek dünyayı tam olarak bizim kurallarımızda yansıtmakla ilgili değildir. Gerçek dünyanın zengin taksonomilerini kaynak kodumuzda kolayca kodlayamayız.

Böyle bir örnek, çalışan-patron modelleme problemidir. PersonHer birinin bir adı ve adresi olan çok sayıda s var . Bir Employeebir olup Personve vardır Boss. A Bossda bir Person. Öyleyse ve Persontarafından miras alınan bir sınıf oluşturmalı mıyım? Şimdi bir sorunum var: patron aynı zamanda bir çalışan ve başka bir üstünlüğü var. Öyleyse , uzatılmalı gibi görünüyor . Ama bu bir değil mi? Herhangi bir kalıtım grafiği, bir şekilde burada bozulur.BossEmployeeBossEmployeeCompanyOwnerBossEmployee

OOP, hiyerarşiler, miras ve mevcut sınıfların yeniden kullanımı ile ilgili değildir, davranışların genelleştirilmesiyle ilgilidir . OOP “Bir sürü nesnem var ve onların belirli bir şey yapmalarını istiyorum - ve nasıl umurumda değil” ile ilgili. Arayüzler bunun için. Eğer uygularsanız Iterableşunlara ait arayüz Nodebunu iterable yapmak istiyorum, çünkü o iyi bir sonuçtur. CollectionArabirimi, alt düğümleri eklemek / kaldırmak istediğiniz için uygularsanız, sorun değil. Fakat başka bir sınıftan miras almanızın nedeni, size, yukarıda belirtildiği gibi dikkatli bir düşünce vermediyseniz, ya da en azından olmayan her şeyi verir.


10
Son 4 paragrafınızı basıp OOP ve Miras Hakkında olarak adlandırdım ve onları işte duvara tokatladım. Meslektaşlarımdan bazıları, bildiğiniz gibi, onu nasıl kullandığınızı takdir edeceklerini görmeye ihtiyaç duyuyorlar :) Teşekkürler.
YSC

17

Bir kabın kendiliğinden açılması genellikle kötü uygulama olarak kabul edilir. Sadece bir konteyner yerine bir konteyneri uzatmak için gerçekten çok az sebep var. Bir kapsayıcıyı genişletmek onu sadece çok garip yapıyor.


14

Söylenenlere ek olarak, bu tür yapılardan kaçınmak için Java'ya özgü bir neden var.

equalsListeler yönteminin sözleşmesi bir listenin başka bir nesneye eşit olarak kabul edilmesini gerektirir

ve yalnızca belirtilen nesne aynı zamanda bir liste ise, her iki listenin boyutu aynıdır ve iki listedeki tüm karşılık gelen öğe çiftleri aynıdır .

Kaynak: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)

Spesifik olarak, bu, kendi bir listesi olarak tasarlanan bir sınıfa sahip olmanın eşitlik karşılaştırmalarını pahalılaştırabilir (ve listeler değişebilirse hash hesaplamaları yapabilir) ve sınıfın bazı örnek alanlara sahip olması durumunda, eşitlik karşılaştırmasında dikkate alınmaması gerektiği anlamına gelir. .


1
Bu, kötü uygulamaların haricinde bunu kodumdan kaldırmak için mükemmel bir neden. : P
Addison Crump,

3

Hafıza ile ilgili:

Bunun mükemmeliyetçilik meselesi olduğunu söyleyebilirim. Varsayılan yapıcı ArrayListşöyle görünür:

public ArrayList(int initialCapacity) {
     super();

     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);

     this.elementData = new Object[initialCapacity];
 }

public ArrayList() {
     this(10);
}

Kaynak . Bu yapıcı, Oracle-JDK'da da kullanılır.

Şimdi kodunuzla tek bir bağlantılı Liste oluşturmayı düşünün. Hafıza tüketiminizi 10x faktörü ile (sadece marjinal olarak daha da yüksek olmak üzere) başarıyla şişirdiniz. Ağaçlar için bu, ağacın yapısına ilişkin herhangi bir özel gereksinim olmadan kolayca kötüleşebilir. LinkedListAlternatif olarak bir veya başka bir sınıf kullanmak bu sorunu çözmelidir.

Dürüst olmak gerekirse, çoğu durumda bu kullanılabilir hafıza miktarını görmezden gelsek bile, mükemmeliyetçilikten başka bir şey değildir. A LinkedList, kodu bir alternatif olarak biraz yavaşlatır, bu yüzden performans ile bellek tüketimi arasında bir denge kurulur. Yine de bu konuda benim kişisel görüşüm, bunun kadar kolay bir şekilde atfedilebilecek kadar fazla hafızayı boşa harcamamak olacak.

EDIT: yorumlarla ilgili açıklama (@ kesin olarak amon). Cevabın Bu bölüm yok değil miras konusuyla ilgilenmek. Hafıza kullanımının karşılaştırması tek başına bir Listeye ve en iyi hafıza kullanımına göre yapılır (gerçek uygulamalarda faktör biraz değişebilir, ancak yine de bir miktar boşa harcanan hafızayı toplamak için yeterince büyüktür).

"Kötü uygulama" ile ilgili olarak:

Kesinlikle. Bu basit bir nedenden dolayı bir grafik uygulama standart bir yol değildir: Bir Grafik-Düğüm sahiptir çocuk düğümleri değil, o çocuk düğüm listesi olarak. Kodda ne demek istediğinizi tam olarak ifade etmek büyük bir beceridir. Değişken isimlerinde veya bunun gibi yapıları ifade ederek. Sonraki nokta: arayüzleri minimumda tutma: ArrayListSınıf kullanıcısı için mevcut her yöntemi miras yoluyla kullandınız. Kodun bütün yapısını bozmadan bunu değiştirmenin bir yolu yoktur. Bunun yerine List, iç değişkeni saklayın ve bir adaptör yöntemi aracılığıyla gerekli yöntemleri sağlayın. Bu sayede, her şeyi batırmadan, işlevselliği sınıftan kolayca ekleyebilir ve kaldırabilirsiniz.


1
Neye göre 10 kat daha yüksek ? Bir örnek alan olarak varsayılan olarak oluşturulmuş bir ArrayList'im varsa (bunun yerine devralmak yerine), nesnemde ilave bir işaretçiyi-ArrayList'e kaydetmem gerektiğinden, aslında biraz daha fazla bellek kullanıyorum. Elmaları portakallarla karşılaştırsak ve ArrayList'ten devralmayı herhangi bir ArrayList kullanmamaya benzetsek bile, Java'da her zaman işaretçilerle nesnelere yöneldiğimizi, hiçbir zaman C ++ değerine sahip olmayan nesneleri kullandığımızı unutmayın. new Object[0]Toplamda 4 kelime kullandığı varsayıldığında , new Object[10]sadece 14 kelime bellek kullanılır.
amon

@amon Açıkça söylemedim, bunun için ağla. Bağlam, LinkedList ile karşılaştırdığımı görmek için yeterli olsa da. Ve buna referans denir, pointer btw değil. Ve hafızalı olan nokta, sınıfın miras yoluyla kullanılıp kullanılmadığıyla ilgili değildi, ama (neredeyse) kullanılmayanların ArrayListoldukça fazla hafızadan mahrum kalması gerçeği ile ilgiliydi.
Paul

-2

Bu, kompozit desenin semantiği gibi görünüyor . Özyinelemeli veri yapılarını modellemek için kullanılır.


13
Bunu küçümsemem ama bu yüzden GoF kitabından gerçekten nefret ediyorum. Kanamış bir ağaç! Bir ağacı tanımlamak için neden "karma desen" terimini icat etmemiz gerekti?
David Arno

3
@DavidArno Ben onu "bileşik desen" olarak tanımlayan bütün polimorfik düğüm yönü olduğuna inanıyorum, bir ağaç veri yapısının kullanımı bunun sadece bir parçası.
JAB

2
@RobertHarvey, katılmıyorum (cevabınız için hem burada hem de yorumlarınızla birlikte). public class Node extends ArrayList<Node> { public string Item; }Cevabınızın miras versiyonu. Sizinki sebeplerden daha basittir, ancak her ikisi de bir şeyi başarır: ikili olmayan ağaçları tanımlar.
David Arno

2
@DavidArno: Asla işe yaramayacağını söylemedim, sadece muhtemelen ideal olmadığını söyledim.
Robert Harvey,

1
@DavidArno burada duygular ve tat ile ilgili değildir, ancak gerçeklerle ilgilidir. Bir ağaç düğümlerden oluşur ve her düğümün potansiyel çocukları vardır. Belirli bir düğüm yalnızca belirli bir anda bir yaprak düğümdür. Bir kompozitte, bir yaprak düğümü yapı olarak yapraktır ve doğası değişmez.
Christophe
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.