Apakah membuat subclass untuk instance tertentu merupakan praktik yang buruk?

37

Pertimbangkan desain berikut

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

Apakah Anda pikir ada sesuatu yang salah di sini? Bagi saya kelas Karl dan John seharusnya hanya instance daripada kelas karena mereka persis sama dengan:

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

Mengapa saya membuat kelas baru ketika instance sudah cukup? Kelas-kelas tidak menambahkan apa pun ke instance.

Ignacio Soler Garcia
sumber
17
Sayangnya, Anda akan menemukan ini dalam banyak kode produksi. Bersihkan saat Anda bisa.
Adam Zuckerman
14
Ini adalah desain yang hebat - jika seorang pengembang ingin menjadikan dirinya sangat diperlukan untuk setiap perubahan dalam data bisnis, dan tidak memiliki masalah untuk tersedia 24/7 ;-)
Doc Brown
1
Berikut adalah semacam pertanyaan terkait di mana OP tampaknya percaya kebalikan dari kebijaksanaan konvensional. programmers.stackexchange.com/questions/253612/…
Dunk
11
Rancang kelas Anda tergantung pada perilaku bukan data. Bagi saya, sub-kelas tidak menambahkan perilaku baru ke hierarki.
Songo
2
Ini banyak digunakan dengan subclass anonim (seperti event handler) di Jawa sebelum lambdas datang ke Java 8 (yang merupakan hal yang sama dengan sintaks yang berbeda).
marczellm

Jawaban:

77

Tidak perlu memiliki subclass khusus untuk setiap orang.

Anda benar, itu harus menjadi contoh.

Sasaran subclass: untuk memperluas kelas induk

Subkelas digunakan untuk memperluas fungsionalitas yang disediakan oleh kelas induk. Misalnya, Anda mungkin memiliki:

  • Kelas induk Batteryyang dapat Power()sesuatu dan dapat memiliki Voltageproperti,

  • Dan subclass RechargeableBattery, yang bisa mewarisi Power()dan Voltage, tetapi juga bisa Recharge()d.

Perhatikan bahwa Anda bisa melewatkan instance RechargeableBatterykelas sebagai parameter ke metode apa pun yang menerima Batteryargumen. Ini disebut prinsip substitusi Liskov , salah satu dari lima prinsip SOLID. Demikian pula, dalam kehidupan nyata, jika pemutar MP3 saya menerima dua baterai AA, saya dapat menggantinya dengan dua baterai AA yang dapat diisi ulang.

Perhatikan bahwa kadang-kadang sulit untuk menentukan apakah Anda perlu menggunakan bidang atau subkelas untuk mewakili perbedaan antara sesuatu. Misalnya, jika Anda harus menangani baterai AA, AAA dan 9-Volt, apakah Anda akan membuat tiga subclass atau menggunakan enum? “Ganti Subclass dengan Fields” di Refactoring oleh Martin Fowler, halaman 232, dapat memberi Anda beberapa ide dan cara berpindah dari satu ke yang lain.

Dalam contoh Anda, Karldan Johnjangan memperluas apa pun, mereka juga tidak memberikan nilai tambahan: Anda dapat memiliki fungsionalitas yang persis sama menggunakan Personkelas secara langsung. Memiliki lebih banyak baris kode tanpa nilai tambahan tidak pernah baik.

Contoh kasus bisnis

Apa yang mungkin menjadi kasus bisnis di mana sebenarnya masuk akal untuk membuat subkelas untuk orang tertentu?

Katakanlah kita membangun aplikasi yang mengelola orang yang bekerja di perusahaan. Aplikasi ini mengelola izin juga, sehingga Helen, akuntan, tidak dapat mengakses repositori SVN, tetapi Thomas dan Mary, dua programmer, tidak dapat mengakses dokumen terkait akuntansi.

Jimmy, bos besar (pendiri dan CEO perusahaan) memiliki hak istimewa yang sangat spesifik yang tidak dimiliki siapa pun. Dia dapat, misalnya, mematikan seluruh sistem, atau memecat seseorang. Anda mendapatkan idenya.

Model termiskin untuk aplikasi tersebut adalah memiliki kelas seperti:

          Diagram kelas menunjukkan kelas Helen, Thomas, Mary dan Jimmy dan orang tua mereka yang sama: kelas Person abstrak.

