Apakah Anda memanfaatkan manfaat dari prinsip terbuka-tertutup?

12

Prinsip open-closed (OCP) menyatakan bahwa suatu objek harus terbuka untuk ekstensi tetapi ditutup untuk modifikasi. Saya percaya saya memahaminya dan menggunakannya bersama dengan SRP untuk membuat kelas yang hanya melakukan satu hal. Dan, saya mencoba membuat banyak metode kecil yang memungkinkan untuk mengekstrak semua kontrol perilaku menjadi metode yang dapat diperluas atau diganti dalam beberapa subkelas. Dengan demikian, saya berakhir dengan kelas yang memiliki banyak titik ekstensi, baik melalui: injeksi ketergantungan dan komposisi, acara, delegasi, dll.

Pertimbangkan kelas berikut yang sederhana dan dapat diperpanjang:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Sekarang katakan, misalnya, bahwa OvertimeFactorperubahan ke 1,5. Karena kelas di atas dirancang untuk diperluas, saya dapat dengan mudah subkelas dan mengembalikan yang berbeda OvertimeFactor.

Tapi ... terlepas dari kelas yang dirancang untuk ekstensi dan mengikuti OCP, saya akan memodifikasi metode tunggal yang dipertanyakan, daripada subklasifikasi dan menimpa metode yang dimaksud dan kemudian mengirim ulang benda-benda saya ke dalam wadah IoC saya.

Akibatnya, saya telah melanggar bagian dari apa yang OCP coba capai. Rasanya saya hanya malas karena hal di atas sedikit lebih mudah. Apakah saya salah memahami OCP? Haruskah saya melakukan sesuatu yang berbeda? Apakah Anda memanfaatkan manfaat OCP secara berbeda?

Pembaruan : berdasarkan jawaban, sepertinya contoh yang dibuat-buat ini buruk karena sejumlah alasan berbeda. Maksud utama dari contoh ini adalah untuk menunjukkan bahwa kelas dirancang untuk diperluas dengan menyediakan metode yang bila diganti akan mengubah perilaku metode publik tanpa perlu mengubah kode internal atau pribadi. Tetap saja, saya pasti salah paham tentang OCP.

Kaleb Pederson
sumber

Jawaban:

10

Jika Anda memodifikasi kelas dasar maka itu tidak benar-benar tertutup kan!

Pikirkan situasi di mana Anda telah merilis perpustakaan ke dunia. Jika Anda pergi dan mengubah perilaku kelas dasar Anda dengan memodifikasi faktor lembur menjadi 1,5 maka Anda telah melanggar semua orang yang menggunakan kode Anda dengan asumsi bahwa kelas itu ditutup.

Benar-benar untuk membuat kelas ditutup tetapi terbuka Anda harus mengambil faktor lembur dari sumber alternatif (file konfigurasi mungkin) atau membuktikan metode virtual yang dapat diganti?

Jika kelas benar-benar ditutup maka setelah perubahan Anda tidak ada kasus uji akan gagal (dengan asumsi Anda memiliki cakupan 100% dengan semua kasus uji Anda) dan saya akan berasumsi bahwa ada kasus uji yang memeriksa GetOvertimeFactor() == 2.0M.

Jangan over Engineer

Tetapi jangan mengambil prinsip buka-tutup ini pada kesimpulan yang logis dan semuanya dapat dikonfigurasikan sejak awal (yang lebih dari rekayasa). Hanya tentukan bit yang saat ini Anda butuhkan.

Prinsip tertutup tidak menghalangi Anda untuk merekayasa ulang objek. Itu hanya menghalangi Anda untuk mengubah antarmuka publik yang saat ini ditetapkan ke objek Anda ( anggota yang dilindungi adalah bagian dari antarmuka publik). Anda masih dapat menambahkan lebih banyak fungsi selama fungsi yang lama tidak rusak.

Martin York
sumber
"Prinsip tertutup tidak menghalangi Anda untuk merekayasa ulang objek." Sebenarnya itu benar . Jika Anda membaca buku di mana Prinsip Terbuka-Tertutup pertama kali diusulkan, atau artikel yang memperkenalkan akronim "OCP", Anda akan melihatnya mengatakan bahwa "Tidak ada yang diizinkan untuk membuat perubahan kode sumber untuk itu" (kecuali untuk bug perbaikan).
Rogério
@ Rogério: Itu mungkin benar (tahun 1988). Tetapi definisi saat ini (dipopulerkan pada 1990 ketika OO menjadi populer) adalah tentang mempertahankan antarmuka publik yang konsisten. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York
Terima kasih untuk referensi Wikipedia. Tapi saya tidak yakin definisi "saat ini" benar-benar berbeda, karena masih bergantung pada jenis (kelas atau antarmuka) warisan. Dan kutipan "tidak ada perubahan kode sumber" yang saya sebutkan berasal dari artikel Robert Martin OCP 1996 yang (seharusnya) sejalan dengan "definisi saat ini". Secara pribadi, saya pikir Prinsip Terbuka-Tertutup akan dilupakan sekarang, seandainya Martin tidak memberinya akronim yang, tampaknya, memiliki banyak nilai pemasaran. Prinsipnya sendiri sudah usang dan berbahaya, IMO.
Rogério
3

