Belirli durumlar için alt sınıflar oluşturmak kötü bir uygulama mıdır?


37

Aşağıdaki tasarımı düşünün

public class Person
{
    public virtual string Name { get; }

    public Person (string name)
    {
        this.Name = name;
    }
}

public class Karl : Person
{
    public override string Name
    {
        get
        {
            return "Karl";
        }
    }
}

public class John : Person
{
    public override string Name
    {
        get
        {
            return "John";
        }
    }
}

Burada bir sorun olduğunu mu düşünüyorsun? Bana göre Karl ve John sınıfları sınıflar yerine sadece örnek olmalıdırlar, çünkü bunlar tamamen aynıdır:

Person karl = new Person("Karl");
Person john = new Person("John");

Örnekler yeterli olduğunda neden yeni sınıflar oluşturayım? Sınıflar, örneğe hiçbir şey eklemiyor.


17
Ne yazık ki, bunu birçok üretim kodunda bulacaksınız. Yapabildiğin zaman temizle.
Adam Zuckerman,

14
Bu harika bir tasarım - bir geliştirici iş verilerindeki her değişiklik için vazgeçilmez olmak istiyorsa ve 7/24 hizmetinizde olması sorun yok ;-)
Doc Brown

1
İşte OP'nin geleneksel bilgeliğin zıddı olduğuna inandığı bir tür ilgili soru. programmers.stackexchange.com/questions/253612/…
Dunk

11
Verilerinize göre davranışa bağlı olarak sınıflarınızı tasarlayın. Bana göre alt sınıflar hiyerarşiye yeni bir davranış eklemiyorlar.
Songo

2
Bu, Java'larda Java 8'e gelmeden (farklı sözdizimi ile aynı olan) Java'da anonim alt sınıflarda (olay işleyicileri gibi) yaygın olarak kullanılır.
marczellm

Yanıtlar:


77

Her insan için özel bir alt sınıfa ihtiyaç yoktur.

Haklısın, bunun yerine örnekler olmalı.

Alt sınıfların amacı: ebeveyn sınıflarını genişletmek

Alt sınıflar , ana sınıf tarafından sağlanan işlevselliği artırmak için kullanılır . Örneğin, şunlara sahip olabilirsiniz:

  • Bir üst sınıf Batterycan Power()bir var bir şey ve Voltagemülk,

  • Ve RechargeableBatterymiras alabilen Power()ve Voltageaynı zamanda Recharge()d olabilen bir alt sınıf .

Bir argüman olarak RechargeableBatterykabul eden herhangi bir yönteme parametre olarak bir sınıf örneğini iletebileceğinizi unutmayın Battery. Buna beş SOLID ilkesinden biri olan Liskov ikame prensibi denir . Benzer şekilde, gerçek hayatta, MP3 çalarım iki adet AA pili kabul ederse, bunları iki adet şarj edilebilir AA pil ile değiştirebilirim.

Bir şey arasındaki farkı temsil etmek için bir alan mı, yoksa alt sınıf mı kullanmanız gerektiğini belirlemek bazen zor olabilir. Örneğin, AA, AAA ve 9 Voltluk pilleri kullanmanız gerekiyorsa, üç alt sınıf oluşturur veya enum kullanır mısınız? Sayfa 232'deki Martin Fowler tarafından Yeniden canlandırmayla ilgili “Alt Sınıfı Alanlarla Değiştirme”, size bazı fikirler verebilir ve birinden diğerine nasıl geçileceği konusunda fikir verebilir.

Örneğinizde Karlve Johnhiçbir şeyi genişletmeyin ya da herhangi bir ek değer sağlamaz: PersonSınıfı doğrudan kullanarak tam olarak aynı işlevselliğe sahip olabilirsiniz . Ek değer içermeyen daha fazla kod satırına sahip olmak hiçbir zaman iyi değildir.

Bir iş vakasına bir örnek

Belirli bir kişi için bir alt sınıf oluşturmanın gerçekten anlamlı olacağı bir iş durumu ne olabilir?

Diyelim ki bir şirkette çalışan kişileri yöneten bir uygulama oluşturduk. Uygulama, izinleri de yönetiyor, bu nedenle muhasebeci Helen, SVN deposuna erişemiyor, ancak iki programcı olan Thomas ve Mary muhasebe ile ilgili belgelere erişemiyor.

