Desain antarmuka tempat fungsi perlu dipanggil dalam urutan tertentu

24

Tugasnya adalah mengkonfigurasi perangkat keras di dalam perangkat, sesuai dengan beberapa spesifikasi input. Ini harus dicapai sebagai berikut:

1) Kumpulkan informasi konfigurasi. Ini dapat terjadi di waktu dan tempat yang berbeda. Sebagai contoh, modul A dan modul B dapat meminta (pada waktu yang berbeda) beberapa sumber daya dari modul saya. 'Sumber daya' itu sebenarnya adalah konfigurasi.

2) Setelah jelas bahwa tidak ada lagi permintaan yang akan direalisasikan, perintah startup, memberikan ringkasan sumber daya yang diminta, perlu dikirim ke perangkat keras.

3) Hanya setelah itu, dapat (dan harus) konfigurasi terperinci dari sumber daya tersebut dilakukan.

4) Juga, hanya setelah 2), dapat (dan harus) melakukan routing sumber daya yang dipilih ke pemanggil yang dinyatakan dapat dilakukan.


Penyebab umum untuk bug, bahkan bagi saya, yang menulis hal itu, salah mengira urutan ini. Konvensi, desain, atau mekanisme penamaan apa yang dapat saya terapkan untuk membuat antarmuka dapat digunakan oleh seseorang yang melihat kode untuk pertama kalinya?

Vorac
sumber
Tahap 1 lebih baik dipanggil discoveryatau handshake?
rwong
1
Kopling temporal adalah anti-pola dan harus dihindari.
1
Judul pertanyaan membuat saya berpikir Anda mungkin tertarik dengan pola pembangun langkah .
Joshua Taylor

Jawaban:

45

Ini adalah desain ulang tetapi Anda dapat mencegah penyalahgunaan banyak API tetapi tidak memiliki metode apa pun yang tidak boleh dipanggil.

Misalnya, bukannya first you init, then you start, then you stop

Konstruktor Anda initadalah objek yang dapat dimulai dan startmembuat sesi yang bisa dihentikan.

Tentu saja jika Anda memiliki batasan untuk satu sesi pada satu waktu Anda harus menangani kasus di mana seseorang mencoba untuk membuat satu dengan yang sudah aktif.

Sekarang terapkan teknik itu pada kasus Anda sendiri.

Uang tunai
sumber
zlibdan jpeglibadalah dua contoh yang mengikuti pola ini untuk inisialisasi. Namun, banyak dokumentasi yang diperlukan untuk mengajarkan konsep ini kepada pengembang.
rwong
5
Ini adalah jawaban yang tepat: jika pesanan penting, setiap fungsi mengembalikan hasil yang kemudian dapat dipanggil untuk melakukan langkah berikutnya. Compiler itu sendiri mampu menegakkan batasan desain.
2
Ini mirip dengan pola pembangun langkah ; hanya menyajikan antarmuka yang masuk akal pada fase tertentu.
Joshua Taylor
@ YosuaTaylor jawaban saya adalah implementasi pola langkah pembangun :)
Silviu Burcea
@ SilviuBurcea Jawaban Anda bukan implementasi pembangun langkah, tapi saya akan mengomentarinya daripada di sini.
Joshua Taylor
19

Anda dapat meminta metode startup mengembalikan objek yang merupakan parameter yang diperlukan ke konfigurasi:

Sumber Daya * MyModule :: GetResource ();
MySession * MyModule :: Startup ();
void Resource :: Configure (sesi MySession *);

Bahkan jika Anda MySessionhanya sebuah struct kosong, ini akan menegakkan melalui keamanan jenis yang tidak ada Configure()metode yang dapat dipanggil sebelum startup.

jpa
sumber
Apa yang menghentikan seseorang dari melakukan module->GetResource()->Configure(nullptr)?
svick
@vick: Tidak ada, tetapi Anda harus secara eksplisit melakukan ini. Pendekatan ini memberi tahu Anda apa yang diharapkan dan dilewati bahwa ekspektasi adalah keputusan sadar. Seperti kebanyakan bahasa pemrograman, tidak ada yang mencegah Anda menembak diri sendiri. Tapi itu selalu baik oleh API untuk secara jelas menunjukkan bahwa Anda melakukannya;)
Michael Klement
+1 terlihat hebat dan sederhana. Namun, saya bisa melihat masalah. Jika saya memiliki objek a, b, c, d, maka saya bisa mulai a, dan menggunakannya MySessionuntuk mencoba menggunakan bsebagai objek yang sudah mulai, sedangkan pada kenyataannya tidak.
Vorac
8

Membangun Jawaban dari Cashcow - mengapa Anda harus menyajikan Obyek baru kepada pemanggil, ketika Anda bisa menyajikan Antarmuka baru? Rubah-Pola:

class IStartable     { public: virtual IRunnable      start()     = 0; };
class IRunnable      { public: virtual ITerminateable run()       = 0; };
class ITerminateable { public: virtual void           terminate() = 0; };

Anda juga dapat membiarkan ITerminateable menerapkan IRunnable, jika sesi dapat dijalankan beberapa kali.