karena duplikasi kode akan muncul dengan sangat cepat. Bahkan dalam contoh dasar empat karyawan, Anda akan menduplikasi kode antara kelas Thomas dan Mary. Ini akan mendorong Anda untuk membuat kelas induk biasa Programmer. Karena Anda mungkin memiliki beberapa akuntan juga, Anda mungkin akan membuat Accountantkelas juga.

          Diagram kelas menunjukkan Orang abstrak dengan tiga anak: Akuntan abstrak, Programmer abstrak, dan Jimmy.  Akuntan memiliki subclass Helen, dan Programmer memiliki dua subclass: Thomas dan Mary.

Sekarang, Anda perhatikan bahwa memiliki kelas Helentidak terlalu berguna, juga menjaga Thomasdan Mary: sebagian besar kode Anda bekerja di tingkat atas — di tingkat akuntan, programmer, dan Jimmy. Server SVN tidak peduli apakah Thomas atau Mary yang perlu mengakses log — ia hanya perlu tahu apakah itu programmer atau akuntan.

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

Jadi Anda akhirnya menghapus kelas yang tidak Anda gunakan:

          Diagram kelas berisi subkelas Akuntan, Programmer, dan Jimmy dengan kelas induk umumnya: Orang abstrak.

"Tapi aku bisa menjaga Jimmy apa adanya, karena selalu ada hanya satu CEO, satu bos besar — ​​Jimmy", begitu. Selain itu, Jimmy banyak digunakan dalam kode Anda, yang sebenarnya lebih mirip ini, dan tidak seperti pada contoh sebelumnya:

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

Masalah dengan pendekatan itu adalah bahwa Jimmy masih bisa ditabrak bus, dan akan ada CEO baru. Atau dewan direksi dapat memutuskan bahwa Mary sangat hebat sehingga ia harus menjadi CEO baru, dan Jimmy akan diturunkan jabatannya menjadi tenaga penjual, jadi sekarang, Anda perlu memeriksa semua kode Anda dan mengubah segalanya.

Arseni Mourzenko
sumber
6
@DocBrown: Saya sering terkejut melihat bahwa jawaban singkat yang tidak salah tetapi tidak cukup dalam memiliki skor yang jauh lebih tinggi daripada jawaban yang menjelaskan secara mendalam subjek. Mungkin terlalu banyak orang yang tidak suka membaca, jadi jawaban yang sangat singkat terlihat lebih menarik bagi mereka. Namun, saya mengedit jawaban saya untuk memberikan setidaknya beberapa penjelasan.
Arseni Mourzenko
3
Jawaban singkat cenderung diposting terlebih dahulu. Posting sebelumnya mendapatkan lebih banyak suara.
Tim B
1
@ TimB: Saya biasanya mulai dengan jawaban berukuran sedang, dan kemudian mengeditnya menjadi sangat lengkap (inilah contohnya , mengingat mungkin ada sekitar lima belas revisi, hanya empat yang ditampilkan). Saya perhatikan bahwa semakin lama menjadi jawaban dari waktu ke waktu, semakin sedikit hasil yang diterima; pertanyaan serupa yang tetap pendek menarik lebih banyak upvotes.
Arseni Mourzenko
Setuju dengan @DocBrown. Sebagai contoh, jawaban yang baik akan mengatakan sesuatu seperti "subkelas untuk perilaku , bukan untuk konten", dan merujuk ke beberapa referensi yang tidak akan saya lakukan dalam komentar ini.
user949300
2
Dalam hal itu tidak jelas dari paragraf terakhir: Bahkan jika Anda hanya memiliki 1 orang dengan akses tidak terbatas, Anda masih harus membuat orang itu menjadi turunan dari kelas terpisah yang mengimplementasikan akses tidak terbatas. Memeriksa orang itu sendiri menghasilkan interaksi kode-data dan dapat membuka seluruh kaleng cacing. Misalnya, bagaimana jika Dewan Direksi memutuskan untuk menunjuk tiga serangkai sebagai CEO? Jika Anda tidak memiliki kelas "Tidak Terbatas", Anda tidak hanya harus memperbarui CEO yang ada, tetapi juga menambahkan 2 cek lagi untuk anggota lainnya. Jika Anda memilikinya, Anda hanya mengaturnya sebagai "Tidak Terbatas".
Nzall
15

Adalah konyol menggunakan struktur kelas semacam ini hanya untuk memvariasikan detail yang jelas-jelas merupakan bidang yang dapat diselesaikan pada instance. Tapi itu khusus untuk contoh Anda.

