Bagaimana cara membuat ulang aplikasi dengan banyak saklar?

10

Saya memiliki aplikasi yang mengambil integer sebagai input dan berdasarkan pada input panggilan metode statis dari berbagai kelas. Setiap kali nomor baru ditambahkan, kita perlu menambahkan case lain dan memanggil metode statis yang berbeda dari kelas yang berbeda. Sekarang ada 50 kasing di sakelar dan setiap kali saya perlu menambahkan kasing lain, saya bergidik. Apakah ada cara yang lebih baik untuk melakukan ini.

Saya melakukan beberapa pemikiran dan muncul dengan ide ini. Saya menggunakan pola strategi. Alih-alih memiliki saklar kasus, saya memiliki peta objek strategi dengan kuncinya adalah input integer. Setelah metode dipanggil, ia akan mencari objek dan memanggil metode generik untuk objek tersebut. Dengan cara ini saya dapat menghindari penggunaan konstruktor case switch.

Bagaimana menurut anda?

Kaushik Chakraborty
sumber
2
Apa masalah sebenarnya dengan kode saat ini?
Philip Kendall
Apa yang terjadi ketika Anda harus melakukan salah satu dari perubahan ini? Apakah Anda harus menambahkan switchkasing dan memanggil metode yang sudah ada sebelumnya di sistem kompleks Anda, atau Anda harus menemukan metode dan panggilannya?
Kilian Foth
@KilianFoth Saya telah mewarisi proyek ini sebagai pengembang pemeliharaan dan belum harus melakukan perubahan apa pun. Namun saya akan segera melakukan perubahan, jadi saya ingin refactor sekarang. Tetapi untuk menjawab pertanyaan Anda, ya untuk yang terakhir.
Kaushik Chakraborty
2
Saya pikir Anda perlu menunjukkan contoh ringkas tentang apa yang terjadi.
whatsisname
1
@ KaushikChakraborty: lalu buat contoh dari memori. Ada situasi di mana 250+ case uber-switch sesuai, dan ada case di mana switch buruk tidak peduli berapa sedikit case itu memiliki. Iblis ada dalam perincian dan kami tidak memiliki perincian.
whatsisname

Jawaban:

13

Sekarang ada 50 kasing di sakelar dan setiap kali saya perlu menambahkan kasing lain, saya bergidik.

Saya suka polimorfisme. Saya suka SOLID. Saya suka pemrograman berorientasi objek murni. Saya benci melihat ini diberikan perwakilan yang buruk karena mereka diterapkan secara dogmatis.

Anda belum membuat kasus yang baik untuk refactoring ke strategi. Omong-omong, refactoring memiliki nama. Ini disebut Ganti Bersyarat dengan Polimorfisme .

Saya telah menemukan beberapa saran untuk Anda dari c2.com :

Sangat masuk akal jika tes bersyarat yang sama atau sangat serupa diulangi berulang kali. Untuk tes yang sederhana dan jarang diulang, mengganti kondisional sederhana dengan verbositas definisi banyak kelas, dan kemungkinan pindah sejauh ini dari kode yang benar-benar memerlukan aktivitas yang disyaratkan secara kondisional, akan menghasilkan contoh buku teks tentang kebingungan kode. Lebih memilih kejelasan atas kemurnian dogmatis. - DanMuller

Anda memiliki sakelar dengan 50 kasing dan alternatif Anda adalah menghasilkan 50 objek. Oh dan 50 baris kode konstruksi objek. Ini bukan kemajuan. Kenapa tidak? Karena refactoring ini tidak mengurangi jumlah dari 50. Anda menggunakan refactoring ini ketika Anda menemukan Anda perlu membuat pernyataan switch lain pada input yang sama di tempat lain. Saat itulah refactoring ini membantu karena mengubah 100 kembali menjadi 50.

Selama Anda mengacu pada "saklar" seperti itu satu-satunya yang Anda miliki, saya tidak merekomendasikan ini. Satu-satunya keuntungan yang didapat dari refactoring sekarang adalah mengurangi kemungkinan beberapa goofball akan menyalin dan menempelkan 50 case switch Anda.

