Jangan pernah membuat anggota publik virtual / abstrak - benarkah?

20

Kembali pada tahun 2000-an, seorang kolega saya mengatakan kepada saya bahwa itu adalah anti-pola untuk membuat metode publik virtual atau abstrak.

Misalnya, ia menganggap kelas seperti ini tidak dirancang dengan baik:

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

Dia menyatakan itu

  • pengembang kelas turunan yang mengimplementasikan Method1dan menimpa Method2harus mengulangi validasi argumen.
  • dalam hal pengembang kelas dasar memutuskan untuk menambahkan sesuatu di sekitar bagian yang dapat dikustomisasi Method1atau yang Method2lebih baru, ia tidak dapat melakukannya.

Sebagai gantinya, kolega saya mengusulkan pendekatan ini:

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

Dia mengatakan kepada saya membuat metode publik (atau properti) virtual atau abstrak sama buruknya dengan membuat bidang publik. Dengan membungkus bidang ke dalam properti, seseorang dapat mencegat akses apa pun ke bidang itu nanti, jika perlu. Hal yang sama berlaku untuk anggota virtual / abstrak publik: membungkus mereka seperti yang ditunjukkan dalam ProtectedAbstractOrVirtualkelas memungkinkan pengembang kelas dasar untuk mencegat panggilan yang masuk ke metode virtual / abstrak.

Tapi saya tidak melihat ini sebagai pedoman desain. Bahkan Microsoft tidak mengikutinya: lihat saja Streamkelas untuk memverifikasi ini.

Apa pendapat Anda tentang pedoman itu? Apakah itu masuk akal, atau Anda pikir itu terlalu rumit API?

Peter Perot
sumber
5
Metode pembuatan virtual memungkinkan penggantian opsional. Metode Anda mungkin harus bersifat publik, karena mungkin tidak diganti. Membuat metode abstract memaksa Anda untuk menimpanya; mereka seharusnya protected, karena mereka tidak terlalu berguna dalam suatu publickonteks.
Robert Harvey
4
Sebenarnya, protectedini paling berguna ketika Anda ingin mengekspos anggota pribadi dari kelas abstrak ke kelas turunan. Bagaimanapun, saya tidak terlalu peduli dengan pendapat teman Anda; pilih pengubah akses yang paling masuk akal untuk situasi khusus Anda.
Robert Harvey
4
Kolega Anda menganjurkan untuk Pola Metode Templat . Dapat digunakan kasus untuk kedua cara, tergantung pada seberapa saling tergantung kedua metode tersebut.
Greg Burghardt
8
@GregBurghardt: terdengar seperti kolega OP menyarankan untuk selalu menggunakan pola metode templat, terlepas dari apakah itu diperlukan atau tidak. Itu adalah pola yang terlalu sering digunakan - jika seseorang memiliki palu, cepat atau lambat setiap masalah mulai terlihat seperti paku ;-)
Doc Brown
3
@PeterPerot: Saya tidak pernah punya masalah untuk memulai dengan DTO sederhana hanya dengan bidang publik, dan ketika DTO seperti itu membutuhkan anggota dengan logika bisnis, refactor mereka ke kelas dengan properti. Tentunya hal-hal berbeda ketika seseorang bekerja sebagai vendor perpustakaan dan harus peduli untuk tidak mengubah API publik, kemudian bahkan mengubah bidang publik menjadi properti publik dengan nama yang sama dapat menyebabkan masalah.
Doc Brown

Jawaban:

30

Pepatah

bahwa itu adalah anti-pola untuk membuat metode publik virtual atau abstrak karena pengembang kelas turunan yang mengimplementasikan Method1 dan menimpa Method2 harus mengulangi validasi argumen

mencampuradukkan sebab dan akibat. Itu membuat asumsi bahwa setiap metode yang dapat ditimpa memerlukan validasi argumen yang tidak dapat disesuaikan. Namun sebaliknya:

Jika seseorang ingin merancang metode dengan cara yang menyediakan beberapa validasi argumen tetap di semua derivasi kelas (atau - lebih umum - bagian yang dapat disesuaikan dan tidak dapat disesuaikan), maka masuk akal untuk membuat titik masuknya non-virtual , dan sebagai gantinya menyediakan metode virtual atau abstrak untuk bagian yang dapat disesuaikan yang disebut secara internal.