Membuat berbagai Personkelas yang berbeda hampir pasti merupakan ide yang buruk - Anda harus mengubah program Anda dan mengkompilasi ulang setiap kali Orang baru memasuki domain Anda. Namun, itu tidak berarti mewarisi dari kelas yang sudah ada sehingga string yang berbeda dikembalikan ke suatu tempat terkadang tidak berguna.

Perbedaannya adalah bagaimana Anda mengharapkan entitas yang diwakili oleh kelas itu bervariasi. Orang-orang hampir selalu dianggap datang dan pergi, dan hampir setiap aplikasi serius yang berurusan dengan pengguna diharapkan dapat menambah dan menghapus pengguna pada saat run-time. Tetapi jika Anda memodelkan hal-hal seperti algoritma enkripsi yang berbeda, mendukung yang baru mungkin merupakan perubahan besar, dan masuk akal untuk menciptakan kelas baru yang myName()metodenya mengembalikan string yang berbeda (dan yang perform()metodenya mungkin melakukan sesuatu yang berbeda).

Kilian Foth
sumber
12

Mengapa saya membuat kelas baru ketika instance sudah cukup?

Dalam kebanyakan kasus, Anda tidak akan melakukannya. Contoh Anda benar-benar kasus bagus di mana perilaku ini tidak menambah nilai nyata.

Ini juga melanggar prinsip Terbuka Tertutup , karena subclass pada dasarnya bukan ekstensi tetapi memodifikasi cara kerja orang tua. Selain itu, konstruktor publik dari induk sekarang menjengkelkan dalam subkelas dan dengan demikian API menjadi kurang dipahami.

Namun, kadang-kadang, jika Anda hanya akan memiliki satu atau dua konfigurasi khusus yang sering digunakan di seluruh kode, kadang-kadang lebih nyaman dan memakan waktu lebih sedikit untuk hanya subkelas induk yang memiliki konstruktor kompleks. Dalam kasus khusus seperti itu, saya tidak dapat melihat ada yang salah dengan pendekatan seperti itu. Sebut saja konstruktor currying j / k

Elang
sumber
Saya tidak yakin saya setuju dengan paragraf OCP. Jika sebuah kelas mengekspos setter properti kepada keturunannya - dengan asumsi mengikuti prinsip desain yang baik - properti tidak boleh menjadi bagian dari pekerjaan dalam dirinya
Ben Aaronson
@ BenAaronson Selama perilaku properti itu sendiri tidak diubah, Anda tidak melanggar OCP, tetapi di sini mereka lakukan. Tentu saja Anda dapat menggunakan Properti itu dalam pekerjaan dalamnya. Tetapi Anda tidak harus mengubah perilaku yang ada! Anda seharusnya hanya memperluas perilaku, bukan mengubahnya dengan warisan. Tentu saja, ada pengecualian untuk setiap aturan.
Falcon
1
Jadi penggunaan anggota virtual adalah pelanggaran OCP?
Ben Aaronson
3
@ Falcon Tidak perlu menurunkan kelas baru hanya untuk menghindari panggilan konstruktor kompleks yang berulang. Cukup buat pabrik untuk kasus-kasus umum dan parameterkan variasi kecilnya.
Tertarik
@ BenAaronson Saya akan mengatakan memiliki anggota virtual yang memiliki implementasi aktual merupakan pelanggaran. Anggota abstrak, atau katakan metode yang tidak melakukan apa-apa, akan menjadi titik ekstensi yang valid. Pada dasarnya, ketika Anda melihat kode kelas dasar, Anda tahu bahwa itu akan berjalan apa adanya, alih-alih setiap metode non-final menjadi kandidat untuk diganti dengan sesuatu yang sama sekali berbeda. Atau dengan kata lain: cara-cara di mana kelas warisan dapat mempengaruhi perilaku kelas dasar akan eksplisit, dan dibatasi menjadi "aditif".
milimoose
8

Jika ini sejauh praktik, maka saya setuju ini praktik buruk.

Jika John dan Karl memiliki perilaku yang berbeda, maka segalanya berubah sedikit. Bisa jadi itu personmemiliki metode untuk di cleanRoom(Room room)mana ternyata John adalah teman sekamar yang hebat dan membersihkan sangat efektif, tetapi Karl tidak dan tidak terlalu banyak membersihkan kamar.

