Bagaimana memperingatkan programmer lain tentang implementasi kelas

96

Saya menulis kelas yang "harus digunakan dengan cara tertentu" (saya kira semua kelas harus ...).

Misalnya, saya membuat fooManagerkelas, yang membutuhkan panggilan ke, katakanlah Initialize(string,string),. Dan, untuk mendorong contoh sedikit lebih jauh, kelas tidak akan berguna jika kita tidak mendengarkan ThisHappenedaksinya.

Maksud saya adalah, kelas yang saya tulis membutuhkan pemanggilan metode. Tapi itu akan dikompilasi dengan baik jika Anda tidak memanggil metode itu dan akan berakhir dengan FooManager baru yang kosong. Pada titik tertentu, itu tidak akan berfungsi, atau mungkin crash, tergantung pada kelas dan apa fungsinya. Programer yang mengimplementasikan kelas saya jelas akan melihat ke dalamnya dan menyadari "Oh, saya tidak memanggil Inisialisasi!", Dan itu akan baik-baik saja.

Tapi saya tidak suka itu. Apa yang saya inginkan adalah kode untuk TIDAK dikompilasi jika metode tidak dipanggil; Saya kira itu tidak mungkin. Atau sesuatu yang akan segera terlihat dan jelas.

Saya menemukan diri saya bermasalah dengan pendekatan saat ini yang saya miliki di sini, yaitu sebagai berikut:

Tambahkan nilai Boolean pribadi di kelas, dan periksa di mana pun diperlukan jika kelas diinisialisasi; jika tidak, saya akan melemparkan pengecualian yang mengatakan "Kelas tidak diinisialisasi, apakah Anda yakin Anda menelepon .Initialize(string,string)?".

Saya agak baik-baik saja dengan pendekatan itu, tetapi mengarah ke banyak kode yang dikompilasi dan, pada akhirnya, tidak perlu bagi pengguna akhir.

Juga, kadang-kadang bahkan lebih banyak kode ketika ada lebih banyak metode daripada sekadar Initiliazemenelepon. Saya mencoba menjaga kelas saya dengan tidak terlalu banyak metode / tindakan publik, tapi itu tidak menyelesaikan masalah, hanya membuatnya masuk akal.

Yang saya cari di sini adalah:

  • Apakah pendekatan saya benar?
  • Apakah ada yang lebih baik?
  • Apa yang kalian lakukan / sarankan?
  • Apakah saya mencoba menyelesaikan non-masalah? Saya telah diberitahu oleh kolega bahwa programmerlah yang memeriksa kelas sebelum mencoba menggunakannya. Saya dengan hormat tidak setuju, tapi itu masalah lain yang saya yakini.

Sederhananya, saya mencoba mencari cara untuk tidak pernah lupa untuk mengimplementasikan panggilan ketika kelas itu digunakan kembali nanti, atau oleh orang lain.

KLARIFIKASI:

Untuk mengklarifikasi banyak pertanyaan di sini:

  • Saya jelas TIDAK hanya berbicara tentang bagian Inisialisasi kelas, tetapi itu seumur hidup. Cegah rekan kerja untuk memanggil metode dua kali, pastikan mereka memanggil X sebelum Y, dll. Apa pun yang pada akhirnya wajib dan dalam dokumentasi, tetapi saya ingin kode dan sesederhana dan sekecil mungkin. Saya benar-benar menyukai ide Asserts, meskipun saya cukup yakin saya perlu mencampurkan beberapa ide lain karena Asserts tidak akan selalu mungkin.

  • Saya menggunakan bahasa C #! Bagaimana saya tidak menyebutkan itu ?! Saya berada di lingkungan Xamarin dan membangun aplikasi seluler yang biasanya menggunakan sekitar 6 hingga 9 proyek dalam solusi, termasuk proyek PCL, iOS, Android, dan Windows. Saya telah menjadi pengembang selama sekitar satu setengah tahun (gabungan sekolah dan pekerjaan), karenanya pernyataan \ pertanyaan saya yang terkadang konyol. Semua itu mungkin tidak relevan di sini, tetapi terlalu banyak informasi tidak selalu buruk.

  • Saya tidak bisa selalu meletakkan segala sesuatu yang wajib dalam konstruktor, karena pembatasan platform dan penggunaan Dependency Injection, memiliki parameter selain Antarmuka tidak dimatikan. Atau mungkin pengetahuan saya tidak cukup, yang sangat memungkinkan. Sebagian besar waktu itu bukan masalah Inisialisasi, tetapi lebih

bagaimana saya bisa memastikan dia mendaftar ke acara itu?

bagaimana saya bisa memastikan dia tidak lupa untuk "menghentikan proses di beberapa titik"

Di sini saya ingat kelas Pengambilan iklan. Selama tampilan tempat Iklan terlihat, kelas akan mengambil iklan baru setiap menit. Kelas itu membutuhkan tampilan ketika dibangun di mana ia dapat menampilkan Iklan, yang bisa masuk dalam parameter jelas. Tapi begitu tampilan hilang, StopFetching () harus dipanggil. Kalau tidak, kelas akan terus mengambil iklan untuk tampilan yang bahkan tidak ada, dan itu buruk.