Tapi ada banyak contoh di mana itu masuk akal untuk memiliki metode virtual publik, karena tidak ada tetap non-disesuaikan bagian: lihat metode standar seperti ToStringatau Equalsatau GetHashCode- akan itu meningkatkan desainobject kelas untuk memiliki ini tidak umum dan virtual pada saat bersamaan? Saya kira tidak.

Atau, dalam hal kode Anda sendiri: ketika kode di kelas dasar akhirnya dan sengaja terlihat seperti ini

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

pemisahan antara Method1dan Method1Corehanya mempersulit hal-hal tanpa alasan yang jelas.

Doc Brown
sumber
1
Dalam hal ToString()metode, Microsoft lebih baik membuatnya non-virtual dan memperkenalkan metode templat virtual ToStringCore(). Mengapa: karena ini: ToString()- catatan untuk pewaris . Mereka menyatakan bahwa ToString()seharusnya tidak mengembalikan null. Mereka bisa saja memaksa permintaan ini dengan menerapkan ToString() => ToStringCore() ?? string.Empty.
Peter Perot
9
@PeterPerot: pedoman yang Anda tautkan juga merekomendasikan untuk tidak kembali string.Empty, apakah Anda perhatikan? Dan itu merekomendasikan banyak hal lain yang tidak dapat ditegakkan dalam kode dengan memperkenalkan sesuatu seperti ToStringCoremetode. Jadi teknik ini mungkin bukan alat yang tepat untuk ToString.
Doc Brown
3
@Theraot: pasti orang dapat menemukan beberapa alasan atau argumen mengapa ToString dan Equals atau GetHashcode bisa dirancang secara berbeda, tetapi hari ini mereka sebagaimana adanya (setidaknya saya pikir desain mereka cukup baik untuk membuat contoh yang baik).
Doc Brown
1
@PeterPerot "Saya melihat banyak kode seperti anyObjectIGot.ToString()bukannya anyObjectIGot?.ToString()" - bagaimana ini relevan? ToStringCore()Pendekatan Anda akan mencegah string null dikembalikan, tetapi masih akan melempar NullReferenceExceptionjika objek tersebut null.
IMil
1
@PeterPerot Saya tidak berusaha menyampaikan argumen dari otoritas. Tidak banyak yang digunakan Microsoft public virtual, tetapi ada beberapa kasus yang public virtualok. Kita bisa berargumen untuk bagian yang tidak dapat dikustomisasi kosong sebagai pembuatan kode masa depan bukti ... Tapi itu tidak berhasil. Kembali dan mengubahnya dapat merusak tipe turunan. Jadi, itu tidak menghasilkan apa-apa.
Theraot
6

Melakukannya seperti yang disarankan oleh kolega Anda memang memberikan lebih banyak fleksibilitas kepada implementor kelas dasar. Tetapi dengan itu juga muncul lebih banyak kompleksitas yang biasanya tidak dibenarkan oleh manfaat yang diperkirakan.

Pikiran bahwa peningkatan fleksibilitas untuk pelaksana kelas dasar datang dengan mengorbankan fleksibilitas yang lebih rendah kepada pihak utama. Mereka mendapatkan perilaku yang dipaksakan yang mungkin tidak mereka pedulikan. Bagi mereka hal-hal baru saja menjadi lebih kaku. Ini dapat dibenarkan dan bermanfaat tetapi ini semua tergantung pada skenario.

Konvensi penamaan untuk mengimplementasikan ini (yang saya tahu) adalah untuk mencadangkan nama baik untuk antarmuka publik dan untuk mengawali nama metode internal dengan "Do".

Satu kasus yang bermanfaat adalah ketika tindakan yang dilakukan membutuhkan beberapa pengaturan dan beberapa penutupan. Seperti membuka aliran dan menutupnya setelah overrider selesai. Secara umum, inisialisasi dan finalisasi yang sama. Ini adalah pola yang valid untuk digunakan tetapi tidak ada gunanya untuk mengamanatkan penggunaannya dalam semua skenario abstrak dan virtual.