Jimmy, büyük patron (şirketin kurucusu ve CEO'su) kimsenin sahip olmadığı çok özel ayrıcalıklara sahip. Örneğin, tüm sistemi kapatabilir veya bir kişiyi işten çıkarabilir. Kaptın bu işi.

Bu tür bir uygulama için en zayıf model aşağıdaki gibi sınıflara sahip olmaktır:

          Sınıf şeması, Helen, Thomas, Mary ve Jimmy sınıflarını ve ortak ebeveynlerini gösterir: soyut Kişi sınıfı.

çünkü kod çoğaltma çok hızlı bir şekilde ortaya çıkacaktır. Dört çalışanın en temel örneğinde bile, Thomas ve Mary sınıfları arasındaki kodu kopyalayacaksınız. Bu sizi ortak bir ebeveyn sınıfı oluşturmaya zorlar Programmer. Sizde birden fazla muhasebeci olabileceğinden, muhtemelen Accountantsınıf da oluşturacaksınız .

          Sınıf şemasında üç çocuklu soyut bir Kişi gösterilmektedir: soyut Muhasebeci, soyut bir Programcı ve Jimmy.  Muhasebecinin bir alt sınıfı Helen vardır ve Programcı'nın iki alt sınıfı vardır: Thomas ve Mary.

Şimdi, sınıfı sahip olduğunu fark Helençok faydalı, hem de tutma değildir Thomasve Mary: kodunuzu çalışmaların en üst düzeyde zaten-de muhasebeciler, programcılar ve Jimmy düzeyinde. SVN sunucusu, kayıt defterine erişmesi gereken Thomas mı yoksa Mary mi olduğunu umursamıyor; yalnızca bir programcı mı yoksa muhasebeci mi olduğunu bilmek gerekiyor.

if (person is Programmer)
{
    this.AccessGranted = true;
}

Böylece, kullanmadığınız sınıfları kaldırırsınız:

          Sınıf şemasında Muhasebeci, Programcı ve Jimmy alt sınıfları ile birlikte bulunur: abstract Person.

“Ama Jimmy'i olduğu gibi tutabilirim, çünkü her zaman sadece bir CEO, bir büyük patron — Jimmy” olacağını düşünüyorum. Dahası, Jimmy, bir önceki örnekte olduğu gibi, kodunuzda çokça kullanılıyor;

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

Bu yaklaşımın sorunu Jimmy'nin hala bir otobüs tarafından vurulabilmesi ve yeni bir CEO olması. Veya yönetim kurulu, Mary'nin yeni bir CEO olması gerektiği için çok büyük olduğuna karar verebilir ve Jimmy bir satış görevlisinin konumuna düşecek, bu yüzden şimdi tüm kodunuzu gözden geçirmeniz ve her şeyi değiştirmeniz gerekir.


6
@DocBrown: Genellikle yanlış olmayan ancak yeterince derin olmayan kısa cevapların konuyu derinlemesine açıklayan cevaplardan çok daha yüksek bir puan aldığına şaşırdım. Muhtemelen çok fazla insan çok okumak istemez, bu yüzden çok kısa cevaplar onlar için daha çekici görünür. Yine de cevabımı en azından bir açıklama yapması için düzenlemiştim.
Arseni Mourzenko

3
Kısa cevaplar ilk önce yayınlanma eğilimindedir. Önceki gönderiler daha fazla oy alır.
Tim B

1
@TimB: Genellikle orta ölçekli cevapları ile başlamak ve daha sonra düzenlemek onları (burada çok tam olduğu bir örnek sadece dört gösterilmeden, muhtemelen yaklaşık on beş revizyonlar olduğunu göz önüne alındığında,). Zaman içinde cevap ne kadar uzun olursa, o kadar az puan alır; Kısa kalan benzer bir soru daha fazla çekim çekiyor.
Arseni Mourzenko

@DocBrown ile aynı fikirde. Örneğin, iyi bir cevap " içerik için değil, davranış için alt sınıf" gibi bir şey söyler ve bu yorumda yapmayacağım bir referansa atıfta bulunur.
user949300,

2
Son paragraftan anlaşılmadığı takdirde: Kısıtlı erişime sahip sadece 1 kişiniz olsa bile, o kişiyi sınırsız erişim sağlayan ayrı bir sınıfın örneği yapmalısınız. Kişinin kendisini kontrol etmek kod-veri etkileşimi ile sonuçlanır ve bir sürü solucan açabilir. Örneğin, Yönetim Kurulu, CEO olarak bir zafer takvimi almaya karar verirse? "Sınırsız" sınıfınız yoksa, mevcut CEO’yu güncellemeniz gerekmez, aynı zamanda diğer üyeler için 2 kontrol daha ekleyin. Eğer sahipseniz, onları sadece "Sınırsız" olarak ayarladınız.
Nzall

15

Bu tür bir sınıf yapısını sadece bir örnek için açıkça ayarlanabilecek alanların ayrıntılarını değiştirmek için kullanmak aptalca. Ancak bu, örneğinize özgüdür.

Farklı yapma Personın farklı sınıfları neredeyse kesin kötü bir fikirdir - Yeni Kişi alan adınızı giren her defasında programı değiştirmek ve yeniden derlemek zorunda kalacak. Ancak, bu, mevcut bir sınıftan miras almanın farklı bir dizgenin bir yere döndürülmesi anlamına gelmediği anlamına gelmez.

Aradaki fark, o sınıfın temsil ettiği varlıkların nasıl değişeceğini beklemenizdir. İnsanların neredeyse her zaman gelip gideceği varsayılır ve kullanıcılarla ilgili hemen hemen her ciddi uygulamanın çalışma anında kullanıcı ekleyebilmesi ve kaldırabilmesi beklenir. Ancak, farklı şifreleme algoritmaları gibi şeyleri modelliyorsanız, yenisini desteklemek muhtemelen büyük bir değişikliktir ve myName()yöntemi farklı bir dize döndüren (ve perform()muhtemelen yöntemi farklı bir şey yapan) yeni bir sınıf icat etmek mantıklıdır .


12

Örnekler yeterli olduğunda neden yeni sınıflar oluşturayım?

Çoğu durumda, yapmazsın. Örneğiniz gerçekten bu davranışın gerçek değer katmadığı iyi bir durumdur.

Ayrıca , Alt Sınıflar temelde bir uzatma olmadığından, ancak ebeveynin iç çalışmalarını değiştirdiğinden , Açık Kapalı ilkesini ihlal eder . Dahası, ebeveynin kamu kurucusu şimdi alt sınıflarda rahatsız edicidir ve API bu nedenle daha az anlaşılabilir hale geldi.

Bununla birlikte, somtimes, kod boyunca sıklıkla kullanılan yalnızca bir veya iki özel konfigürasyona sahip olacaksanız, karmaşık bir kurucuya sahip olan bir ebeveyni alt sınıflamak bazen daha kolay ve daha az zaman alır. Böyle özel durumlarda, böyle bir yaklaşımla yanlış bir şey göremiyorum. Diyelim ki kurucu kıvrımlı j / k


OCP paragrafına katıldığımdan emin değilim. Bir sınıf iyi tasarım takip ediyor onu varsayarak sonra- onun soyundan bir özellik ayarlayıcı tutulmuşsa mülk gerektiğini İlkeleri- değil kendi iç işleyişini bir parçası olmak
Ben Aaronson

@ BenAaronson Mülkiyetin davranışı değişmediği sürece, OCP'yi ihlal etmiyorsunuz, ama işte buradalar. Tabii ki, bu Mülkü içsel çalışmalarında kullanabilirsiniz. Ancak mevcut davranışı değiştirmemelisiniz! Davranışı genişletmelisiniz, kalıtımsallık ile değiştirmeyin. Tabii ki her kuralın istisnaları var.
Şahin

1
Yani sanal üyelerin herhangi bir kullanımı bir OCP ihlalidir?
Ben Aaronson

3
@Falcon Sadece tekrarlayan karmaşık yapıcı çağrıları önlemek için yeni bir sınıf türetmek gerekmez. Sadece ortak durumlar için fabrikalar yaratın ve küçük varyasyonları parametreleyin.
Keen

@ BenAaronson Gerçek bir uygulaması olan sanal üyelere sahip olmanın böyle bir ihlal olduğunu söyleyebilirim. Soyut üyeler, ya da hiçbir şey yapmayan yöntemler söyleyerek geçerli uzatma noktaları olurlar. Temel olarak, bir temel sınıfın koduna baktığınızda, nihai olmayan her yöntemin tamamen farklı bir şeyle değiştirilmeye aday olması yerine, olduğu gibi çalışacağını biliyorsunuz. Ya da farklı ifade etmek gerekirse, miras kalan bir sınıfın temel sınıfın davranışını etkileyebilme yolları açık olacak ve “katkı maddesi” olmakla sınırlandırılacaktır.
millimoose

8

Uygulamanın kapsamı buysa, bunun kötü bir uygulama olduğuna katılıyorum.

John ve Karl'ın farklı davranışları varsa, işler biraz değişiyor. John'un çok iyi bir oda arkadaşı olduğu ve çok etkili bir şekilde temizlediği, ancak Karl'ın odayı çok fazla temizleyemediği ve temizleyemediği personbir yöntem olabilir cleanRoom(Room room).

Bu durumda, tanımlanmış davranışlarla kendi alt sınıfları olmalarının bir anlamı olacaktır. Aynı şeyi başarmanın daha iyi yolları var (örneğin, bir CleaningBehaviorsınıf), ama en azından bu şekilde OO ilkelerinin korkunç bir ihlali yoktur.


Farklı davranış yok, sadece farklı veriler var. Teşekkürler.
Ignacio Soler Garcia

6

Bazı durumlarda, yalnızca belirli sayıda örnek olacağını biliyorsunuz ve daha sonra (benim görüşüme göre çirkin olsa da) bu yaklaşımı kullanmak biraz iyi olacak. Dil izin veriyorsa, enum kullanmak daha iyidir.

Java örneği:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}