Juga, kelas itu memiliki acara yang harus didengarkan, seperti "AdClicked" misalnya. Semuanya berfungsi dengan baik jika tidak didengarkan, tetapi kami kehilangan pelacakan analitik di sana jika ketukan tidak terdaftar. Iklan masih berfungsi, sehingga pengguna dan pengembang tidak akan melihat perbedaan, dan analitik hanya akan memiliki data yang salah. Itu perlu, dihindari, tapi saya tidak yakin bagaimana pengembang bisa tahu mereka harus mendaftar ke acara tao. Itu adalah contoh yang disederhanakan, tetapi idenya ada di sana, "pastikan dia menggunakan Aksi publik yang tersedia" dan pada waktu yang tepat tentu saja!

Gil Sand
sumber
13
Memastikan bahwa panggilan ke metode kelas terjadi dalam urutan tertentu tidak mungkin secara umum. Ini adalah salah satu dari banyak, banyak masalah yang setara dengan masalah penghentian. Meskipun dalam beberapa kasus mungkin membuat kesalahan ketidakpatuhan sebagai kesalahan waktu kompilasi , tidak ada solusi umum. Itu mungkin mengapa beberapa bahasa mendukung konstruksi yang akan membiarkan Anda mengambil diagnosa setidaknya kasus yang jelas, meskipun analisis aliran data telah menjadi sangat canggih.
Kilian Foth
5
Jawaban untuk pertanyaan lain tidak relevan, pertanyaannya tidak sama, oleh karena itu pertanyaannya berbeda. Jika seseorang mencari diskusi itu, dia tidak akan mengetik judul pertanyaan lain.
Gil Sand
6
Apa yang mencegah untuk menggabungkan konten inisialisasi di ctor? Haruskah panggilan initializedibuat terlambat setelah pembuatan objek? Apakah sang aktor terlalu "berisiko" dalam arti ia bisa melempar pengecualian dan memutus rantai penciptaan?
Terlihat
7
Masalah ini disebut kopling sementara . Jika Anda bisa, cobalah untuk menghindari menempatkan objek dalam keadaan tidak valid dengan melemparkan pengecualian dalam konstruktor ketika menemukan input yang tidak valid, dengan cara itu Anda tidak akan pernah bisa melewati panggilan newjika objek tidak siap untuk digunakan. Mengandalkan metode inisialisasi pasti akan kembali menghantui Anda dan harus dihindari kecuali benar-benar diperlukan, setidaknya itulah pengalaman saya.
Serialisasi

Jawaban:

161

Dalam kasus seperti itu, yang terbaik adalah menggunakan sistem jenis bahasa Anda untuk membantu Anda dengan inisialisasi yang tepat. Bagaimana kita mencegah agar FooManagertidak digunakan tanpa diinisialisasi? Dengan mencegah FooManageragar tidak dibuat tanpa informasi yang diperlukan untuk menginisialisasi dengan benar. Secara khusus, semua inisialisasi adalah tanggung jawab konstruktor. Anda tidak boleh membiarkan konstruktor Anda membuat objek dalam keadaan ilegal.

Tetapi penelepon perlu membuat FooManagersebelum mereka dapat menginisialisasi, misalnya karena FooManagerdilewatkan sebagai ketergantungan.

Jangan membuat FooManagerjika Anda tidak memilikinya. Apa yang dapat Anda lakukan sebagai gantinya adalah melewatkan objek di sekitar yang memungkinkan Anda mengambil yang dibangun sepenuhnya FooManager, tetapi hanya dengan informasi inisialisasi. (Dalam berbicara pemrograman fungsional, saya sarankan Anda menggunakan aplikasi parsial untuk konstruktor.) Mis:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

Masalahnya adalah Anda harus memberikan info init setiap kali mengakses FooManager.

Jika perlu dalam bahasa Anda, Anda bisa membungkus getFooManager()operasi dalam kelas seperti pabrik atau pembangun.

Saya benar-benar ingin melakukan pengecekan runtime bahwa initialize()metode itu dipanggil, daripada menggunakan solusi tipe-sistem-level.

Dimungkinkan untuk menemukan kompromi. Kami membuat kelas pembungkus MaybeInitializedFooManageryang memiliki get()metode yang mengembalikan FooManager, tetapi melempar jika FooManagertidak sepenuhnya diinisialisasi. Ini hanya berfungsi jika inisialisasi dilakukan melalui pembungkus, atau jika ada FooManager#isInitialized()metode.

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

Saya tidak ingin mengubah API kelas saya.

Dalam hal ini, Anda harus menghindari if (!initialized) throw;persyaratan dalam setiap metode. Untungnya, ada pola sederhana untuk menyelesaikan ini.

Objek yang Anda berikan kepada pengguna hanyalah shell kosong yang mendelegasikan semua panggilan ke objek implementasi. Secara default, objek implementasi melempar kesalahan untuk setiap metode yang tidak diinisialisasi. Namun, initialize()metode ini menggantikan objek implementasi dengan objek yang dibangun sepenuhnya.

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

Ini mengekstrak perilaku utama kelas ke dalam MainImpl.

