Bagaimana seharusnya sebuah kelas berkomunikasi dengan penggunanya yang merupakan bagian dari metode yang diterapkannya?


12

Skenario

Aplikasi web mendefinisikan antarmuka backend pengguna IUserBackenddengan metode

  • getUser (uid)
  • createUser (uid)
  • deleteUser (uid)
  • setPassword (uid, kata sandi)
  • ...

Backend pengguna yang berbeda (misalnya LDAP, SQL, ...) mengimplementasikan antarmuka ini tetapi tidak setiap backend dapat melakukan semuanya. Misalnya server LDAP konkret tidak memungkinkan aplikasi web ini menghapus pengguna. Jadi LdapUserBackendkelas yang mengimplementasikan IUserBackendtidak akan mengimplementasikan deleteUser(uid).

Kelas konkret perlu berkomunikasi dengan aplikasi web apa aplikasi web diizinkan untuk dilakukan dengan pengguna backend.

Solusi yang dikenal

Saya telah melihat solusi di mana IUserInterfacememiliki implementedActionsmetode yang mengembalikan integer yang merupakan hasil dari bitwise OR dari tindakan bitwise ANDed dengan tindakan yang diminta:

function implementedActions(requestedActions) {
    return (bool)(
        ACTION_GET_USER
        | ACTION_CREATE_USER
        | ACTION_DELTE_USER
        | ACTION_SET_PASSWORD
        ) & requestedActions)
}

Dimana

  • ACTION_GET_USER = 1
  • ACTION_CREATE_USER = 2
  • ACTION_DELETE_USER = 4
  • ACTION_SET_PASSWORD = 8
  • .... = 16
  • .... = 32

dll.

Jadi aplikasi web menetapkan bitmask dengan apa yang dibutuhkan dan implementedActions()menjawab dengan boolean apakah itu mendukungnya.

Pendapat

Operasi bit ini bagi saya terlihat seperti peninggalan dari zaman C, tidak selalu mudah dipahami dalam hal kode bersih.

Pertanyaan

Apa pola modern (lebih baik?) Untuk kelas untuk mengkomunikasikan subset dari metode antarmuka yang diterapkannya? Atau "metode operasi bit" dari atas masih merupakan praktik terbaik?

( Dalam hal ini penting: PHP, meskipun saya mencari solusi umum untuk bahasa OO )


5
Solusi umum adalah dengan membagi antarmuka. The IUserBackendtidak harus berisi deleteUsermetode sama sekali. Itu harus menjadi bagian dari IUserDeleteBackend(atau apa pun yang Anda ingin menyebutnya). Kode yang perlu dihapus pengguna akan memiliki argumen IUserDeleteBackend, kode yang tidak memerlukan fungsionalitas yang akan digunakan IUserBackenddan tidak akan memiliki masalah dengan metode yang tidak diterapkan.
Bakuriu

3
Pertimbangan desain yang penting adalah apakah ketersediaan suatu tindakan tergantung pada keadaan runtime. Apakah ini semua server LDAP yang tidak mendukung penghapusan? Atau apakah itu properti dari konfigurasi server, dan mungkin berubah dengan sistem restart? Haruskah konektor LDAP Anda secara otomatis menemukan situasi ini, atau haruskah konfigurasi diubah untuk menyambungkan konektor LDAP yang berbeda dengan kemampuan yang berbeda? Hal-hal ini memiliki dampak kuat pada solusi mana yang layak.
Sebastian Redl

@ SebastianRedl Ya, itu adalah sesuatu yang tidak saya perhitungkan. Saya sebenarnya membutuhkan solusi untuk runtime. Karena saya tidak ingin membatalkan jawaban yang sangat bagus, saya membuka pertanyaan baru yang berfokus pada runtime
problemofficer

Jawaban:


24

Secara umum, ada dua pendekatan yang dapat Anda ambil di sini: tes & lemparan atau komposisi melalui polimorfisme.

Uji & lempar

Ini adalah pendekatan yang sudah Anda jelaskan. Melalui beberapa cara, Anda menunjukkan kepada pengguna kelas apakah metode lain tertentu diterapkan atau tidak. Ini dapat dilakukan dengan metode tunggal dan enumerasi bitwise (seperti yang Anda jelaskan), atau melalui serangkaian supportsDelete()metode dll.

Kemudian, jika supportsDelete()kembali false, panggilan deleteUser()dapat mengakibatkan NotImplementedExeptionpelemparan, atau metode tidak melakukan apa-apa.

Ini adalah solusi yang populer di antara beberapa, karena sederhana. Namun, banyak - termasuk saya sendiri - berpendapat bahwa ini merupakan pelanggaran prinsip substitusi Liskov (L dalam SOLID) dan karenanya bukan solusi yang baik.

Komposisi melalui Polimorfisme