6

Çok sayıda insan çoktan cevapladı. Kendi kişisel bakış açımı verebileceğimi düşündüm.


Bir zamanlar müzik yapan bir uygulama üzerinde çalıştım.

Uygulamanın soyut vardı Scale: Birkaç alt sınıf ile sınıf CMajor, DMinorvb Scaleyüzden böyle bir şey görünüyordu:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

Müzik jeneratörleri Scale, müzik üretmek için belirli bir örnekle çalıştı . Kullanıcı, müzik yapmak için listeden bir ölçek seçer.

Bir gün havalı bir fikir aklıma geldi: neden kullanıcının kendi ölçeklerini yaratmasına izin vermiyor? Kullanıcı listeden notları seçer, bir düğmeye basar ve listedeki mevcut ölçeklere yeni bir ölçek eklenir.

Ama bunu yapamadım. Bunun nedeni, tüm ölçeklerin zaten derleme zamanında ayarlanmış olmasıdır - çünkü sınıf olarak ifade edilirler. Sonra bana vurdu:

“Süper sınıflar ve alt sınıflar” anlamında düşünmek çoğu zaman sezgiseldir. Hemen hemen her şey bu sistem aracılığıyla ifade edilebilir: üst sınıf Personve alt sınıflar Johnve Mary; üst sınıf Carve alt sınıflar Volvove Mazda; SuperClass Missileve alt sınıfları SpeedRocked, LandMineve TrippleExplodingThingy.