amon
sumber
9
Saya suka dua solusi pertama (+1), tetapi saya tidak berpikir cuplikan terakhir Anda dengan pengulangan metode di FooManagerdan di UninitialisedImpllebih baik daripada mengulangi if (!initialized) throw;.
Bergi
3
@Bergi Beberapa orang terlalu menyukai kelas. Meskipun jika FooManagermemiliki banyak metode, mungkin lebih mudah daripada berpotensi melupakan beberapa if (!initialized)pemeriksaan. Tetapi dalam hal ini Anda mungkin harus memilih untuk memecah kelas.
user253751
1
A MaybeInitializedFoo sepertinya tidak lebih baik daripada Foo yang diinisialisasi juga untuk saya, tetapi +1 untuk memberikan beberapa opsi / ide.
Matthew James Briggs
7
+1 Pabrik adalah opsi yang benar. Anda tidak dapat meningkatkan desain tanpa mengubahnya, jadi IMHO jawaban terakhir tentang bagaimana setengahnya tidak akan membantu OP.
Nathan Cooper
6
@Bergi Saya menemukan teknik yang disajikan di bagian terakhir sangat berguna (lihat juga teknik ganti “Conditionals through Polymorphism”, dan State Pattern). Sangat mudah untuk melupakan centang dalam satu metode jika kita menggunakan if / selain itu, jauh lebih sulit untuk melupakan menerapkan metode yang diperlukan oleh antarmuka. Yang paling penting, MainImpl sesuai persis dengan objek di mana metode inisialisasi bergabung dengan konstruktor. Ini berarti implementasi MainImpl dapat menikmati jaminan yang lebih kuat yang diberikan ini, yang menyederhanakan kode utama. …
amon
79

Cara paling efektif dan bermanfaat untuk mencegah klien dari "menyalahgunakan" suatu objek adalah dengan membuatnya tidak mungkin.

Solusi paling sederhana adalah bergabung Initializedengan konstruktor. Dengan begitu, objek tidak akan pernah tersedia untuk klien dalam kondisi tidak diinisialisasi sehingga kesalahan tidak dimungkinkan. Jika Anda tidak dapat melakukan inisialisasi dalam konstruktor itu sendiri, Anda dapat membuat metode pabrik. Misalnya, jika kelas Anda mengharuskan acara tertentu untuk didaftarkan, Anda bisa meminta pendengar acara sebagai parameter dalam konstruktor atau tanda tangan metode pabrik.

Jika Anda harus dapat mengakses objek yang tidak diinisialisasi sebelum inisialisasi, maka Anda bisa memiliki dua negara diimplementasikan sebagai kelas yang terpisah, jadi Anda mulai dengan sebuah UnitializedFooManagerinstance, yang memiliki Initialize(...)metode yang mengembalikan InitializedFooManager. Metode yang hanya dapat dipanggil dalam kondisi inisialisasi hanya ada pada InitializedFooManager. Pendekatan ini dapat diperluas ke beberapa negara jika Anda perlu.

Dibandingkan dengan pengecualian runtime, ini lebih berfungsi untuk merepresentasikan status sebagai kelas yang berbeda, tetapi juga memberi Anda waktu kompilasi bahwa Anda tidak memanggil metode yang tidak valid untuk status objek, dan mendokumentasikan transisi status dengan lebih jelas dalam kode.

Tetapi lebih umum, solusi yang ideal adalah merancang kelas dan metode Anda tanpa kendala seperti mengharuskan Anda memanggil metode pada waktu tertentu dan dalam urutan tertentu. Mungkin tidak selalu memungkinkan, tetapi dalam banyak kasus dapat dihindari dengan menggunakan pola yang sesuai.

Jika Anda memiliki penggabungan temporal yang kompleks (sejumlah metode harus dipanggil dalam urutan tertentu), salah satu solusinya adalah menggunakan inversi kontrol , jadi Anda membuat kelas yang memanggil metode dalam urutan yang sesuai, tetapi menggunakan metode templat atau peristiwa untuk memungkinkan klien melakukan operasi kustom pada tahapan yang sesuai dalam alur. Dengan begitu, tanggung jawab untuk melakukan operasi dalam urutan yang tepat didorong dari klien ke kelas itu sendiri.

Contoh sederhana: Anda memiliki objek- Fileyang memungkinkan Anda membaca dari file. Namun, klien perlu menelepon Opensebelum ReadLinemetode dapat dipanggil, dan perlu diingat untuk selalu memanggil Close(bahkan jika terjadi pengecualian) setelah itu ReadLinemetode tidak boleh dipanggil lagi. Ini adalah penggabungan temporal. Itu dapat dihindari dengan memiliki satu metode yang mengambil panggilan balik atau mendelegasikan sebagai argumen. Metode mengelola membuka file, memanggil panggilan balik dan kemudian menutup file. Itu dapat melewati antarmuka yang berbeda dengan Readmetode ke callback. Dengan begitu, klien tidak mungkin lupa memanggil metode dengan urutan yang benar.

Antarmuka dengan kopling sementara:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

Tanpa penggabungan waktu:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

Solusi yang lebih berat akan didefinisikan Filesebagai kelas abstrak yang mengharuskan Anda untuk menerapkan metode (dilindungi) yang akan dieksekusi pada file terbuka. Ini disebut pola metode templat .

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

Keuntungannya sama: Klien tidak harus ingat untuk memanggil metode dalam urutan tertentu. Tidak mungkin memanggil metode dengan urutan yang salah.