Pendekatan di sini adalah melihat IUserBackendinstrumen yang terlalu tumpul. Jika kelas tidak selalu dapat menerapkan semua metode di antarmuka itu, maka pisahkan antarmuka menjadi bagian-bagian yang lebih fokus. Jadi, Anda mungkin memiliki: IGeneralUser IDeletableUser IRenamableUser ... Dengan kata lain, semua metode yang dapat diimplementasikan oleh semua backend Anda masuk IGeneralUserdan Anda membuat antarmuka terpisah untuk setiap tindakan yang hanya dapat dilakukan beberapa orang.

Dengan begitu, LdapUserBackendtidak diterapkan IDeletableUserdan Anda menguji untuk itu menggunakan tes seperti (menggunakan sintaks C #):

if (backend is IDeletableUser deletableUser)
{
    deletableUser.deleteUser(id);
}

(Saya tidak yakin dengan mekanisme dalam PHP untuk menentukan apakah sebuah instance mengimplementasikan antarmuka dan bagaimana Anda kemudian melemparkan ke antarmuka itu, tapi saya yakin ada padanan dalam bahasa itu)

Keuntungan dari metode ini adalah penggunaan polimorfisme yang baik untuk memungkinkan kode Anda untuk mematuhi prinsip-prinsip SOLID dan hanya jauh lebih elegan dalam pandangan saya.

The downside adalah bahwa hal itu dapat menjadi sangat sulit dengan mudah. Jika, misalnya, Anda akhirnya harus mengimplementasikan puluhan antarmuka karena setiap backend beton memiliki kemampuan yang sedikit berbeda, maka ini bukan solusi yang baik. Jadi saya hanya akan menyarankan Anda menggunakan penilaian Anda pada apakah pendekatan ini praktis untuk Anda pada kesempatan ini dan menggunakannya, jika ya.


4
+1 untuk pertimbangan desain SOLID. Selalu senang menunjukkan jawaban dengan pendekatan berbeda yang akan membuat pembersih kode terus bergerak maju!
Caleb

2
dalam PHP itu akanif (backend instanceof IDelatableUser) {...}
Rad80

Anda sudah menyebutkan pelanggaran LSP. Saya setuju, tetapi ingin sedikit menambahkannya: Test & Throw valid jika nilai input membuatnya tidak mungkin untuk melakukan tindakan, mis. Melewatkan 0 sebagai pembagi dalam suatu Divide(float,float)metode. Nilai input bervariasi, dan pengecualian mencakup sebagian kecil kemungkinan eksekusi. Tetapi jika Anda melempar berdasarkan jenis implementasi Anda, maka ketidakmampuannya untuk mengeksekusi adalah fakta yang diberikan. Pengecualian mencakup semua input yang mungkin , bukan hanya sebagian saja. Itu seperti menempatkan tanda "lantai basah" di setiap lantai basah di dunia di mana setiap lantai selalu basah.
Flater

Ada pengecualian (pun tidak dimaksudkan) untuk prinsip tidak melempar tipe. Untuk C #, yaitu NotImplementedException. Pengecualian ini dimaksudkan untuk pemadaman sementara , yaitu kode yang belum dikembangkan tetapi akan dikembangkan. Itu tidak sama dengan memutuskan secara pasti bahwa kelas yang diberikan tidak akan pernah melakukan apa pun dengan metode yang diberikan, bahkan setelah pengembangan selesai.
Flater

Terima kasih atas jawabannya. Saya sebenarnya membutuhkan solusi runtime tetapi gagal untuk menekankannya dalam pertanyaan saya. Karena saya tidak ingin membatalkan jawaban Anda, saya memutuskan untuk membuat pertanyaan baru .
problemofficer

5

Situasi saat ini

Pengaturan saat ini melanggar Prinsip Segregasi Antarmuka (I dalam SOLID).

Referensi

Menurut Wikipedia prinsip pemisahan antarmuka (ISP) menyatakan bahwa tidak ada klien yang harus dipaksa untuk bergantung pada metode yang tidak digunakannya . Prinsip pemisahan antarmuka dirumuskan oleh Robert Martin pada pertengahan 1990-an.

Dengan kata lain, jika ini antarmuka Anda:

public interface IUserBackend
{
    User getUser(int uid);
    User createUser(int uid);
    void deleteUser(int uid);
    void setPassword(int uid, string password);
}

Maka setiap kelas yang mengimplementasikan antarmuka ini harus memanfaatkan setiap metode antarmuka yang terdaftar. Tanpa pengecualian.

Bayangkan jika ada metode umum:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     backendService.deleteUser(user.Uid);
}

Jika Anda benar-benar membuatnya sehingga hanya beberapa kelas implementasi yang benar-benar dapat menghapus pengguna, maka metode ini kadang-kadang akan meledak di wajah Anda (atau tidak melakukan apa-apa sama sekali). Itu bukan desain yang bagus.