Apa yang saya rekomendasikan adalah melihat dengan cermat pada 50 kasus ini untuk kesamaan yang dapat diperhitungkan. Maksud saya 50? Betulkah? Anda yakin perlu banyak kasus? Anda mungkin mencoba melakukan banyak hal di sini.

candied_orange
sumber
Saya setuju dengan apa yang Anda katakan. Kode memiliki banyak redudansi, bisa jadi banyak kasus bahkan tidak diperlukan tetapi dari pandangan sepintas sepertinya tidak begitu. Setiap kasing memanggil metode yang memanggil beberapa sistem dan mengumpulkan hasil dan kembali ke kode panggilan. Setiap kelas mandiri, melakukan satu pekerjaan dan saya khawatir saya akan melanggar prinsip kohesi yang tinggi, jika saya mengurangi jumlah kasus.
Kaushik Chakraborty
2
Saya bisa mendapatkan 50 tanpa melanggar kohesi tinggi dan menjaga hal-hal mandiri. Saya hanya tidak bisa melakukannya dengan satu nomor. Saya membutuhkan 2, 5, dan 5. lainnya. Itu sebabnya disebut factoring out. Serius, lihat seluruh arsitektur Anda dan lihat apakah Anda tidak dapat menemukan jalan keluar dari 50 kasus ini. Refactoring adalah tentang membatalkan keputusan yang buruk. Tidak mengabadikannya dalam bentuk baru.
candied_orange
Sekarang, jika Anda dapat melihat cara untuk mengurangi 50 menggunakan refactoring ini, lakukan saja. Untuk memanfaatkan ide Doc Browns: Peta peta dapat mengambil dua kunci. Sesuatu untuk dipikirkan.
candied_orange
1
Saya setuju dengan komentar Candied. Masalahnya bukan 50 kasus dalam pernyataan switch, masalahnya adalah desain arsitektur tingkat tinggi yang menyebabkan Anda memanggil fungsi yang perlu memutuskan antara 50 opsi. Saya telah merancang beberapa sistem yang sangat besar dan kompleks dan tidak pernah dipaksa ke dalam situasi seperti itu.
Dunk
@Candied "Anda menggunakan refactoring ini ketika Anda menemukan Anda perlu membuat pernyataan switch lain pada input yang sama di tempat lain." Bisakah Anda menguraikan ini? otorisasi proyek pertama, validasi, prosedur CRUD kemudian dao. Jadi di setiap lapisan ada kasus saklar pada input yang sama yaitu integer, tetapi melakukan fungsi yang berbeda seperti auth, valid. jadi haruskah kita membuat satu kelas fir setiap jenis yang memiliki metode yang berbeda? Apakah case kami sesuai dengan apa yang Anda coba katakan dengan "mengulangi saklar yang sama pada input yang sama"?
Siddharth Trikha
9

Peta objek strategi saja, yang diinisialisasi dalam beberapa fungsi kode Anda, tempat Anda memiliki beberapa baris kode

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

mengharuskan Anda dan kolega Anda untuk mengimplementasikan fungsi / strategi untuk dipanggil dalam kelas yang terpisah, dengan cara yang lebih seragam (karena objek strategi Anda semua harus mengimplementasikan antarmuka yang sama). Kode seperti itu seringkali sedikit lebih komprehensif daripada

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

Namun, itu masih tidak akan membebaskan Anda dari beban mengedit file kode ini setiap kali nomor baru perlu ditambahkan. Manfaat nyata dari pendekatan ini berbeda:

  • inisialisasi peta sekarang menjadi terpisah dari kode pengiriman yang sebenarnya memanggil fungsi yang terkait dengan nomor tertentu, dan yang terakhir tidak mengandung 50 pengulangan lagi, itu hanya akan terlihat seperti myMap[number].DoIt(someParameters). Jadi kode pengiriman ini tidak perlu disentuh setiap kali nomor baru tiba dan dapat diimplementasikan sesuai dengan prinsip Terbuka-Tertutup. Selain itu, ketika Anda mendapatkan persyaratan di mana Anda perlu memperpanjang kode pengiriman itu sendiri, Anda tidak perlu mengubah 50 tempat lagi, tetapi hanya satu.

  • konten peta ditentukan pada saat run-time (sementara konten konstruksi switch ditentukan sebelum waktu kompilasi), jadi ini memberi Anda kesempatan untuk membuat logika inisialisasi lebih fleksibel atau dapat diperluas.

