Antarmuka umum vs umum?

20

Saya tidak ingat kapan saya menulis kelas generik terakhir kali. Setiap kali saya pikir saya membutuhkannya setelah berpikir saya membuat kesimpulan saya tidak membutuhkannya.

Jawaban kedua untuk pertanyaan ini membuat saya meminta klarifikasi (karena saya belum bisa berkomentar, saya membuat pertanyaan baru).

Jadi mari kita ambil kode yang diberikan sebagai contoh kasus di mana kita membutuhkan obat generik:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Ini memiliki batasan tipe: IBusinessObject

Cara berpikir saya yang biasa adalah: kelas dibatasi untuk digunakan IBusinessObject, begitu juga kelas yang menggunakan ini Repository. Repositori menyimpan ini IBusinessObject, kemungkinan besar klien ini Repositoryingin mendapatkan dan menggunakan objek melalui IBusinessObjectantarmuka. Jadi mengapa tidak melakukannya saja

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Ya, contohnya tidak bagus, karena itu hanya jenis koleksi lain dan koleksi umum adalah klasik. Dalam hal ini jenis kendala juga terlihat aneh.

Sebenarnya contohnya class Repository<T> where T : class, IBusinessbBjectterlihat sangat mirip class BusinessObjectRepositorydengan saya. Yang merupakan hal yang dibuat generik untuk diperbaiki.

Intinya adalah: apakah generik baik untuk apa pun kecuali koleksi dan jangan jenis kendala menjadikan generik sebagai khusus, seperti penggunaan batasan tipe ini alih-alih parameter tipe generik di dalam kelas?

jungle_mole
sumber

Jawaban:

24

Mari kita bicara tentang polimorfisme parametrik murni dan membahas polimorfisme terikat nanti.

Polimorfisme parametrik

Apa arti parametrik polimorfisme? Nah, itu berarti bahwa suatu tipe, atau lebih tepatnya tipe konstruktor di- parameterkan oleh suatu tipe. Karena jenis dilewatkan sebagai parameter, Anda tidak dapat mengetahui sebelumnya apa itu mungkin. Anda tidak dapat membuat asumsi berdasarkan itu. Sekarang, jika Anda tidak tahu apa itu mungkin, lalu apa gunanya? Apa yang bisa kamu lakukan dengan itu?

Nah, Anda bisa menyimpan dan mengambilnya, misalnya. Itu kasus yang sudah Anda sebutkan: koleksi. Untuk menyimpan item dalam daftar atau array, saya tidak perlu tahu apa-apa tentang item tersebut. Daftar atau larik dapat sepenuhnya mengabaikan jenis.

Tapi bagaimana dengan Maybetipenya? Jika Anda tidak terbiasa dengan itu, Maybeadalah tipe yang mungkin memiliki nilai dan mungkin tidak. Di mana Anda akan menggunakannya? Misalnya, ketika mengeluarkan item dari kamus: fakta bahwa item mungkin tidak ada dalam kamus bukanlah situasi yang luar biasa, jadi Anda benar-benar tidak boleh melempar pengecualian jika item itu tidak ada. Sebagai gantinya, Anda mengembalikan contoh subtipe dari Maybe<T>, yang memiliki tepat dua subtipe: Nonedan Some<T>. int.Parseadalah kandidat lain dari sesuatu yang benar-benar harus mengembalikan Maybe<int>bukannya melemparkan pengecualian atau seluruh int.TryParse(out bla)tarian.

Sekarang, Anda mungkin berpendapat bahwa Maybeini agak seperti daftar yang hanya dapat memiliki nol atau satu elemen. Dan dengan demikian agak-agak koleksi.

Lalu bagaimana dengan Task<T>? Ini adalah tipe yang berjanji untuk mengembalikan nilai di beberapa titik di masa mendatang, tetapi tidak harus memiliki nilai sekarang.

Atau bagaimana Func<T, …>? Bagaimana Anda akan merepresentasikan konsep fungsi dari satu tipe ke tipe lain jika Anda tidak dapat abstrak lebih dari tipe?

Atau, lebih umum: mengingat bahwa abstraksi dan penggunaan kembali adalah dua operasi mendasar dari rekayasa perangkat lunak, mengapa Anda tidak ingin dapat abstrak lebih dari tipe?

Polimorfisme Terikat

Jadi, sekarang mari kita bicara tentang polimorfisme terbatas. Polimorfisme terikat pada dasarnya adalah tempat polimorfisme parametrik dan polimorfisme subtipe bertemu: alih-alih konstruktor tipe yang sepenuhnya tidak mengetahui tentang parameter jenisnya, Anda dapat mengikat (atau membatasi) tipe menjadi subtipe dari beberapa tipe tertentu.