Solusi yang Anda usulkan

Saya telah melihat solusi di mana IUserInterface memiliki metode implementActions yang mengembalikan integer yang merupakan hasil dari bitwise OR dari tindakan bitwise ANDed dengan tindakan yang diminta.

Apa yang pada dasarnya ingin Anda lakukan adalah:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     if(backendService.canDeleteUser())
         backendService.deleteUser(user.Uid);
}

Saya mengabaikan bagaimana tepatnya kami menentukan apakah kelas yang diberikan dapat menghapus pengguna. Apakah itu boolean, sedikit bendera, ... tidak masalah. Semuanya bermuara pada jawaban biner: dapatkah menghapus pengguna, ya atau tidak?

Itu akan menyelesaikan masalah, kan? Secara teknis, ya. Tapi sekarang, Anda melanggar Prinsip Pergantian Liskov (L dalam SOLID).

Meninggalkan penjelasan Wikipedia yang agak rumit, saya menemukan contoh yang layak di StackOverflow . Perhatikan contoh "buruk":

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();

    duck.Swim();
}

Saya berasumsi Anda melihat kesamaan di sini. Ini adalah metode yang seharusnya menangani objek yang diabstraksi ( IDuck, IUserBackend), tetapi karena desain kelas yang dikompromikan, ia terpaksa menangani implementasi spesifik terlebih dahulu ( ElectricDuck, pastikan itu bukan IUserBackendkelas yang tidak dapat menghapus pengguna).

Ini mengalahkan tujuan mengembangkan pendekatan abstrak.

Catatan: Contoh di sini lebih mudah untuk diperbaiki daripada kasing Anda. Misalnya, itu sudah cukup untuk memiliki ElectricDuckturn sendiri di dalam yang Swim()metode. Kedua itik masih bisa berenang, sehingga hasil fungsionalnya sama.

Anda mungkin ingin melakukan hal serupa. Jangan . Anda tidak bisa hanya berpura - pura menghapus pengguna tetapi pada kenyataannya memiliki badan metode yang kosong. Meskipun ini berfungsi dari perspektif teknis, tidak mungkin untuk mengetahui apakah kelas pelaksana Anda benar-benar akan melakukan sesuatu ketika diminta untuk melakukan sesuatu. Itu adalah tempat berkembang biak untuk kode yang tidak dapat dipelihara.


Solusi yang saya usulkan

Tetapi Anda mengatakan bahwa itu mungkin (dan benar) untuk kelas implementasi hanya menangani beberapa metode ini.

Sebagai contoh, katakanlah untuk setiap kemungkinan kombinasi dari metode ini, ada kelas yang akan mengimplementasikannya. Ini mencakup semua pangkalan kami.

Solusinya di sini adalah dengan membagi antarmuka .

public interface IGetUserService
{
    User getUser(int uid);
}

public interface ICreateUserService
{
    User createUser(int uid);
}

public interface IDeleteUserService
{
    void deleteUser(int uid);
}

public interface ISetPasswordService
{
    void setPassword(int uid, string password);
}

Perhatikan bahwa Anda bisa melihat ini pada awal jawaban saya. Nama Prinsip Segregasi Antarmuka telah mengungkapkan bahwa prinsip ini dirancang untuk membuat Anda memisahkan antarmuka hingga tingkat yang memadai.

Ini memungkinkan Anda untuk mencampur dan mencocokkan antarmuka sesuka Anda:

public class UserRetrievalService 
              : IGetUserService, ICreateUserService
{
    //getUser and createUser methods implemented here
}

public class UserDeleteService
              : IDeleteUserService
{
    //deleteUser method implemented here
}

public class DoesEverythingService 
              : IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
    //All methods implemented here
}

Setiap kelas dapat memutuskan yang ingin mereka lakukan, tanpa harus melanggar kontrak antarmuka mereka.

Ini juga berarti bahwa kita tidak perlu memeriksa apakah kelas tertentu dapat menghapus pengguna. Setiap kelas yang mengimplementasikan IDeleteUserServiceantarmuka akan dapat menghapus pengguna = Tidak ada pelanggaran Prinsip Pergantian Liskov .

public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
     backendService.deleteUser(user.Uid); //guaranteed to work
}

Jika ada yang mencoba meneruskan objek yang tidak diimplementasikan IDeleteUserService, program akan menolak untuk dikompilasi. Inilah mengapa kami suka memiliki keamanan jenis.

HaveUserDeleted(new DoesEverythingService());    // No problem.
HaveUserDeleted(new UserDeleteService());        // No problem.
HaveUserDeleted(new UserRetrievalService());     // COMPILE ERROR

Catatan kaki

Saya mengambil contohnya dengan ekstrem, memisahkan antarmuka menjadi potongan terkecil yang mungkin. Namun, jika situasinya berbeda, Anda bisa lolos dengan potongan yang lebih besar.