Objek Anda:

class Service : IStartable, IRunnable, ITerminateable
{
  public:
    IRunnable      start()     { ...; return this; }
    ITerminateable run()       { ...; return this; }
    void           terminate() { ...; }
}

// And use it like this:
IStartable myService = Service();

// Now you can only call start() via the interface
IRunnable configuredService = myService.start();

// Now you can also call run(), because it is wrapped in the new interface...

Dengan cara ini Anda hanya dapat memanggil metode yang tepat, karena Anda hanya memiliki IStartable-Interface di awal dan akan menjalankan () Metode hanya dapat diakses ketika Anda telah memanggil start (); Dari luar terlihat seperti pola dengan beberapa kelas dan Objek, tetapi kelas yang mendasarinya tetap satu kelas, yang selalu direferensikan.

Falco
sumber
1
Apa keuntungan memiliki hanya satu kelas yang mendasari daripada beberapa? Karena ini adalah satu-satunya perbedaan dengan solusi yang saya usulkan, saya akan tertarik pada poin khusus ini.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald Ini tidak perlu untuk mengimplementasikan semua antarmuka dengan satu kelas, tetapi untuk objek tipe konfigurasi, itu mungkin teknik implementasi paling sederhana untuk berbagi data antara instance dari antarmuka (yaitu, karena itu dibagi berdasarkan menjadi sama obyek).
Joshua Taylor
1
Ini pada dasarnya adalah pola pembangun langkah .
Joshua Taylor
@JoshuaTaylor Berbagi data di antara instance antarmuka ada dua: sementara mungkin lebih mudah untuk diterapkan, kita harus berhati-hati untuk tidak mengakses "keadaan tidak terdefinisi" (seperti mengakses alamat klien dari server yang tidak terhubung). Karena OP menekankan pada kegunaan antarmuka, kita bisa menilai kedua pendekatan itu sama. Terima kasih telah mengutip "pola pembangun langkah" BTW.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald Jika Anda hanya berinteraksi dengan objek melalui antarmuka tertentu yang ditentukan pada titik tertentu, seharusnya tidak ada cara (tanpa casting, dll.) Untuk mengakses keadaan itu.
Joshua Taylor
2

Ada banyak pendekatan yang valid untuk menyelesaikan masalah Anda. Basile Starynkevitch mengusulkan pendekatan "nol-birokrasi" yang membuat Anda memiliki antarmuka yang sederhana dan mengandalkan programmer yang menggunakan antarmuka dengan tepat. Sementara saya suka pendekatan ini, saya akan menyajikan satu lagi yang memiliki lebih banyak eingineering tetapi memungkinkan kompiler untuk menangkap beberapa kesalahan.

  1. Mengidentifikasi berbagai negara perangkat Anda dapat di, sebagai Uninitialised, Started, Configureddan sebagainya. Daftarnya harus terbatas.¹

  2. Untuk setiap negara, tentukan structholding informasi tambahan yang diperlukan yang relevan dengan negara itu, misalnya DeviceUninitialised, DeviceStarteddan sebagainya.

  3. Kemas semua perawatan dalam satu objek di DeviceStrategymana metode menggunakan struktur yang ditentukan dalam 2. sebagai input dan output. Dengan demikian, Anda mungkin memiliki DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)metode (atau apa pun yang setara mungkin sesuai dengan konvensi proyek Anda).

Dengan pendekatan ini, program yang valid harus memanggil beberapa metode dalam urutan yang diberlakukan oleh prototipe metode.

Berbagai negara adalah objek yang tidak terkait, ini karena prinsip substitusi. Jika berguna bagi Anda untuk memiliki struktur ini memiliki leluhur yang sama, ingatlah bahwa pola pengunjung dapat digunakan untuk memulihkan tipe konkret dari instance kelas abstrak.

Sementara saya jelaskan dalam 3. DeviceStrategykelas yang unik , ada situasi di mana Anda mungkin ingin membagi fungsionalitas yang disediakannya di beberapa kelas.

Untuk meringkasnya, poin-poin utama dari desain yang saya jelaskan adalah:

  1. Karena prinsip substitusi, objek yang mewakili status perangkat harus berbeda dan tidak memiliki hubungan pewarisan khusus.

  2. Kemas perawatan perangkat di objek pemula daripada objek yang mewakili perangkat itu sendiri, sehingga setiap perangkat atau negara perangkat hanya melihat dirinya sendiri, dan strategi melihat semuanya dan mengekspresikan kemungkinan transisi di antara mereka.

Saya bersumpah bahwa saya pernah melihat deskripsi implementasi klien telnet mengikuti baris-baris ini, tetapi saya tidak dapat menemukannya lagi. Itu akan menjadi referensi yang sangat berguna!

¹: Untuk ini, ikuti intuisi Anda atau temukan kelas metode ekuivalen dalam implementasi aktual Anda untuk relasi “metode₁ ~ metode₂ iff. valid untuk menggunakannya pada objek yang sama ”- dengan asumsi Anda memiliki objek besar yang merangkum semua perawatan pada perangkat Anda. Kedua metode daftar negara memberikan hasil yang fantastis.