Ayo kembali ke koleksi. Ambil hashtable. Kami katakan di atas bahwa daftar tidak perlu tahu apa-apa tentang elemen-elemennya. Yah, hashtable tidak: perlu tahu bahwa itu bisa hash mereka. (Catatan: dalam C #, semua objek dapat hashable, sama seperti semua objek dapat dibandingkan untuk kesetaraan. Namun, itu tidak berlaku untuk semua bahasa, dan kadang-kadang dianggap kesalahan desain bahkan dalam C #.)

Jadi, Anda ingin membatasi parameter tipe Anda untuk jenis kunci dalam hashtable menjadi turunan dari IHashable:

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

Bayangkan jika Anda memiliki ini:

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

Apa yang akan Anda lakukan dengan valueAnda keluar dari sana? Anda tidak dapat melakukan apa pun dengan itu, Anda hanya tahu itu objek. Dan jika Anda mengulanginya, yang Anda dapatkan hanyalah sepasang dari sesuatu yang Anda tahu adalah IHashable(yang tidak banyak membantu Anda karena hanya memiliki satu properti Hash) dan sesuatu yang Anda tahu adalah object(yang membantu Anda lebih sedikit).

Atau sesuatu berdasarkan contoh Anda:

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

Item harus serializable karena akan disimpan di disk. Tetapi bagaimana jika Anda memiliki ini sebagai gantinya:

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

Dengan kasus generik, jika Anda menempatkan BankAccountdi, Anda mendapatkan BankAccountkembali, dengan metode dan properti seperti Owner, AccountNumber, Balance, Deposit, Withdraw, dll Sesuatu Anda dapat bekerja dengan. Sekarang, yang lainnya? Anda dimasukkan ke dalam BankAccounttetapi Anda mendapatkan kembali Serializable, yang hanya memiliki satu properti: AsString. Apa yang akan kamu lakukan dengan itu?

Ada juga beberapa trik rapi yang dapat Anda lakukan dengan polimorfisme terbatas:

Polimorfisme terikat-F

Kuantifikasi F-bounded pada dasarnya adalah di mana variabel tipe muncul lagi dalam kendala. Ini dapat bermanfaat dalam beberapa keadaan. Misalnya bagaimana Anda menulis sebuah ICloneableantarmuka? Bagaimana Anda menulis metode di mana tipe pengembalian adalah tipe kelas pelaksana? Dalam bahasa dengan fitur MyType , itu mudah:

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

Dalam bahasa dengan polimorfisme terbatas, Anda dapat melakukan sesuatu seperti ini sebagai gantinya:

interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

Perhatikan bahwa ini tidak seaman versi MyType, karena tidak ada yang menghentikan seseorang dari hanya meneruskan kelas "salah" ke konstruktor tipe:

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

Tipe Abstrak Anggota

Ternyata, jika Anda memiliki anggota tipe abstrak dan subtipe, Anda dapat benar-benar bertahan tanpa polimorfisme parametrik dan masih melakukan semua hal yang sama. Scala sedang menuju ke arah ini, menjadi bahasa utama pertama yang dimulai dengan generik dan kemudian mencoba untuk menghapusnya, yang persis sebaliknya dari Jawa dan C #.

Pada dasarnya, di Scala, sama seperti Anda dapat memiliki bidang dan properti serta metode sebagai anggota, Anda juga dapat memiliki jenis. Dan seperti halnya bidang dan properti serta metode dapat dibiarkan abstrak untuk diimplementasikan dalam subkelas nanti, anggota tipe juga dapat dibiarkan abstrak. Mari kita kembali ke koleksi, yang sederhana List, yang akan terlihat seperti ini, jika didukung di C #:

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics
Jörg W Mittag
sumber
saya mengerti, bahwa abstraksi atas tipe berguna. saya hanya tidak melihat penggunaannya dalam "kehidupan nyata". Fungsi <> dan Tugas <> dan Tindakan <> adalah tipe perpustakaan. dan terima kasih saya ingat interface IFoo<T> where T : IFoo<T>juga. itu jelas aplikasi kehidupan nyata. contohnya bagus. tetapi untuk beberapa alasan saya tidak merasa puas. Saya lebih suka untuk mendapatkan pikiran saya ketika itu tepat dan ketika tidak. jawaban di sini memiliki kontribusi untuk proses ini, tetapi saya masih merasa tidak nyaman dengan semua ini. itu aneh karena masalah tingkat bahasa sudah lama tidak mengganggu saya.
jungle_mole
Contoh yang bagus. Aku merasa seperti kembali ke ruang kelas. +1
Chef_Code
1
@Chef_Code: Saya harap itu pujian :-P
Jörg W Mittag
Ya itu. Saya kemudian berpikir tentang bagaimana hal itu dapat dirasakan setelah saya berkomentar. Jadi untuk mengkonfirmasi ketulusan ... Ya itu adalah pujian, tidak ada yang lain.
Chef_Code
14

Intinya adalah: apakah generik baik untuk apa pun kecuali koleksi dan jangan jenis kendala menjadikan generik sebagai khusus, seperti penggunaan batasan tipe ini alih-alih parameter tipe generik di dalam kelas?

No Anda berpikir terlalu banyak tentang Repository, di mana ia adalah hampir sama. Tapi bukan untuk apa obat generik itu ada. Mereka ada di sana untuk para pengguna .

Poin kuncinya di sini bukanlah bahwa Repositori itu sendiri lebih umum. Ini karena para pengguna lebih terspesialisasi - yaitu, itu Repository<BusinessObject1>dan Repository<BusinessObject2>jenis yang berbeda, dan lebih jauh lagi, bahwa jika saya mengambil Repository<BusinessObject1>, saya tahu bahwa saya akan BusinessObject1keluar dari Get.

Anda tidak dapat menawarkan pengetikan yang kuat ini dari warisan sederhana. Kelas Repositori yang Anda usulkan tidak melakukan apa pun untuk mencegah orang-orang membuat repositori yang membingungkan untuk berbagai jenis objek bisnis atau menjamin jenis objek bisnis yang benar akan muncul kembali.

DeadMG
sumber
Terima kasih, itu masuk akal. Tetapi apakah seluruh titik penggunaan fitur bahasa yang sangat dipuji ini sesederhana membantu pengguna yang menonaktifkan IntelliSense? (Saya sedikit melebih-lebihkan, tapi saya yakin Anda mengerti maksudnya)
jungle_mole
@ zloidooraque: IntelliSense juga tidak tahu jenis objek apa yang disimpan dalam repositori. Tapi ya, Anda bisa melakukan apa saja tanpa obat generik jika Anda bersedia menggunakan gips.
gexicide
@ gexicide intinya: Saya tidak melihat di mana saya perlu menggunakan gips jika saya menggunakan antarmuka umum. Saya tidak pernah mengatakan "gunakanObject ". Saya juga mengerti mengapa menggunakan obat generik saat menulis koleksi (prinsip KERING). Mungkin, pertanyaan awal saya seharusnya adalah sesuatu tentang menggunakan obat generik di luar konteks koleksi ..
jungle_mole
@zloidooraque: Ini tidak ada hubungannya dengan lingkungan. Intellisense tidak dapat memberi tahu Anda apakah IBusinessObjecta BusinessObject1atau a BusinessObject2. Itu tidak bisa menyelesaikan kelebihan berdasarkan pada tipe turunan yang tidak diketahui. Itu tidak bisa menolak kode yang masuk dalam tipe yang salah. Ada sejuta bit dari pengetikan yang lebih kuat yang tidak bisa dilakukan oleh Intellisense sama sekali. Dukungan perkakas yang lebih baik adalah manfaat yang bagus tetapi benar-benar tidak ada hubungannya dengan alasan intinya.
DeadMG
@DeadMG dan itu maksud saya: Intellisense tidak bisa melakukannya: gunakan generik, jadi bisa? apakah itu penting? ketika Anda mendapatkan objek dengan antarmuka, mengapa tertekan itu? jika Anda melakukannya, itu desain yang buruk, bukan? dan mengapa dan apa itu "mengatasi kelebihan"? pengguna tidak boleh memutuskan, apakah metode panggilan atau tidak berdasarkan tipe turunan jika ia mendelegasikan panggilan metode yang tepat ke sistem (yang polimorfismenya). dan ini sekali lagi membawa saya ke sebuah pertanyaan: apakah obat generik berguna di luar wadah? Saya tidak berdebat dengan Anda, saya benar-benar perlu memahami itu.
jungle_mole
13

"kemungkinan besar klien dari Repositori ini ingin mendapatkan dan menggunakan objek melalui antarmuka IBusinessObject".

Tidak, mereka tidak akan melakukannya.

Mari kita pertimbangkan bahwa IBusinessObject memiliki definisi berikut:

public interface IBusinessObject
{
  public int Id { get; }
}

Itu hanya mendefinisikan Id karena itu adalah satu-satunya fungsi bersama antara semua objek bisnis. Dan Anda memiliki dua objek bisnis yang sebenarnya: Orang dan Alamat (karena Orang tidak memiliki jalan dan Alamat tidak memiliki nama, Anda tidak dapat membatasi keduanya ke antarmuka umum dengan fungsi keduanya. Itu akan menjadi desain yang mengerikan, melanggar Prinsip Segragation Antarmuka , "I" dalam SOLID )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

Sekarang, apa yang terjadi ketika Anda menggunakan versi generik repositori:

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Ketika Anda memanggil metode Dapatkan di repositori generik objek yang dikembalikan akan sangat diketik, memungkinkan Anda untuk mengakses semua anggota kelas.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

Di sisi lain, ketika Anda menggunakan Gudang non-generik:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Anda hanya akan dapat mengakses anggota dari antarmuka IBusinessObject:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

Jadi, kode sebelumnya tidak dapat dikompilasi karena baris berikut:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

Tentu, Anda dapat melemparkan IBussinesObject ke kelas aktual, tetapi Anda akan kehilangan semua waktu kompilasi yang diizinkan oleh obat generik (yang mengarah ke InvalidCastExceptions di ujung jalan), akan menderita overhead pemain yang tidak perlu ... Dan bahkan jika Anda tidak peduli kompilasi waktu memeriksa kedua kinerja (Anda harus), casting setelah pasti tidak akan memberi Anda manfaat lebih dari menggunakan obat generik di tempat pertama.

Lucas Corsaletti
sumber