Misalnya, jika setiap layanan yang dapat membuat pengguna selalu mampu menghapus pengguna (dan sebaliknya), Anda dapat menjaga metode ini sebagai bagian dari satu antarmuka:

public interface IManageUserService
{
    User createUser(int uid);
    void deleteUser(int uid);
}

Tidak ada manfaat teknis melakukan hal ini daripada memisahkan ke potongan yang lebih kecil; tetapi itu akan membuat pengembangan sedikit lebih mudah karena membutuhkan lebih sedikit boilerplating.


+1 untuk memisahkan antarmuka berdasarkan perilaku yang didukungnya, yang merupakan tujuan keseluruhan antarmuka.
Greg Burghardt

Terima kasih atas jawabannya. Saya sebenarnya membutuhkan solusi runtime tetapi gagal untuk menekankannya dalam pertanyaan saya. Karena saya tidak ingin membatalkan jawaban Anda, saya memutuskan untuk membuat pertanyaan baru .
problemofficer

@problemofficer: Evaluasi runtime untuk kasus-kasus ini jarang merupakan pilihan terbaik, tetapi memang ada kasus untuk melakukannya. Dalam kasus seperti itu, Anda bisa membuat metode yang bisa dipanggil tetapi mungkin akhirnya tidak melakukan apa-apa (panggilan TryDeleteUseruntuk mencerminkannya); atau Anda memiliki metode yang dengan sengaja melempar Pengecualian jika itu adalah situasi yang mungkin tapi bermasalah. Menggunakan pendekatan metode CanDoThing()dan DoThing()bekerja, tetapi penelepon eksternal Anda harus menggunakan dua panggilan (dan dihukum karena gagal melakukannya), yang kurang intuitif dan tidak elegan.
Flater

0

Jika Anda ingin menggunakan tipe level yang lebih tinggi, Anda bisa menggunakan tipe yang diset dalam bahasa pilihan Anda. Semoga ini menyediakan beberapa sintaksis gula untuk melakukan persimpangan set dan penentuan subset.

Ini pada dasarnya adalah apa yang dilakukan Java dengan EnumSet (minus gula sintaks, tapi hei, itu Java)


0

Di dunia .NET Anda dapat menghias metode dan kelas dengan atribut khusus. Ini mungkin tidak relevan dengan kasus Anda.

Kedengarannya bagi saya bahwa masalah yang Anda miliki mungkin lebih berkaitan dengan tingkat desain yang lebih tinggi.

Jika ini adalah fitur UI, seperti halaman atau komponen pengguna edit, lalu bagaimana perbedaan kemampuan yang sedang ditutup? Dalam hal ini 'test and throw' akan menjadi pendekatan yang tidak efisien untuk tujuan itu. Diasumsikan bahwa sebelum memuat setiap halaman Anda menjalankan panggilan tiruan ke setiap fungsi untuk menentukan apakah widget atau elemen harus disembunyikan, atau disajikan secara berbeda. Atau, Anda memiliki halaman web yang pada dasarnya memaksa pengguna untuk menemukan apa yang tersedia secara manual 'test and throw,' mana pun rute pengkodean yang Anda ambil, karena pengguna tidak menemukan bahwa ada sesuatu yang tidak tersedia sampai peringatan sembulan muncul.

Jadi untuk UI, Anda mungkin ingin melihat bagaimana Anda melakukan manajemen fitur, dan mengikat pilihan implementasi yang tersedia untuk itu, daripada memiliki implementasi yang dipilih mendorong fitur apa yang dapat dikelola. Anda mungkin ingin melihat kerangka kerja untuk menyusun dependensi fitur, dan secara eksplisit mendefinisikan kemampuan sebagai entitas dalam model domain Anda. Ini bahkan dapat dikaitkan dengan otorisasi. Pada dasarnya, memutuskan apakah suatu kapabilitas tersedia atau tidak berdasarkan tingkat otorisasi dapat diperluas hingga memutuskan apakah kapabilitas benar-benar diimplementasikan, dan kemudian 'fitur' UI tingkat tinggi dapat memiliki pemetaan eksplisit ke set kapabilitas.

Jika ini adalah API Web, maka pilihan desain keseluruhan mungkin menjadi rumit dengan harus mendukung beberapa versi publik dari sumber daya 'Kelola Pengguna' atau sumber daya REST 'Pengguna' saat kemampuan berkembang dari waktu ke waktu.

Jadi untuk meringkas, di dunia. NET Anda dapat mengeksploitasi berbagai Refleksi / Atribut cara menentukan terlebih dahulu kelas mana yang mengimplementasikan apa, tetapi dalam hal apa pun tampaknya masalah sebenarnya akan berada pada apa yang Anda lakukan dengan informasi itu.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.