JacquesB
sumber
2
Saya juga ingat kasus-kasus di mana itu tidak mungkin untuk pergi dari konstruktor, tapi saya tidak punya contoh untuk ditampilkan sekarang
Gil Sand
2
Contohnya sekarang menggambarkan pola pabrik - tetapi kalimat pertama sebenarnya menggambarkan paradigma RAII . Jadi jawaban manakah yang sebaiknya direkomendasikan?
Ext3h
11
@ Ext3h Saya tidak berpikir pola pabrik dan RAII saling eksklusif.
Pieter B
@Petereter Tidak, tidak, pabrik dapat memastikan RAII. Tetapi hanya dengan implikasi tambahan bahwa argumen templat / konstruktor (dipanggil UnitializedFooManager) tidak boleh berbagi keadaan dengan instance ( InitializedFooManager), seperti yang Initialize(...)telah benar-benar Instantiate(...)semantik. Jika tidak, contoh ini menyebabkan masalah baru jika templat yang sama digunakan dua kali oleh pengembang. Dan itu tidak mungkin untuk mencegah / memvalidasi secara statis baik jika bahasa tidak secara eksplisit mendukung movesemantik agar dapat menggunakan templat dengan andal.
Ext3h
2
@ Ext3h: Saya merekomendasikan solusi pertama karena ini adalah yang paling sederhana. Tetapi jika klien harus dapat mengakses objek sebelum memanggil Inisialisasi, maka Anda tidak dapat menggunakannya, tetapi Anda mungkin dapat menggunakan solusi kedua.
JacquesB
39

Saya biasanya hanya memeriksa untuk intiialisation dan melempar (katakanlah) IllegalStateExceptionjika Anda mencoba dan menggunakannya sementara tidak diinisialisasi.

Namun, jika Anda ingin waktu kompilasi aman (dan itu patut dipuji dan disukai), mengapa tidak memperlakukan inisialisasi sebagai metode pabrik mengembalikan objek yang dibangun dan diinisialisasi misalnya

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

sehingga Anda mengontrol pembuatan objek dan siklus hidup, dan klien Anda mendapatkan Componenthasil dari inisialisasi Anda. Ini secara efektif merupakan contoh malas.

Brian Agnew
sumber
1
Jawaban bagus - 2 pilihan bagus dalam jawaban ringkas yang bagus. Jawaban lain di atas menghadirkan senam sistem tipe yang (secara teori) sah; tetapi untuk sebagian besar kasus, 2 opsi sederhana ini adalah pilihan praktis yang ideal.
Thomas W
1
Catatan: Di .NET, InvalidOperationException dipromosikan oleh Microsoft menjadi apa yang Anda panggil IllegalStateException.
miroxlav
1
Saya tidak memilih-bawah jawaban ini, tetapi itu bukan jawaban yang baik. Dengan hanya melempar IllegalStateExceptionatau InvalidOperationExceptionkeluar secara tiba-tiba, pengguna tidak akan pernah mengerti apa yang dia lakukan salah. Pengecualian ini seharusnya tidak menjadi jalan pintas untuk memperbaiki cacat desain.
displayName
Anda akan perhatikan bahwa melempar pengecualian adalah salah satu opsi yang memungkinkan dan saya melanjutkan untuk menguraikan opsi pilihan saya - membuat waktu kompilasi ini aman. Saya telah mengedit jawaban saya untuk menekankan ini
Brian Agnew
14

Saya akan memecah sedikit dari jawaban lain dan tidak setuju: tidak mungkin untuk menjawab ini tanpa mengetahui bahasa apa yang Anda gunakan. Apakah ini rencana yang berharga, dan "peringatan" yang tepat untuk diberikan pengguna Anda sepenuhnya bergantung pada mekanisme yang disediakan bahasa Anda dan konvensi yang cenderung diikuti oleh pengembang lain dari bahasa itu.

Jika Anda memiliki FooManagerdi Haskell, itu akan kriminal untuk memungkinkan pengguna Anda untuk membangun satu yang tidak mampu mengelola Foo, karena sistem tipe membuatnya sangat mudah dan itu adalah konvensi yang diharapkan pengembang Haskell.

Di sisi lain, jika Anda menulis C, rekan kerja Anda akan sepenuhnya berhak untuk membawa Anda kembali dan menembak Anda karena mendefinisikan terpisah struct FooManagerdan struct UninitializedFooManagerjenis yang mendukung operasi yang berbeda, karena itu akan menyebabkan kode rumit yang tidak perlu untuk manfaat yang sangat sedikit .

Jika Anda menulis Python, tidak ada mekanisme dalam bahasa untuk membiarkan Anda melakukan ini.

Anda mungkin tidak menulis Haskell, Python, atau C, tetapi mereka adalah contoh ilustratif dalam hal seberapa banyak / sedikit kerja sistem tipe yang diharapkan dan dapat dilakukan untuk dilakukan.

Ikuti ekspektasi yang wajar dari seorang pengembang dalam bahasa Anda , dan tahan keinginan untuk merekayasa berlebihan suatu solusi yang tidak memiliki implementasi idiomatik yang alami (kecuali kesalahannya begitu mudah dibuat dan sangat sulit ditangkap sehingga perlu dilakukan secara ekstrem panjang untuk membuatnya mustahil). Jika Anda tidak memiliki cukup pengalaman dalam bahasa Anda untuk menilai apa yang masuk akal, ikuti saran dari seseorang yang mengetahuinya lebih baik dari Anda.