Bu şekilde düşünmek çok doğal, özellikle de OO için nispeten yeni olan biri için.

Ancak sınıfların şablon olduğunu ve nesnelerin bu şablonlara içerik döktüğünü daima hatırlamalıyız . Sayısız olasılık oluşturarak, şablona istediğiniz içeriği dökün.

Şablonu doldurmak alt sınıfın işi değildir. Bu nesnenin işi. Alt sınıfın işi, gerçek işlevsellik eklemek veya şablonu genişletmektir .

İşte bu yüzden Scale, bir Note[]alan içeren somut bir sınıf yaratmalı ve nesnelerin bu şablonu doldurmasına izin vermeliydim ; muhtemelen yapıcı veya başka bir şey aracılığıyla. Ve nihayet yaptım.

Bir sınıfta bir şablon tasarladığınızda (örneğin Note[]doldurulması gereken boş bir üye veya String namedeğer atanması gereken bir alan), şablonu doldurmak için bu sınıftaki nesnelerin işi olduğunu unutmayın ( veya muhtemelen bu nesneleri yaratanlar). Alt sınıflar, şablonları doldurmamak için işlevsellik eklemek içindir.


"Süper sınıf Person, alt sınıflar Johnve Mary" gibi bir sistem yaratmaya istekli olabilirsiniz , çünkü böyle bir biçim alırsınız.

Bu şekilde, sadece söyleyebiliriz Person p = new Mary()yerine, Person p = new Person("Mary", 57, Sex.FEMALE). İşleri daha organize ve daha yapılandırılmış hale getirir. Ancak dediğimiz gibi, her veri kombinasyonu için yeni bir sınıf oluşturmak iyi bir yaklaşım değildir, çünkü hiçbir şeyin kodunu engeller ve çalışma zamanı yetenekleri açısından sizi sınırlar.