Michael Le Barbier Grünewald
sumber
1
Daripada mendefinisikan struct terpisah, mungkin cukup untuk mendefinisikan antarmuka yang diperlukan bahwa suatu objek pada setiap fase harus hadir. Maka itu adalah pola pembangun langkah .
Joshua Taylor
2

Gunakan pola pembangun.

Memiliki objek yang memiliki metode untuk semua operasi yang Anda sebutkan di atas. Namun, itu tidak melakukan operasi ini segera. Itu hanya mengingat setiap operasi untuk nanti. Karena operasi tidak dijalankan segera, urutan di mana Anda meneruskannya ke pembangun tidak masalah.

Setelah Anda mendefinisikan semua operasi pada pembangun, Anda memanggil execute-metode. Ketika metode ini dipanggil, ia melakukan semua langkah yang Anda sebutkan di atas dalam urutan yang benar dengan operasi yang Anda simpan di atas. Metode ini juga merupakan tempat yang baik untuk melakukan beberapa pemeriksaan kewarasan rentang operasi (seperti mencoba mengkonfigurasi sumber daya yang belum diatur) sebelum menulisnya ke perangkat keras. Ini dapat menyelamatkan Anda dari kerusakan perangkat keras dengan konfigurasi yang tidak masuk akal (jika perangkat keras Anda rentan terhadap hal ini).

Philipp
sumber
1

Anda hanya perlu mendokumentasikan dengan benar bagaimana antarmuka digunakan, dan memberikan contoh tutorial.

Anda juga mungkin memiliki varian pustaka debugging yang melakukan beberapa pemeriksaan runtime.

Mungkin mendefinisikan dan mendokumentasikan dengan benar beberapa konvensi penamaan (misalnya preconfigure*, startup*, postconfigure*, run*....)

BTW, banyak antarmuka yang ada mengikuti pola yang sama (misalnya toolkit X11).

Basile Starynkevitch
sumber
Diagram transisi keadaan, mirip dengan siklus hidup aktivitas aplikasi Android , mungkin diperlukan untuk menyampaikan informasi.
rwong
1

Ini memang jenis kesalahan yang umum dan berbahaya, karena kompiler hanya dapat menegakkan kondisi sintaks, sementara Anda membutuhkan program klien Anda untuk menjadi "secara tata bahasa" yang benar.

Sayangnya, konvensi penamaan hampir seluruhnya tidak efektif terhadap kesalahan semacam ini. Jika Anda benar-benar ingin mendorong orang untuk tidak melakukan hal-hal yang tidak sesuai tata ruang, Anda harus membagikan objek perintah yang harus diinisialisasi dengan nilai-nilai untuk prasyarat, sehingga mereka tidak dapat melakukan langkah-langkah yang tidak sesuai dengan aturan.

Kilian Foth
sumber
Maksud Anda sesuatu seperti ini ?
Vorac
1
public class Executor {

private Executor() {} // helper class

  public void execute(MyStepsRunnable r) {
    r.step1();
    r.step2();
    r.step3();
  }
}

interface MyStepsRunnable {

  void step1();
  void step2();
  void step3();
}

Dengan menggunakan pola ini, Anda yakin bahwa setiap implementor akan mengeksekusi dalam urutan yang tepat ini. Anda dapat melangkah lebih jauh dan membuat ExecutorFactory yang akan membangun Executor dengan jalur eksekusi kustom.

Silviu Burcea
sumber
Dalam komentar lain Anda menyebut ini sebagai implementasi pembangun langkah, tetapi tidak. Jika Anda memiliki instance dari MyStepsRunnable, maka Anda bisa memanggil step3 sebelum step1. Implementasi step builder akan lebih sesuai dengan ideone.com/UDECgY . Idenya adalah hanya mendapatkan sesuatu dengan step2 dengan menjalankan step1. Dengan demikian Anda terpaksa memanggil metode dalam urutan yang benar. Misalnya, lihat stackoverflow.com/q/17256627/1281433 .
Joshua Taylor
Anda dapat mengonversinya ke kelas abstrak dengan metode yang dilindungi (atau bahkan default) untuk membatasi cara penggunaannya. Anda akan dipaksa untuk menggunakan pelaksana, tetapi saya memiliki kemungkinan ada satu atau dua cacat dengan implementasi saat ini.
Silviu Burcea
Itu masih tidak membuatnya menjadi pembangun langkah. Dalam kode Anda, tidak ada yang dapat dilakukan pengguna untuk menjalankan kode di antara langkah-langkah yang berbeda. Idenya bukan hanya untuk mengurutkan kode (terlepas apakah itu publik atau pribadi, atau dienkapsulasi). Seperti yang ditunjukkan kode Anda, itu cukup mudah dilakukan dengan sederhana step1(); step2(); step3();. Titik pembangun langkah adalah untuk memberikan API yang memaparkan beberapa langkah, dan untuk menegakkan urutan di mana mereka dipanggil. Seharusnya tidak mencegah programmer melakukan hal-hal lain di antara langkah-langkah.
Joshua Taylor