Dalam hal ini, masuk akal untuk menjadikan mereka subkelas mereka sendiri dengan perilaku yang ditentukan. Ada cara yang lebih baik untuk mencapai hal yang sama (misalnya, CleaningBehaviorkelas), tetapi setidaknya cara ini bukanlah pelanggaran prinsip OO yang mengerikan.

corsiKa
sumber
Tidak ada perilaku yang berbeda, hanya data yang berbeda. Terima kasih.
Ignacio Soler Garcia
6

Dalam beberapa kasus Anda tahu hanya akan ada sejumlah contoh dan kemudian akan baik-baik saja (meskipun menurut saya jelek) untuk menggunakan pendekatan ini. Lebih baik menggunakan enum jika bahasa memungkinkan.

Contoh jawa:

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

    private final String name;

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

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

Banyak orang sudah menjawab. Kupikir aku akan memberikan perspektif pribadiku sendiri.


Sekali waktu saya mengerjakan sebuah aplikasi (dan masih melakukannya) yang menciptakan musik.

Aplikasi ini memiliki abstrak Scalekelas dengan beberapa subclass: CMajor, DMinor, dll Scaletampak sesuatu seperti:

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

Generator musik bekerja dengan Scaleinstance spesifik untuk menghasilkan musik. Pengguna akan memilih skala dari daftar, untuk menghasilkan musik.

Suatu hari, sebuah ide keren muncul di benak saya: mengapa tidak mengizinkan pengguna untuk membuat skala sendiri? Pengguna akan memilih catatan dari daftar, tekan tombol, dan skala baru akan ditambahkan ke daftar skala yang tersedia.

Tetapi saya tidak dapat melakukan ini. Itu karena semua skala sudah ditetapkan pada waktu kompilasi - karena mereka dinyatakan sebagai kelas. Lalu aku tersadar:

Seringkali intuitif untuk berpikir dalam istilah 'superclasses and subclasses'. Hampir semuanya dapat diekspresikan melalui sistem ini: superclass Persondan subclass Johndan Mary; superclass Cardan subclass Volvodan Mazda; superclass Missiledan subclass SpeedRocked, LandMinedan TrippleExplodingThingy.

Sangat wajar untuk berpikir seperti ini, terutama bagi orang yang relatif baru mengenal OO.

Tetapi kita harus selalu ingat bahwa kelas adalah template , dan objek adalah konten yang dituangkan ke dalam template ini . Anda dapat menuangkan konten apa pun yang Anda inginkan ke dalam templat, menciptakan kemungkinan yang tak terhitung jumlahnya.

Bukan tugas subclass untuk mengisi templat. Ini pekerjaan dari objek. Tugas subclass adalah menambahkan fungsionalitas aktual , atau memperluas templat .

Dan itu sebabnya saya seharusnya membuat Scalekelas yang konkret , dengan Note[]bidang, dan membiarkan objek mengisi template ini ; mungkin melalui konstruktor atau sesuatu. Dan akhirnya, jadi saya melakukannya.

Setiap kali Anda mendesain templat di kelas (misalnya, anggota kosong Note[]yang perlu diisi, atau String namebidang yang perlu diberi nilai), ingat bahwa tugas dari objek kelas ini untuk mengisi templat ( tugas atau mungkin mereka yang membuat objek ini). Subkelas dimaksudkan untuk menambah fungsionalitas, bukan untuk mengisi templat.


Anda mungkin tergoda untuk membuat semacam "superclass Person, subclass, Johndan Mary" sistem, seperti yang Anda lakukan, karena Anda menyukai formalitas yang Anda dapatkan.

Dengan cara ini, Anda bisa mengatakan Person p = new Mary(), alih-alih Person p = new Person("Mary", 57, Sex.FEMALE). Itu membuat segalanya lebih teratur, dan lebih terstruktur. Tapi seperti yang kami katakan, membuat kelas baru untuk setiap kombinasi data bukanlah pendekatan yang baik, karena membengkokkan kode untuk apa-apa dan membatasi Anda dalam hal kemampuan runtime.

Jadi inilah solusinya: gunakan pabrik dasar, bahkan mungkin yang statis. Seperti itu:

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

    // ...
}

Dengan cara ini, Anda dapat dengan mudah menggunakan 'preset' yang 'datang dengan program', seperti:, Person mary = PersonFactory.createMary()tetapi Anda juga berhak untuk merancang orang baru secara dinamis, misalnya dalam hal Anda ingin mengizinkan pengguna untuk melakukannya . Misalnya:

// .. 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);

Atau bahkan lebih baik: lakukan sesuatu seperti itu:

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

Saya terbawa suasana. Saya pikir Anda mendapatkan idenya.


Subclass tidak dimaksudkan untuk mengisi template yang ditetapkan oleh superclasses mereka. Subkelas dimaksudkan untuk menambah fungsionalitas . Objek dimaksudkan untuk mengisi templat, itulah gunanya.

Anda seharusnya tidak membuat kelas baru untuk setiap kemungkinan kombinasi data. (Sama seperti saya seharusnya tidak membuat Scalesubclass baru untuk setiap kombinasi Notes yang mungkin).

Ini adalah pedoman: setiap kali Anda membuat subkelas baru, pertimbangkan apakah itu menambahkan fungsionalitas baru yang tidak ada di superclass. Jika jawaban untuk pertanyaan itu adalah "tidak", daripada Anda mungkin mencoba untuk 'mengisi template' dari superclass, dalam hal ini buat saja sebuah objek. (Dan mungkin Pabrik dengan 'preset', untuk membuat hidup lebih mudah).

Semoga itu bisa membantu.

Aviv Cohn
sumber
Ini adalah contoh yang bagus, terima kasih banyak.
Ignacio Soler Garcia
@ SoMoS Senang saya bisa membantu.
Aviv Cohn
2

Ini adalah pengecualian untuk 'harus menjadi contoh' di sini - meskipun sedikit ekstrim.

Jika Anda ingin kompiler untuk menegakkan izin untuk mengakses metode berdasarkan apakah itu Karl atau John (dalam contoh ini) daripada meminta implementasi metode untuk memeriksa instance mana yang telah berlalu maka Anda mungkin menggunakan kelas yang berbeda. Dengan menerapkan kelas yang berbeda daripada instantiasi maka Anda membuat pemeriksaan diferensiasi antara 'Karl' dan 'John' dimungkinkan pada waktu kompilasi daripada waktu berjalan dan ini mungkin berguna - khususnya dalam konteks keamanan di mana kompiler mungkin lebih dipercaya daripada waktu berjalan. pemeriksaan kode perpustakaan (misalnya) eksternal.

David Scholefield
sumber
2

Ini dianggap praktik buruk untuk memiliki kode duplikat .

Ini setidaknya sebagian karena garis kode dan karena itu ukuran basis kode berkorelasi dengan biaya pemeliharaan dan cacat .

Inilah logika akal sehat yang relevan bagi saya tentang topik khusus ini:

1) Jika saya perlu mengubah ini, berapa banyak tempat yang perlu saya kunjungi? Kurang lebih baik di sini.

2) Apakah ada alasan mengapa dilakukan seperti ini? Dalam contoh Anda - tidak mungkin ada - tetapi dalam beberapa contoh mungkin ada semacam alasan untuk melakukan ini. Satu yang bisa saya pikirkan dari atas kepala saya adalah dalam beberapa kerangka kerja seperti awal Grails di mana warisan tidak selalu bermain dengan baik dengan beberapa plugin untuk GORM. Jarang sekali.

3) Apakah lebih bersih dan mudah dipahami dengan cara ini? Sekali lagi, ini tidak ada dalam contoh Anda - tetapi ada beberapa pola pewarisan yang mungkin lebih merupakan peregangan di mana kelas yang terpisah sebenarnya lebih mudah untuk dipertahankan (penggunaan pola perintah atau strategi strategi tertentu muncul dalam pikiran).

dcgregorya
sumber
1
apakah itu buruk atau tidak tidak diatur dalam batu. Misalnya ada skenario di mana overhead tambahan dari pembuatan objek dan pemanggilan metode bisa sangat merugikan kinerja sehingga aplikasi tidak lagi memenuhi spesifikasi.
jwenting
Lihat poin # 2. Tentu ada alasan mengapa, sesekali. Itu sebabnya Anda perlu bertanya sebelum mengeluarkan kode seseorang.
dcgregorya
0

Ada use case: membentuk referensi ke fungsi atau metode sebagai objek biasa.

Anda dapat melihat ini dalam kode Java GUI:

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

Kompiler membantu Anda di sini dengan membuat subkelas anonim baru.

Simon Richter
sumber