Öyleyse işte çözüm: temel bir fabrika kullanın, hatta statik bir fabrika bile olabilir. Bunun gibi:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

Bu şekilde, 'önceden ayarlanmış' olan 'programla birlikte gelen' kolayca kullanabilirsiniz: Person mary = PersonFactory.createMary()ancak örneğin, kullanıcının yapmasına izin vermek istemeniz durumunda dinamik olarak yeni kişiler tasarlama hakkını da saklı tutarsınız. . Örneğin:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

Ya da daha iyisi: Böyle bir şey yapın:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

Kendini kaptırdım. Sanırım fikri anladın.


Alt sınıflar, üst sınıfları tarafından belirlenen şablonları doldurmaya yönelik değildir. Alt sınıflar işlevsellik eklemek içindir . Nesne , şablonları doldurmak içindir, onlar bunun içindir.

Her olası veri kombinasyonu için yeni bir sınıf oluşturmamalısınız. (Tıpkı Scaleolası her Notes birleşimi için yeni bir alt sınıf oluşturmamam gerektiği gibi ).

Her bir kılavuz: Ne zaman yeni bir alt sınıf oluşturursanız, üst sınıfta olmayan yeni bir işlevsellik eklediğini düşünün. Bu sorunun cevabı "hayır" ise, üst sınıfın 'şablonunu doldurmaya' çalışıyorsanız, bu durumda sadece bir nesne oluşturun. (Ve muhtemelen hayatı daha kolay hale getirmek için 'ön ayarlı' bir Fabrika).

Umarım yardımcı olur.


Bu mükemmel bir örnek, çok teşekkür ederim.
Ignacio Soler Garcia,

@SoMoS Yardım edebildiğime sevindim.
Aviv Cohn

2

Bu, burada 'örnek olmalı' - bunun biraz aşırı olmasına rağmen bir istisnadır.

Eğer isteseydi derleyici (bu örnekte) Karl veya John olmasına bağlı olarak erişim yöntemleri izinleri uygulamak yerine hangi örneği o zaman farklı sınıfları kullanabilir geçti kontrol etmek yöntem uygulanmasını istemeye. Örneklemeden ziyade farklı sınıfları zorladığınızda, çalışma zamanı yerine derleme zamanında 'Karl' ve 'John' arasındaki farklılaşma kontrollerini yaparsınız ve bu yararlı olabilir - özellikle de derleyicinin çalışma zamanından daha güvenilir olabileceği bir güvenlik bağlamında (örneğin) harici kütüphanelerin kod kontrollerini yapar.


2

Çoğaltılmış kodun olması kötü bir uygulama olarak kabul edilir .

Bu, en azından kısmen çünkü kod satırları ve dolayısıyla kod tabanının boyutu bakım maliyetleri ve kusurları ile ilişkilidir .

İşte bu belirli konuda bana ilgili sağduyu mantığı:

1) Bunu değiştirmem gerekirse, kaç yeri ziyaret etmem gerekir? Burada daha az iyidir.

2) Bu şekilde yapılmasının bir nedeni var mı? Örneğinizde - olamaz - ancak bazı örneklerde bunu yapmanın bir nedeni olabilir. Kafamın üstünden düşünebildiğim bir şey, kalıtımın GORM için bazı eklentilerle iyi bir şekilde oynamadığı erken Grails gibi bazı çerçevelerde. Birkaç ve uzak arasında.

3) Bu yolu anlamak daha temiz ve daha kolay mı? Yine, bu sizin örneğinizde değil - ancak ayrılmış sınıfların korunmasının gerçekten daha kolay olduğu bir gerginlik olabilecek bazı kalıtım kalıpları var (komut veya strateji kalıplarının belirli kullanımları akla geliyor).


1
Kötü olup olmaması taşa ayarlanmamış. Örneğin, nesne oluşturma ve yöntem çağrılarının ek yükünün, uygulamanın artık şartnamelere uymadığı performans için o kadar zararlı olabileceği senaryolar vardır.
07

2. maddeye bakınız. Tabii ki ara sıra nedenler var. Bu yüzden birinin kodunu çözmeden önce sormanız gerekiyor.
dcgregorya

0

Bir kullanım durumu vardır: normal bir nesne olarak bir fonksiyon veya yönteme referans oluşturma.

Bunu Java GUI kodunda çok görebilirsiniz:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

Derleyici burada yeni bir anonim alt sınıf oluşturarak size yardımcı olur.

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.