Patrick Collins
sumber
Saya sedikit bingung dengan "Jika Anda menulis Python, tidak ada mekanisme dalam bahasa untuk membiarkan Anda melakukan ini." Python memiliki konstruktor, dan cukup sepele untuk membuat metode pabrik. Jadi mungkin saya tidak mengerti apa yang Anda maksud dengan "ini"?
AL Flanagan
3
Dengan "ini," maksud saya kesalahan waktu kompilasi, yang bertentangan dengan runtime.
Patrick Collins
Hanya melompat untuk menyebutkan Python 3.5 yang merupakan versi saat ini memiliki jenis melalui sistem tipe pluggable. Jelas dimungkinkan untuk mendapatkan kesalahan Python sebelum menjalankan kode hanya karena itu diimpor. Hingga 3,5 itu sepenuhnya benar.
Benjamin Gruenbaum
Oh oke. +1 Terlambat. Bahkan bingung, ini sangat mendalam.
AL Flanagan
Aku suka gayamu, Patrick.
Gil Sand
4

Karena Anda tampaknya tidak ingin mengirimkan cek kode kepada pelanggan (tetapi tampaknya baik-baik saja untuk melakukannya bagi programmer), Anda dapat menggunakan fungsi yang menegaskan , jika mereka tersedia dalam bahasa pemrograman Anda.

Dengan cara itu Anda memiliki cek di lingkungan pengembangan (dan setiap tes yang oleh rekan dev akan AKAN gagal diprediksi), tetapi Anda tidak akan mengirimkan kode kepada pelanggan karena menegaskan (setidaknya di Jawa) hanya dikompilasi secara selektif.

Jadi, kelas Java yang menggunakan ini akan terlihat seperti ini:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

Pernyataan adalah alat yang hebat untuk memeriksa keadaan runtime dari program Anda yang Anda butuhkan, tetapi jangan berharap ada orang yang melakukan kesalahan di lingkungan nyata.

Juga mereka cukup ramping dan tidak mengambil porsi besar jika () melempar ... sintaks, mereka tidak perlu ditangkap, dll.

Angelo Fuchs
sumber
3

Melihat masalah ini lebih umum daripada jawaban saat ini, yang sebagian besar berfokus pada inisialisasi. Pertimbangkan objek yang akan memiliki dua metode, a()dan b(). Syaratnya adalah yang a()selalu disebut sebelumnya b(). Anda dapat membuat pemeriksaan waktu kompilasi bahwa ini terjadi dengan mengembalikan objek baru dari a()dan pindah b()ke objek baru daripada yang asli. Contoh implementasi:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

Sekarang, tidak mungkin untuk memanggil b () tanpa terlebih dahulu memanggil a (), karena diimplementasikan dalam antarmuka yang disembunyikan sampai a () dipanggil. Memang, ini adalah upaya yang cukup banyak, jadi saya biasanya tidak akan menggunakan pendekatan ini, tetapi ada situasi di mana itu bisa bermanfaat (terutama jika kelas Anda akan digunakan kembali dalam banyak situasi oleh programmer yang mungkin tidak akrab dengan implementasinya, atau dalam kode di mana keandalan sangat penting). Perhatikan juga bahwa ini adalah generalisasi dari pola builder, seperti yang disarankan oleh banyak jawaban yang ada. Ini bekerja dengan cara yang hampir sama, satu-satunya perbedaan nyata adalah di mana data disimpan (dalam objek asli daripada yang dikembalikan) dan ketika itu dimaksudkan untuk digunakan (kapan saja dibandingkan hanya selama inisialisasi).

Jules
sumber
2

Ketika saya mengimplementasikan kelas dasar yang membutuhkan informasi inisialisasi tambahan atau tautan ke objek lain sebelum menjadi berguna, saya cenderung membuat abstrak kelas dasar itu, lalu mendefinisikan beberapa metode abstrak di kelas itu yang digunakan dalam aliran kelas dasar (seperti abstract Collection<Information> get_extra_information(args);dan abstract Collection<OtherObjects> get_other_objects(args);yang dengan kontrak warisan harus dilaksanakan oleh kelas beton, memaksa pengguna kelas dasar itu untuk memasok semua hal yang diperlukan oleh kelas dasar.

Jadi ketika saya mengimplementasikan kelas dasar, saya segera dan secara eksplisit tahu apa yang harus saya tulis untuk membuat kelas dasar berperilaku dengan benar, karena saya hanya perlu mengimplementasikan metode abstrak dan hanya itu.

EDIT: untuk memperjelas, ini hampir sama dengan memberikan argumen kepada konstruktor dari kelas dasar, tetapi implementasi metode abstrak memungkinkan implementasi untuk memproses argumen yang diteruskan ke panggilan metode abstrak. Atau bahkan dalam kasus tidak ada argumen yang digunakan, masih bisa berguna jika nilai kembali dari metode abstrak tergantung pada keadaan yang bisa Anda definisikan dalam tubuh metode, yang tidak mungkin ketika meneruskan variabel sebagai argumen untuk konstruktor. Memang Anda masih dapat memberikan argumen yang akan memiliki perilaku dinamis berdasarkan pada prinsip yang sama jika Anda lebih suka menggunakan komposisi daripada warisan.

klaar
sumber
2

Jawabannya adalah: Ya, tidak, dan kadang-kadang. :-)