Martin Maat
sumber
1
The Do Metode prefix adalah salah satu pilihan. Microsoft sering menggunakan postfix metode Core .
Peter Perot
@Peter Perot. Saya tidak pernah melihat awalan Core dalam materi Microsoft apa pun, tetapi ini mungkin karena saya belum banyak memperhatikan belakangan ini. Saya menduga mereka mulai melakukan ini baru-baru ini hanya untuk mempromosikan moniker Core, untuk membuat nama untuk .NET Core.
Martin Maat
Tidak, itu sebuah topi tua: BindingList. Selain itu saya menemukan rekomendasi di suatu tempat, mungkin Pedoman Desain Kerangka mereka atau yang serupa. Dan: ini adalah postfix . ;-)
Peter Perot
Intinya kurang fleksibel untuk kelas turunan. Kelas dasar adalah batas abstraksi. Kelas dasar memberi tahu konsumen apa yang dilakukan API publiknya, dan mendefinisikan API yang diperlukan untuk mencapai tujuan tersebut. Jika kelas turunan dapat menimpa metode publik di kelas dasar, Anda berisiko meningkatkan pelanggaran Prinsip Pergantian Liskov.
Adrian McCarthy
2

Dalam C ++, ini disebut pola antarmuka non-virtual (NVI). (Sekali waktu, itu disebut Metode Templat. Itu membingungkan, tetapi beberapa artikel yang lebih tua memiliki terminologi itu.) NVI dipromosikan oleh Herb Sutter, yang telah menulis tentang hal itu setidaknya beberapa kali. Saya pikir salah satu yang paling awal ada di sini .

Jika saya ingat dengan benar, premisnya adalah bahwa kelas turunan tidak boleh mengubah apa yang dilakukan kelas dasar tetapi bagaimana melakukannya.

Misalnya, Bentuk mungkin memiliki metode Pindahkan untuk memindahkan bentuk. Implementasi konkret (mis., Seperti Kotak dan Lingkaran) tidak boleh secara langsung mengesampingkan Move, karena Shape mendefinisikan apa arti Moving (pada level konseptual). Kotak mungkin memiliki detail implementasi yang berbeda dari Lingkaran dalam hal bagaimana posisi diwakili secara internal, sehingga mereka harus mengganti beberapa metode untuk menyediakan fungsionalitas Pindah.

Dalam contoh sederhana, ini sering bermuara pada Pindah publik yang hanya mendelegasikan semua pekerjaan ke virtual pribadi ReallyDoTheMove, jadi sepertinya banyak overhead tanpa manfaat.

Tetapi korespondensi satu-satu ini bukan keharusan. Misalnya, Anda bisa menambahkan metode Animate ke API publik Shape, dan mungkin menerapkannya dengan memanggil ReallyDoTheMove dalam satu lingkaran. Anda berakhir dengan dua metode publik non-virtual API yang keduanya bergantung pada satu metode abstrak pribadi. Lingkaran dan Kuadrat Anda tidak perlu melakukan pekerjaan ekstra apa pun, dan Override Animate juga tidak bisa .

Kelas dasar mendefinisikan antarmuka publik yang digunakan oleh konsumen, dan mendefinisikan antarmuka operasi primitif yang diperlukan untuk mengimplementasikan metode publik tersebut. Tipe turunannya bertanggung jawab untuk menyediakan implementasi operasi primitif tersebut.

Saya tidak mengetahui adanya perbedaan antara C # dan C ++ yang akan mengubah aspek desain kelas ini.

Adrian McCarthy
sumber
Bagus temukan! Sekarang saya ingat bahwa saya menemukan tepat yang memposting tautan ke-2 Anda ke (atau salinannya), pada tahun 2000-an. Saya ingat bahwa saya sedang mencari lebih banyak bukti klaim kolega saya, dan bahwa saya tidak menemukan apa pun dalam konteks C #, tetapi C ++. Ini. Adalah. Itu! :-) Tapi, kembali ke tanah C #, sepertinya pola ini tidak banyak digunakan. Mungkin orang menyadari bahwa menambahkan fungsionalitas basis nantinya dapat memecah kelas turunan juga, dan bahwa penggunaan TMP atau NVIP yang ketat sebagai ganti metode virtual publik tidak selalu masuk akal.
Peter Perot
Catatan untuk diri sendiri: baik untuk mengetahui pola ini memiliki nama: NVIP.
Peter Perot