Jadi Prinsip Terbuka Tertutup adalah gotcha ... terutama jika Anda mencoba menerapkannya pada saat yang sama dengan YAGNI . Bagaimana cara saya mematuhi keduanya sekaligus? Terapkan aturan tiga . Pertama kali Anda melakukan perubahan, lakukan secara langsung. Dan kedua kalinya juga. Ketiga kalinya, saatnya untuk abstrak yang berubah.

Pendekatan lain adalah "menipu saya sekali ...", ketika Anda harus melakukan perubahan, terapkan OCP untuk melindungi dari perubahan itu di masa depan . Saya hampir akan mengusulkan bahwa mengubah tingkat lembur adalah cerita baru. "Sebagai administrator penggajian, saya ingin mengubah tingkat lembur sehingga saya bisa mematuhi hukum perburuhan yang berlaku". Sekarang Anda memiliki UI baru untuk mengubah tingkat lembur, cara untuk menyimpannya, dan GetOvertimeFactor () hanya meminta repositori berapa tingkat lemburnya.

Michael Brown
sumber
2

Pada contoh yang Anda posting, faktor lembur harus berupa variabel atau konstanta. * (Contoh Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

ATAU

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Kemudian ketika Anda memperluas kelas, atur atau timpa faktornya. "Angka ajaib" seharusnya hanya muncul sekali. Ini jauh lebih dalam gaya OCP dan KERING (Don't Repeat Yourself), karena tidak perlu membuat kelas yang sama sekali baru untuk faktor yang berbeda jika menggunakan metode pertama, dan hanya harus mengubah konstanta dalam satu idiomatis tempatkan di yang kedua.

Saya akan menggunakan yang pertama dalam kasus di mana akan ada beberapa jenis kalkulator, masing-masing membutuhkan nilai konstan yang berbeda. Contohnya adalah pola Rantai Tanggung Jawab, yang biasanya diimplementasikan menggunakan tipe yang diturunkan. Objek yang hanya bisa melihat antarmuka (yaitu getOvertimeFactor()) menggunakannya untuk mendapatkan semua informasi yang dibutuhkan, sementara subtipe khawatir tentang informasi aktual yang akan diberikan.

Yang kedua berguna dalam kasus-kasus di mana konstanta tidak mungkin diubah, tetapi digunakan di banyak lokasi. Memiliki satu konstanta untuk diubah (jika tidak mungkin terjadi) jauh lebih mudah daripada mengaturnya di semua tempat atau mendapatkannya dari file properti.

Prinsip Open-closed adalah panggilan untuk tidak mengubah objek yang ada daripada peringatan untuk membiarkan antarmuka mereka tidak berubah. Jika Anda memerlukan beberapa perilaku yang sedikit berbeda dari kelas, atau menambahkan fungsionalitas untuk kasus tertentu, perluas dan ganti. Tetapi jika persyaratan untuk kelas itu sendiri berubah (seperti mengubah faktor), Anda perlu mengubah kelas. Tidak ada gunanya dalam hierarki kelas besar, yang sebagian besar tidak pernah digunakan.

Michael K.
sumber
Ini adalah perubahan data, bukan perubahan kode. Tingkat lembur seharusnya tidak dikodekan dengan keras.
Jim C
Anda tampaknya memiliki Get dan Set Anda mundur.
Mason Wheeler
Aduh! seharusnya diuji ...
Michael K
2

Saya tidak benar-benar melihat contoh Anda sebagai representasi hebat dari OCP. Saya pikir apa arti sebenarnya dari aturan ini:

Ketika Anda ingin menambahkan fitur, Anda hanya perlu menambahkan satu kelas dan Anda tidak perlu memodifikasi kelas lain (tetapi mungkin file konfigurasi).

Implementasi yang buruk di bawah ini. Setiap kali Anda menambahkan game, Anda perlu memodifikasi kelas GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

Kelas GamePlayer seharusnya tidak perlu dimodifikasi

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Sekarang dengan asumsi GameFactory saya juga mematuhi OCP, ketika saya ingin menambahkan game lain, saya hanya perlu membangun kelas baru yang mewarisi dari Gamekelas dan semuanya harusnya berfungsi.

Terlalu sering kelas-kelas seperti yang pertama dibangun setelah "ekstensi" bertahun-tahun dan tidak pernah dire-refraktur dengan benar dari versi aslinya (atau lebih buruk, apa yang seharusnya menjadi beberapa kelas tetap menjadi satu kelas besar).

Contoh yang Anda berikan adalah OCP-ish. Menurut pendapat saya, cara yang benar untuk menangani perubahan nilai lembur adalah dalam database dengan nilai historis disimpan sehingga data dapat diproses ulang. Kode masih harus ditutup untuk modifikasi karena akan selalu memuat nilai yang sesuai dari pencarian.

Sebagai contoh dunia nyata, saya telah menggunakan varian dari contoh saya dan Prinsip Terbuka-Tertutup benar-benar bersinar. Fungsionalitas sangat mudah untuk ditambahkan karena saya hanya harus berasal dari kelas dasar abstrak dan "pabrik" saya mengambilnya secara otomatis dan "pemain" tidak peduli apa implementasi konkret yang dikembalikan pabrik.

Austin Salonen
sumber
1

Dalam contoh khusus ini, Anda memiliki apa yang dikenal sebagai "Nilai Ajaib". Pada dasarnya nilai kode keras yang mungkin atau mungkin tidak berubah dari waktu ke waktu. Saya akan mencoba untuk mengatasi teka-teki yang Anda ungkapkan secara umum, tetapi ini adalah contoh dari jenis hal di mana membuat subkelas lebih berfungsi daripada mengubah nilai di kelas.

Kemungkinan besar, Anda telah menetapkan perilaku terlalu dini dalam hierarki kelas Anda.

Katakanlah kita punya PaycheckCalculator. The OvertimeFactorakan lebih cenderung akan mengetik off informasi tentang karyawan. Seorang karyawan per jam dapat menikmati bonus lembur, sementara karyawan yang digaji tidak akan mendapatkan bayaran apa pun. Namun, beberapa karyawan yang digaji akan mendapatkan waktu langsung karena kontrak yang sedang mereka kerjakan. Anda dapat memutuskan bahwa ada beberapa kelas skenario pembayaran yang diketahui, dan itulah cara Anda membangun logika Anda.

Di PaycheckCalculatorkelas dasar Anda membuatnya abstrak, dan tentukan metode yang Anda harapkan. Perhitungan intinya sama, hanya saja faktor-faktor tertentu dihitung secara berbeda. Anda HourlyPaycheckCalculatorkemudian akan menerapkan getOvertimeFactormetode dan mengembalikan 1,5 atau 2.0 seperti kasus Anda. Anda StraightTimePaycheckCalculatorakan menerapkan getOvertimeFactoruntuk mengembalikan 1.0. Akhirnya implementasi ketiga akan menjadi NoOvertimePaycheckCalculatoryang akan menerapkan getOvertimeFactoruntuk mengembalikan 0.

Kuncinya adalah hanya menjelaskan perilaku di kelas dasar yang dimaksudkan untuk diperluas. Rincian bagian dari keseluruhan algoritma atau nilai spesifik akan diisi oleh subclass. Fakta bahwa Anda menyertakan nilai default untuk getOvertimeFactormengarah ke "perbaikan" yang cepat dan mudah ke satu baris alih-alih memperluas kelas seperti yang Anda inginkan. Ini juga menyoroti fakta bahwa ada upaya yang terlibat dengan perluasan kelas. Ada juga upaya yang terlibat dalam memahami hierarki kelas dalam aplikasi Anda. Anda ingin merancang kelas Anda sedemikian rupa untuk meminimalkan kebutuhan untuk membuat subclass namun memberikan fleksibilitas yang Anda butuhkan.

Makanan untuk dipikirkan: Ketika kelas kami merangkum faktor data tertentu seperti OvertimeFactorpada contoh Anda, Anda mungkin perlu cara untuk menarik informasi itu dari sumber lain. Misalnya, file properti (karena ini terlihat seperti Java) atau database akan menyimpan nilainya, dan Anda PaycheckCalculatorakan menggunakan objek akses data untuk menarik nilai Anda. Ini memungkinkan orang yang tepat untuk mengubah perilaku sistem tanpa memerlukan penulisan ulang kode.

Berin Loritsch
sumber