Jadi ya, ada beberapa manfaat, dan ini jelas merupakan langkah menuju kode yang lebih SOLID. Namun, jika itu terbayar untuk refactor, adalah sesuatu yang Anda atau tim Anda harus putuskan sendiri. Jika Anda tidak mengharapkan kode pengiriman diubah, logika inisialisasi diubah, dan keterbacaan switchbukan masalah nyata, maka refactoring Anda mungkin tidak begitu penting sekarang.

Doc Brown
sumber
Sementara saya enggan untuk mengganti secara buta setiap switch dengan polimorfisme, saya akan mengatakan bahwa menggunakan peta seperti yang disarankan Doc Brown di sini telah bekerja sangat baik bagi saya di masa lalu. Ketika Anda menerapkan antarmuka yang sama silahkan ganti Doit1, Doit2dll dengan satu Doitmetode yang memiliki banyak implementasi yang berbeda.
candied_orange
Dan jika Anda memiliki kontrol atas jenis simbol input yang digunakan sebagai kunci, Anda dapat melangkah lebih jauh dengan membuat doTheThing()metode simbol input. Maka Anda tidak perlu mempertahankan peta.
Kevin Krumwiede
1
@KevinKrumwiede: apa yang Anda sarankan berarti hanya melewatkan objek strategi sendiri di dalam program, sebagai pengganti bilangan bulat. Namun, ketika program mengambil integer sebagai input dari beberapa sumber data eksternal, harus ada pemetaan dari integer ke strategi terkait setidaknya di satu tempat sistem.
Doc Brown
Memperluas saran Doc Brown: Anda juga dapat membuat pabrik yang akan berisi logika untuk membuat objek strategi, jika Anda memutuskan untuk pergi dengan cara ini. Yang mengatakan, jawaban yang diberikan oleh CandiedOrange paling masuk akal bagi saya.
Vladimir Stokic
@DocBrown Itulah yang saya maksud dengan "jika Anda memiliki kendali atas jenis simbol input."
Kevin Krumwiede
0

Saya sangat mendukung strategi yang dijabarkan dalam jawaban oleh @DocBrown .

Saya akan menyarankan peningkatan jawaban.

Panggilan

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

dapat didistribusikan. Anda tidak harus kembali ke file yang sama untuk menambahkan strategi lain, yang menganut prinsip Open-Closed lebih baik.

Katakanlah Anda menerapkan Strategy1dalam file Strategy1.cpp. Anda dapat memiliki blok kode berikut di dalamnya.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Anda dapat mengulangi kode yang sama di setiap file StategyN.cpp. Seperti yang Anda lihat, itu akan menjadi banyak kode berulang. Untuk mengurangi duplikasi kode, Anda bisa menggunakan templat yang bisa dimasukkan ke dalam file yang dapat diakses oleh semua Strategykelas.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

Setelah itu, satu-satunya hal yang harus Anda gunakan di Strategy1.cpp adalah:

static StrategyHelper::Initializer<1, Strategy1> initializer;

Baris yang sesuai di StrategyN.cpp adalah:

static StrategyHelper::Initializer<N, StrategyN> initializer;

Anda bisa menggunakan templat ke level lain dengan menggunakan templat kelas untuk kelas Strategi konkret.

class Strategy { ... };

template <int N> class ConcreteStrategy;

Dan kemudian, alih-alih Strategy1, gunakan ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Ubah kelas pembantu untuk mendaftar Strategymenjadi:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Ubah kode di Strateg1.cpp ke:

static StrategyHelper::Initializer<1> initializer;

Ubah kode di StrategN.cpp ke:

static StrategyHelper::Initializer<N> initializer;
R Sahu
sumber