Beberapa masalah yang Anda gambarkan mudah diselesaikan, setidaknya dalam banyak kasus.

Pengecekan waktu kompilasi tentu lebih baik daripada pengecekan waktu berjalan, yang lebih baik daripada tidak ada pengecekan sama sekali.

RE run-time:

Setidaknya secara konsep sederhana untuk memaksa fungsi dipanggil dalam urutan tertentu, dll, dengan pemeriksaan run-time. Masukkan saja flag yang mengatakan fungsi mana yang telah dijalankan, dan biarkan setiap fungsi mulai dengan sesuatu seperti "jika tidak pra-kondisi-1 atau tidak pra-kondisi-2 lalu buang pengecualian". Jika pemeriksaan menjadi rumit, Anda bisa mendorongnya ke fungsi pribadi.

Anda dapat membuat beberapa hal terjadi secara otomatis. Untuk mengambil contoh sederhana, pemrogram sering berbicara tentang "populasi malas" dari suatu objek. Ketika instance dibuat, Anda mengatur flag is-populated ke false, atau beberapa objek referensi kunci ke nol. Kemudian ketika suatu fungsi dipanggil yang membutuhkan data yang relevan, ia memeriksa flag. Jika itu salah, ia mengisi data dan menyetel bendera menjadi benar. Jika itu benar itu hanya berjalan dengan asumsi data ada di sana. Anda dapat melakukan hal yang sama dengan prasyarat lainnya. Tetapkan bendera di konstruktor atau sebagai nilai default. Kemudian ketika Anda mencapai titik di mana beberapa fungsi pra-kondisi seharusnya dipanggil, jika belum dipanggil, sebut saja. Tentu saja ini hanya berfungsi jika Anda memiliki data untuk memanggilnya pada saat itu, tetapi dalam banyak kasus Anda dapat memasukkan data yang diperlukan dalam parameter fungsi "

Waktu kompilasi RE:

Seperti yang orang lain katakan, jika Anda perlu menginisialisasi sebelum objek dapat digunakan, itu menempatkan inisialisasi dalam konstruktor. Ini adalah saran yang sangat khas untuk OOP. Jika memungkinkan, Anda harus membuat konstruktor membuat instance objek yang valid dan dapat digunakan.

Saya melihat beberapa diskusi tentang Apa yang harus dilakukan jika Anda perlu memberikan referensi ke objek sebelum diinisialisasi. Saya tidak bisa memikirkan contoh nyata dari itu di atas kepala saya, tetapi saya kira itu bisa terjadi. Solusi telah disarankan, tetapi solusi yang saya lihat di sini dan yang bisa saya pikirkan adalah berantakan dan menyulitkan kode. Pada titik tertentu Anda harus bertanya, Apakah layak membuat kode yang jelek dan sulit dipahami sehingga kita bisa melakukan pengecekan waktu kompilasi daripada pengecekan run-time? Atau apakah saya menciptakan banyak pekerjaan untuk diri saya sendiri dan orang lain untuk tujuan yang baik tetapi tidak diperlukan?

Jika dua fungsi harus selalu dijalankan bersama, solusi sederhana adalah menjadikannya satu fungsi. Seperti jika setiap kali Anda menambahkan iklan ke halaman, Anda juga harus menambahkan penangan metrik untuknya, lalu masukkan kode untuk menambahkan penangan metrik di dalam fungsi "tambah iklan".

Beberapa hal akan hampir mustahil untuk diperiksa pada waktu kompilasi. Seperti persyaratan bahwa fungsi tertentu tidak dapat dipanggil lebih dari sekali. (Saya telah menulis banyak fungsi seperti itu. Saya baru-baru ini menulis sebuah fungsi yang mencari file dalam direktori ajaib, memproses apa pun yang ditemukannya, dan kemudian menghapusnya. Tentu saja setelah dihapus file itu tidak mungkin dijalankan kembali. Dll) Saya tidak tahu bahasa apa pun yang memiliki fitur yang memungkinkan Anda mencegah fungsi dipanggil dua kali pada contoh yang sama pada waktu kompilasi. Saya tidak tahu bagaimana kompiler dapat secara pasti mengetahui bahwa itu terjadi. Yang bisa Anda lakukan adalah menjalankan cek waktu berjalan. Pemeriksaan waktu berjalan yang khusus itu jelas, bukan? "jika sudah-ada-di sini = true lalu lontarkan pengecualian yang sudah disetel sudah-di-sini = benar".

RE tidak mungkin dilaksanakan

Tidak jarang sebuah kelas membutuhkan semacam pembersihan ketika Anda selesai: menutup file atau koneksi, membebaskan memori, menulis hasil akhir ke DB, dll. Tidak ada cara mudah untuk memaksa hal itu terjadi pada waktu kompilasi atau menjalankan waktu. Saya pikir sebagian besar bahasa OOP memiliki semacam ketentuan untuk fungsi "finalizer" yang dipanggil ketika sebuah instance dikumpulkan sampah, tetapi saya pikir sebagian besar juga mengatakan bahwa mereka tidak dapat menjamin ini akan dijalankan. Bahasa OOP sering menyertakan fungsi "buang" dengan semacam ketentuan untuk menetapkan ruang lingkup penggunaan instance, klausa "menggunakan" atau "dengan", dan ketika program keluar dari ruang lingkup itu, pembuangan tersebut dijalankan. Tetapi ini membutuhkan programmer untuk menggunakan specifier lingkup yang sesuai. Itu tidak memaksa programmer untuk melakukannya dengan benar,

Dalam kasus di mana tidak mungkin untuk memaksa programmer untuk menggunakan kelas dengan benar, saya mencoba membuatnya sehingga program meledak jika dia melakukannya dengan salah, daripada memberikan hasil yang salah. Contoh sederhana: Saya telah melihat banyak program yang menginisialisasi sekelompok data ke nilai dummy, sehingga pengguna tidak akan pernah mendapatkan pengecualian null-pointer bahkan jika ia gagal memanggil fungsi untuk mengisi data dengan benar. Dan saya selalu bertanya-tanya mengapa. Anda akan berusaha membuat bug lebih sulit ditemukan. Jika, ketika seorang programmer gagal memanggil fungsi "memuat data" dan kemudian mencoba menggunakan data, ia mendapat pengecualian null pointer, pengecualian akan dengan cepat menunjukkan kepadanya di mana masalahnya. Tetapi jika Anda menyembunyikannya dengan membuat data kosong atau nol dalam kasus seperti itu, program dapat berjalan sampai selesai tetapi menghasilkan hasil yang tidak akurat. Dalam beberapa kasus, programmer bahkan mungkin tidak menyadari ada masalah. Jika dia memperhatikan, dia mungkin harus mengikuti jejak panjang untuk menemukan di mana kesalahannya. Lebih baik gagal lebih awal daripada mencoba melanjutkan dengan berani.

Secara umum, tentu saja, itu selalu baik ketika Anda dapat membuat penggunaan objek yang salah menjadi tidak mungkin. Ada baiknya jika fungsi disusun sedemikian rupa sehingga tidak ada cara bagi programmer untuk membuat panggilan tidak valid, atau jika panggilan tidak valid menghasilkan kesalahan waktu kompilasi.

Tetapi ada juga beberapa poin di mana Anda harus mengatakan, Programmer harus membaca dokumentasi pada fungsi.

Jay
sumber
1
Mengenai memaksa pembersihan, ada pola yang dapat digunakan di mana sumber daya hanya dapat dialokasikan dengan memanggil metode tertentu (misalnya dalam bahasa OO khas itu bisa memiliki konstruktor pribadi). Metode ini mengalokasikan sumber daya, memanggil fungsi (atau metode antarmuka) yang dilewatkan ke sana dengan referensi ke sumber daya, dan kemudian menghancurkan sumber daya setelah fungsi ini kembali. Selain penghentian utas yang tiba-tiba, pola ini memastikan sumber daya selalu dihancurkan. Ini adalah pendekatan umum dalam bahasa fungsional, tetapi saya juga telah melihatnya digunakan dalam sistem OO untuk efek yang baik.
Jules
@Jules aka "disposers"
Benjamin Gruenbaum
2

Ya, insting Anda tepat. Bertahun-tahun yang lalu saya menulis artikel di majalah (seperti tempat ini, tetapi di pohon mati dan memadamkan sebulan sekali) membahas Debugging , Abugging , dan Antibugging .

Jika Anda bisa mendapatkan kompiler untuk menangkap penyalahgunaan, itu adalah cara terbaik. Fitur bahasa apa yang tersedia tergantung pada bahasa, dan Anda tidak menentukan. Teknik spesifik akan dirinci untuk setiap pertanyaan di situs ini.

Tetapi memiliki tes untuk mendeteksi penggunaan pada waktu kompilasi alih-alih run-time adalah jenis tes yang sama: Anda masih perlu tahu untuk memanggil fungsi yang tepat terlebih dahulu dan menggunakannya dengan cara yang benar.

Merancang komponen untuk menghindari bahwa bahkan menjadi masalah di tempat pertama jauh lebih halus, dan saya suka Buffer adalah seperti contoh Elemen . Bagian 4 (diterbitkan dua puluh tahun yang lalu! Wow!) Berisi contoh dengan diskusi yang sangat mirip dengan kasus Anda: Kelas ini harus digunakan dengan hati-hati, karena buffer () mungkin tidak dipanggil setelah Anda mulai menggunakan read (). Sebaliknya, itu hanya harus digunakan sekali, segera setelah konstruksi. Memiliki kelas mengelola buffer secara otomatis dan tidak terlihat ke pemanggil akan menghindari masalah secara konseptual.

Anda bertanya, jika tes dapat dilakukan pada saat run-time untuk memastikan penggunaan yang tepat, haruskah Anda melakukannya?

Saya akan mengatakan ya . Ini akan menghemat banyak pekerjaan debug pada tim Anda pada suatu hari, ketika kode dipertahankan dan penggunaan spesifik terganggu. Pengujian dapat diatur sebagai set formal kendala dan invarian yang dapat diuji. Itu tidak berarti overhead ruang ekstra untuk melacak negara dan bekerja untuk melakukan pengecekan selalu dibiarkan untuk setiap panggilan. Ada di kelas sehingga bisa dipanggil, dan kode mendokumentasikan kendala dan asumsi aktual.

Mungkin pemeriksaan hanya dilakukan di build debug.

Mungkin pemeriksaannya rumit (pikirkan heapcheck, misalnya), tetapi ada dan dapat dilakukan sesekali, atau panggilan ditambahkan di sana-sini ketika kode sedang dikerjakan dan beberapa masalah muncul, atau untuk melakukan pengujian khusus untuk pastikan tidak ada masalah seperti itu dalam program pengujian unit atau pengujian integrasi yang terhubung ke kelas yang sama.

Anda mungkin memutuskan bahwa biaya overhead itu sepele. Jika kelas melakukan file I / O, dan byte status ekstra dan tes untuk itu tidak ada artinya. Kelas iostream umum memeriksa aliran berada dalam kondisi buruk.

Nalurimu bagus. Teruskan!

JDługosz
sumber
0

Prinsip Penyembunyian Informasi (Enkapsulasi) adalah bahwa entitas di luar kelas tidak boleh tahu lebih dari apa yang diperlukan untuk menggunakan kelas ini dengan benar.

Kasing Anda sepertinya Anda berusaha agar objek kelas berjalan dengan benar tanpa memberi tahu pengguna luar cukup banyak tentang kelas tersebut. Anda telah menyembunyikan lebih banyak informasi daripada yang seharusnya Anda miliki.

  • Entah secara eksplisit meminta informasi yang diperlukan dalam konstruktor;
  • Atau menjaga konstruktor tetap utuh dan memodifikasi metode sehingga untuk memanggil metode, Anda harus memberikan data yang hilang.

Kata-kata bijak:

* Bagaimanapun Anda harus mendesain ulang kelas dan / atau konstruktor dan / atau metode.

* Dengan tidak merancang dengan benar dan membuat kelas kelaparan dari informasi yang benar, Anda berisiko mengambil kelas Anda di satu tempat tetapi berpotensi banyak tempat.

* Jika Anda menulis pengecualian dan pesan kesalahan dengan buruk, kelas Anda akan memberikan lebih banyak lagi tentang dirinya sendiri.

* Akhirnya, jika orang luar seharusnya tahu bahwa ia harus menginisialisasi a, b dan c dan kemudian memanggil d (), e (), f (int) maka Anda memiliki kebocoran dalam abstraksi Anda.

nama tampilan
sumber
0

Karena Anda telah menentukan bahwa Anda menggunakan C # solusi yang sesuai untuk situasi Anda adalah dengan memanfaatkan Roslyn Code Analyzers . Ini akan memungkinkan Anda untuk segera menangkap pelanggaran dan bahkan memungkinkan Anda untuk menyarankan perbaikan kode .

Salah satu cara untuk mengimplementasikannya adalah dengan menghias metode yang digabungkan sementara dengan atribut yang menentukan urutan metode yang perlu disebut 1 . Ketika penganalisa menemukan atribut ini di kelas itu memvalidasi bahwa metode dipanggil secara berurutan. Ini akan membuat kelas Anda terlihat seperti berikut:

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: Saya tidak pernah menulis Roslyn Code Analyzer jadi ini mungkin bukan implementasi terbaik. Gagasan untuk menggunakan Roslyn Code Analyzer untuk memverifikasi API Anda digunakan dengan benar adalah 100% sehat.

Erik
sumber
-1

Cara saya memecahkan masalah ini sebelumnya adalah dengan konstruktor pribadi dan MakeFooManager()metode statis di dalam kelas. Contoh:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

Karena konstruktor diimplementasikan, tidak ada cara bagi siapa pun untuk membuat instance FooManagertanpa melalui MakeFooManager().

WillB3
sumber
1
ini tampaknya hanya mengulangi poin yang dibuat dan dijelaskan dalam jawaban sebelumnya yang telah diposting beberapa jam sebelumnya
nyamuk
1
apakah ini memiliki keuntungan sama sekali hanya dari nol-memeriksa di ctor? sepertinya Anda baru saja membuat metode yang tidak menggunakan metode lain. Mengapa?
Sara
Juga, mengapa variabel anggota foo dan bar?
Patrick M
-1

Ada beberapa pendekatan:

  1. Jadikan kelas mudah digunakan dengan benar dan sulit digunakan salah. Beberapa jawaban lain fokus secara eksklusif pada titik ini, jadi saya hanya akan menyebutkan RAII dan pola pembangun .

  2. Berhati-hatilah dengan cara menggunakan komentar. Jika kelas itu menjelaskan sendiri, jangan menulis komentar. * Dengan cara itu orang lebih cenderung membaca komentar Anda ketika kelas benar-benar membutuhkannya. Dan jika Anda memiliki salah satu kasus langka di mana kelas sulit digunakan dengan benar dan perlu komentar, sertakan contoh.

  3. Gunakan menegaskan di build debug Anda.

  4. Lempar pengecualian jika penggunaan kelas tidak valid.

* Ini adalah jenis komentar yang tidak boleh Anda tulis:

// gets Foo
GetFoo();
Peter
sumber