Halo, Habr! Berikut ini adalah terjemahan dari artikel oleh Robert Martin dari Prinsip Terbuka-Tertutup , yang ia terbitkan pada Januari 1996. Artikel ini, secara sederhana, bukan yang terbaru. Tetapi di RuNet, artikel Paman Bob tentang SOLID hanya diceritakan kembali dalam bentuk terpotong, jadi saya pikir terjemahan penuh tidak akan berlebihan.

Saya memutuskan untuk memulai dengan huruf O, karena prinsip keterbukaan-penutupan, sebenarnya, adalah sentral. Di antara hal-hal lain, ada banyak seluk-beluk penting yang perlu diperhatikan:
- Tidak ada program yang dapat "ditutup" 100%.
- Pemrograman berorientasi objek (OOP) beroperasi tidak dengan objek fisik dari dunia nyata, tetapi dengan konsep - misalnya, konsep "pemesanan".
Ini adalah artikel pertama di kolom Catatan Teknisi untuk The C ++ Report . Artikel-artikel yang diterbitkan dalam kolom ini akan fokus pada penggunaan C ++ dan OOP dan menyentuh kesulitan dalam pengembangan perangkat lunak. Saya akan mencoba membuat bahan-bahan yang pragmatis dan berguna untuk melatih para insinyur. Untuk dokumentasi desain berorientasi objek dalam artikel ini saya akan menggunakan notasi Buch.
Ada banyak heuristik yang terkait dengan pemrograman berorientasi objek. Misalnya, "semua variabel anggota harus pribadi", atau "variabel global harus dihindari", atau "penentuan jenis saat runtime berbahaya". Apa alasan heuristik seperti itu? Mengapa itu benar? Apakah itu selalu benar? Kolom ini mengeksplorasi prinsip desain yang mendasari heuristik ini - prinsip keterbukaan-penutupan.
Ivar Jacobson berkata: “Semua sistem berubah selama siklus hidup. Ini harus diingat ketika merancang sistem yang memiliki lebih dari satu versi yang diharapkan. " Bagaimana kita bisa merancang suatu sistem sehingga stabil dalam menghadapi perubahan dan memiliki lebih dari satu versi yang diharapkan? Bertrand Meyer memberi tahu kami tentang hal ini pada tahun 1988, ketika prinsip keterbukaan-kedekatan yang terkenal sekarang dirumuskan:
Entitas program (kelas, modul, fungsi, dll.) Harus terbuka untuk ekspansi dan ditutup untuk perubahan.
Jika satu perubahan dalam program ini melibatkan perubahan dalam modul dependen, maka program menampilkan tanda-tanda yang tidak diinginkan dari desain "buruk".
Program menjadi rapuh, tidak fleksibel, tidak dapat diprediksi dan tidak digunakan. Prinsip keterbukaan-kedekatan menyelesaikan masalah-masalah ini dengan cara yang sangat mudah. Dia mengatakan bahwa perlu untuk merancang modul yang tidak pernah berubah . Ketika persyaratan berubah, Anda perlu memperluas perilaku modul tersebut dengan menambahkan kode baru, daripada mengubah kode lama yang sudah berfungsi.
Deskripsi
Modul yang memenuhi prinsip keterbukaan-kedekatan memiliki dua karakteristik utama:
- Terbuka untuk ekspansi. Ini berarti bahwa perilaku modul dapat diperluas. Artinya, kita dapat menambahkan perilaku baru ke modul sesuai dengan persyaratan perubahan untuk aplikasi atau untuk memenuhi kebutuhan aplikasi baru.
- Ditutup untuk perubahan. Kode sumber modul semacam itu tidak dapat disentuh. Tidak ada yang punya hak untuk mengubahnya.
Tampaknya kedua tanda ini tidak cocok satu sama lain. Cara standar untuk memperluas perilaku modul adalah dengan mengubahnya. Modul yang tidak dapat diubah biasanya dianggap sebagai modul dengan perilaku tetap. Bagaimana dua kondisi yang berlawanan ini dipenuhi?
Kunci dari solusinya adalah abstraksi.
Dalam C ++, menggunakan prinsip-prinsip desain berorientasi objek, dimungkinkan untuk membuat abstraksi tetap yang dapat mewakili serangkaian perilaku yang tidak terbatas.
Abstraksi adalah kelas dasar abstrak, dan serangkaian perilaku yang tidak terbatas diwakili oleh semua kelas penerus yang mungkin. Modul dapat memanipulasi abstraksi. Modul semacam itu ditutup untuk perubahan, karena itu tergantung pada abstraksi tetap. Juga, perilaku modul dapat diperluas dengan menciptakan keturunan abstraksi baru.
Diagram di bawah ini menunjukkan opsi desain sederhana yang tidak memenuhi prinsip keterbukaan-kedekatan. Kedua kelas, Client
dan Server
, tidak abstrak. Tidak ada jaminan bahwa fungsi yang menjadi anggota kelas Server
adalah virtual. Kelas Client
menggunakan kelas Server
. Jika kami ingin objek kelas Client
menggunakan objek server yang berbeda, kami harus mengubah kelas Client
untuk merujuk ke kelas server baru.

Klien tertutup
Dan diagram berikut menunjukkan opsi desain yang sesuai, yang memenuhi prinsip keterbukaan-kedekatan. Dalam hal ini, kelas AbstractServer
adalah kelas abstrak, semua fungsi anggota yang virtual. Kelas Client
menggunakan abstraksi. Namun, objek dari kelas Client
akan menggunakan objek dari kelas penerus Server
. Jika kami ingin objek kelas Client
menggunakan kelas server yang berbeda, kami akan memperkenalkan turunan baru dari kelas AbstractServer
. Kelas Client
akan tetap tidak berubah.

Buka klien
Shape
Abstrak
Pertimbangkan aplikasi yang harus menggambar lingkaran dan bujur sangkar dalam GUI standar. Lingkaran dan kotak harus digambar dalam urutan tertentu. Dalam urutan yang sesuai, daftar lingkaran dan kotak akan dikompilasi, program harus melalui daftar ini dalam urutan dan menggambar setiap lingkaran atau kotak.
Dalam C, menggunakan teknik pemrograman prosedural yang tidak memenuhi prinsip buka-tutup, kita bisa menyelesaikan masalah ini seperti yang ditunjukkan pada Listing 1. Di sini kita melihat banyak struktur data dengan elemen pertama yang sama. Elemen ini adalah kode tipe yang mengidentifikasi struktur data sebagai lingkaran atau kuadrat. Fungsi DrawAllShapes
melewati array pointer ke struktur data ini, mengenali kode jenis dan kemudian memanggil fungsi yang sesuai ( DrawCircle
atau DrawSquare
).
Fungsi DrawAllShapes
tidak memenuhi prinsip keterbukaan-penutupan, karena tidak dapat "ditutup" dari jenis bentuk baru. Jika saya ingin memperluas fungsi ini dengan kemampuan untuk menggambar bentuk dari daftar yang menyertakan segitiga, maka saya perlu mengubah fungsinya. Sebenarnya, saya harus mengubah fungsi untuk setiap jenis bentuk baru yang perlu saya gambar.
Tentu saja, program ini hanyalah sebuah contoh. Dalam kehidupan nyata, operator switch
dari fungsi DrawAllShapes
akan diulang berkali-kali di berbagai fungsi di seluruh aplikasi, dan masing-masing akan melakukan sesuatu yang berbeda. Menambahkan bentuk baru ke aplikasi seperti itu berarti menemukan semua tempat di mana switch
(atau if/else
rantai if/else
) digunakan, dan menambahkan bentuk baru ke masing-masing. Selain itu, sangat tidak mungkin bahwa semua switch
dan if/else
rantai if/else
akan terstruktur sebaik di DrawAllShapes
. Adalah jauh lebih mungkin bahwa predikat if
akan digabungkan dengan operator logis, atau blok case
dari switch
akan digabungkan sedemikian rupa untuk “menyederhanakan” tempat tertentu dalam kode. Karena itu, masalah menemukan dan memahami semua tempat di mana Anda perlu menambahkan sosok baru bisa menjadi hal yang tidak sepele.
Dalam Listing 2, saya akan menunjukkan kode yang menunjukkan solusi persegi / lingkaran yang memenuhi prinsip keterbukaan-penutupan. Kelas Shape
abstrak diperkenalkan. Kelas abstrak ini berisi satu fungsi Draw
virtual murni. Kelas Circle
dan Square
adalah turunan dari kelas Shape
.
Perhatikan bahwa jika kita ingin memperluas perilaku fungsi DrawAllShapes
di Listing 2 untuk menggambar bentuk baru, yang perlu kita lakukan adalah menambahkan turunan baru dari kelas Shape
. Tidak perlu mengubah fungsi DrawAllShapes
. Oleh karena itu, DrawAllShapes
memenuhi prinsip keterbukaan-kedekatan. Perilakunya dapat diperluas tanpa mengubah fungsi itu sendiri.
Di dunia nyata, kelas Shape
akan berisi banyak metode lain. Namun, menambahkan bentuk baru ke aplikasi masih sangat sederhana, karena yang perlu Anda lakukan adalah memasukkan pewaris baru dan mengimplementasikan fungsi-fungsi ini. Tidak perlu menjelajahi seluruh aplikasi untuk mencari tempat yang membutuhkan perubahan.
Oleh karena itu, program yang memenuhi prinsip keterbukaan-kedekatan diubah dengan menambahkan kode baru, dan bukan dengan mengubah kode yang sudah ada, mereka tidak mengubah perubahan karakteristik program yang tidak sesuai dengan prinsip ini.
Strategi Entri Tertutup
Jelas, tidak ada program yang dapat ditutup 100%. Misalnya, apa yang terjadi pada fungsi DrawAllShapes
di Listing 2 jika kita memutuskan bahwa lingkaran dan kotak harus digambar terlebih dahulu? Fungsi DrawAllShapes
tidak tertutup dari perubahan semacam ini. Secara umum, tidak masalah seberapa "tertutup" modul ini, selalu ada beberapa jenis perubahan yang tidak ditutup.
Karena penutupan tidak dapat lengkap, itu harus diperkenalkan secara strategis. Artinya, perancang harus memilih jenis perubahan dari mana program akan ditutup. Ini membutuhkan beberapa pengalaman. Pengembang berpengalaman mengetahui pengguna dan industri dengan cukup baik untuk menghitung kemungkinan berbagai perubahan. Dia kemudian memastikan bahwa prinsip keterbukaan-kedekatan dihormati untuk perubahan yang paling mungkin.
Gunakan abstraksi untuk mencapai kedekatan tambahan
Bagaimana kita bisa menutup fungsi DrawAllShapes
dari perubahan dalam urutan gambar? Ingat bahwa penutupan didasarkan pada abstraksi. Oleh karena itu, untuk menutup DrawAllShapes
dari pemesanan, kita perlu semacam "pemesanan abstraksi". Kasus khusus pemesanan, yang disajikan di atas, adalah menggambar tokoh dari satu jenis di depan tokoh dari jenis lain.
Kebijakan pemesanan menyiratkan bahwa dengan dua objek, Anda dapat menentukan mana yang harus ditarik terlebih dahulu. Oleh karena itu, kita dapat mendefinisikan metode untuk kelas Shape
disebut Precedes
, yang mengambil objek Shape
lain sebagai argumen dan mengembalikan nilai Boolean true
sebagai hasilnya jika objek kelas Shape
yang menerima pesan ini perlu disortir sebelum objek kelas Shape
yang sebelumnya disahkan sebagai argumen.
Dalam C ++, fungsi ini dapat direpresentasikan sebagai kelebihan dari operator "<". Listing 3 memperlihatkan kelas Shape
dengan metode penyortiran.
Sekarang kita memiliki cara untuk menentukan urutan objek dari kelas Shape
, kita dapat mengurutkannya dan kemudian menggambarnya. Kode 4 menunjukkan kode C ++ yang sesuai. Menggunakan kelas Set
, OrderedSet
dan Iterator
dari kategori Components
dikembangkan dalam buku saya (Merancang Aplikasi Berorientasi Objek C ++ menggunakan Metode Booch, Robert C. Martin, Prentice Hall, 1995).
Jadi, kami telah menerapkan pemesanan objek dari kelas Shape
dan menggambarnya dalam urutan yang sesuai. Namun kami masih belum memiliki implementasi abstraksi pemesanan. Jelas, setiap objek Shape
harus menimpa metode Precedes
untuk menentukan urutan. Bagaimana ini bisa berhasil? Kode apa yang perlu ditulis dalam Circle::Precedes
sehingga lingkaran ditarik ke kotak? Perhatikan daftar 5.
Jelas bahwa fungsi ini tidak memenuhi prinsip keterbukaan-kedekatan. Tidak ada cara untuk menutupnya dari keturunan baru dari kelas Shape
. Setiap kali keturunan baru dari kelas Shape
muncul, fungsi ini perlu diubah.
Menggunakan Pendekatan Data Driven untuk Mencapai Penutupan
Kedekatan pewaris dari kelas Shape
dapat dicapai dengan menggunakan pendekatan tabular yang tidak memicu perubahan di setiap kelas yang diwariskan. Contoh dari pendekatan ini ditunjukkan pada Listing 6.
Dengan menggunakan pendekatan ini, kami berhasil menutup fungsi DrawAllShapes
dari perubahan yang terkait dengan pemesanan, dan setiap keturunan dari kelas Shape
- dari memperkenalkan turunan baru atau dari perubahan kebijakan pemesanan untuk objek-objek dari kelas Shape
tergantung pada jenisnya (misalnya, sehingga objek dari kelas Squares
harus ditarik terlebih dahulu).
Satu-satunya elemen yang tidak tertutup dari mengubah urutan bentuk gambar adalah tabel. Tabel dapat ditempatkan dalam modul terpisah, terpisah dari semua modul lainnya, dan karenanya perubahannya tidak akan memengaruhi modul lainnya.
Penutupan lebih lanjut
Ini bukan akhir dari cerita. Kami menutup hierarki kelas Shape
dan fungsi DrawAllShapes
dari mengubah kebijakan pemesanan berdasarkan jenis bentuk. Namun, turunan dari kelas Shape
tidak tertutup dari kebijakan pemesanan yang tidak terkait dengan tipe Shape
. Tampaknya kita perlu mengatur gambar bentuk sesuai dengan struktur tingkat yang lebih tinggi. Sebuah studi lengkap tentang masalah-masalah semacam itu berada di luar cakupan artikel ini; namun, pembaca yang tertarik mungkin berpikir bagaimana menyelesaikan masalah ini menggunakan kelas OrderedObject
abstrak yang terkandung dalam kelas OrderedShape
, yang mewarisi dari kelas Shape
dan OrderedObject
.
Heuristik dan Konvensi
Seperti yang telah disebutkan di awal artikel, prinsip keterbukaan-kedekatan adalah motivasi utama di balik banyak heuristik dan konvensi yang telah muncul selama bertahun-tahun dalam pengembangan paradigma OOP. Berikut ini adalah yang paling penting.
Jadikan semua variabel anggota bersifat pribadi
Ini adalah salah satu konvensi PLO yang paling bertahan lama. Variabel anggota hanya boleh diketahui dengan metode kelas di mana mereka didefinisikan. Anggota variabel tidak boleh diketahui ke kelas lain, termasuk kelas turunan. Oleh karena itu, mereka harus dinyatakan dengan pengubah akses private
, bukan public
atau protected
.
Dalam terang prinsip keterbukaan-kedekatan, alasan konvensi semacam itu dapat dipahami. Ketika variabel anggota kelas berubah, setiap fungsi yang bergantung padanya harus berubah. Artinya, fungsinya tidak tertutup dari perubahan variabel-variabel ini.
Dalam OOP, kami berharap bahwa metode kelas tidak tertutup terhadap perubahan variabel yang menjadi anggota kelas ini. Namun, kami berharap bahwa kelas lain, termasuk subclass, ditutup dari perubahan variabel-variabel ini. Ini disebut enkapsulasi.
Tetapi bagaimana jika Anda memiliki variabel yang Anda yakin tidak akan pernah berubah? Apakah masuk akal untuk menjadikannya private
? Misalnya, Listing 7 menunjukkan kelas Device
yang berisi bool status
anggota variabel. Ini menyimpan status operasi terakhir. Jika operasi berhasil, maka nilai variabel status
akan true
, jika tidak false
.
Kita tahu bahwa jenis atau makna variabel ini tidak akan pernah berubah. Jadi mengapa tidak membuatnya public
dan memberikan klien akses langsung ke sana? Jika variabel benar-benar tidak pernah berubah, jika semua klien mengikuti aturan dan hanya membaca dari variabel ini, maka tidak ada yang salah dengan fakta bahwa variabel tersebut bersifat publik. Namun, pertimbangkan apa yang akan terjadi jika salah satu klien mengambil kesempatan untuk menulis ke variabel ini dan mengubah nilainya.
Tiba-tiba, klien ini dapat memengaruhi operasi klien lain apa pun dari kelas Device
. Ini berarti bahwa tidak mungkin untuk menutup klien dari kelas Device
dari perubahan ke modul yang salah ini. Ini terlalu banyak risiko.
Di sisi lain, anggaplah kita memiliki kelas Time
, ditunjukkan pada Listing 8. Apa bahaya dari publisitas variabel yang menjadi anggota kelas ini? Sangat tidak mungkin mereka akan berubah. Selain itu, tidak masalah jika modul klien mengubah nilai-nilai variabel ini atau tidak, karena perubahan dalam variabel-variabel ini diasumsikan. Juga sangat tidak mungkin bahwa kelas yang diwarisi dapat bergantung pada nilai variabel anggota tertentu. Jadi, apakah ada masalah?
Satu-satunya keluhan yang bisa saya buat pada kode di Listing 8 adalah bahwa perubahan waktu tidak terjadi secara atom. Artinya, klien dapat mengubah nilai variabel minutes
tanpa mengubah nilai variabel hours
. Ini dapat menyebabkan objek dari kelas Time
mengandung data yang tidak konsisten. Saya lebih suka memperkenalkan fungsi tunggal untuk mengatur waktu, yang akan membutuhkan tiga argumen, yang akan menjadikan pengaturan waktu sebagai operasi atom. Tapi ini argumen yang lemah.
Sangat mudah untuk menemukan kondisi lain di mana publisitas variabel-variabel ini dapat menyebabkan masalah. Namun, pada akhirnya, tidak ada alasan yang meyakinkan untuk menjadikannya private
. Saya masih berpikir bahwa mempublikasikan variabel semacam itu adalah gaya yang buruk, tapi mungkin itu bukan desain yang buruk. Saya percaya bahwa ini adalah gaya yang buruk, karena hampir tidak ada biaya untuk memasuki fungsi yang sesuai untuk mengakses anggota ini, dan sudah pasti layak untuk melindungi diri Anda dari risiko kecil yang terkait dengan kemungkinan terjadinya masalah dengan penutupan.
Oleh karena itu, dalam kasus yang jarang terjadi, ketika prinsip keterbukaan-kedekatan tidak dilanggar, larangan variabel public
- dan protected
lebih tergantung pada gaya dan bukan pada konten.
Tidak ada variabel global ... sama sekali!
Argumen terhadap variabel global sama dengan argumen terhadap variabel anggota publik. Tidak ada modul yang bergantung pada variabel global yang dapat ditutup dari modul yang dapat menulisnya. Setiap modul yang menggunakan variabel ini dengan cara yang tidak dimaksudkan oleh modul lain akan merusak modul ini. Terlalu berisiko untuk memiliki banyak modul, tergantung pada keanehan dari satu modul jahat.
Di sisi lain, dalam kasus di mana variabel global memiliki sejumlah kecil modul bergantung padanya atau tidak dapat digunakan dengan cara yang salah, mereka tidak membahayakan. Perancang harus mengevaluasi seberapa banyak privasi dikorbankan dan menentukan apakah kenyamanan yang disediakan oleh variabel global sepadan.
Di sini lagi, masalah gaya ikut bermain. Alternatif untuk menggunakan variabel global biasanya tidak mahal. Dalam kasus seperti itu, penggunaan teknik yang memperkenalkan, meskipun kecil, tetapi risiko untuk penutupan, bukan teknik yang sepenuhnya menghilangkan risiko seperti itu, adalah tanda gaya buruk. Namun, terkadang menggunakan variabel global sangat nyaman. Contoh tipikal adalah variabel global cout dan cin. Dalam kasus seperti itu, jika prinsip keterbukaan-kedekatan tidak dilanggar, Anda dapat mengorbankan gaya demi kenyamanan.
RTTI berbahaya
Larangan umum lainnya adalah penggunaan dynamic_cast
. Sangat sering, dynamic_cast
atau bentuk lain dari penentuan jenis runtime (RTTI) dituduh sebagai teknik yang sangat berbahaya dan karenanya harus dihindari. Pada saat yang sama, mereka sering memberikan contoh dari Listing 9, yang jelas-jelas melanggar prinsip keterbukaan-kedekatan. Namun, Listing 10 menunjukkan contoh program serupa yang menggunakan dynamic_cast
tanpa melanggar prinsip buka-tutup.
Perbedaan di antara mereka adalah bahwa dalam kasus pertama, ditunjukkan pada Listing 9, kode perlu diubah setiap kali keturunan baru dari kelas Shape
muncul (belum lagi bahwa ini adalah solusi yang benar-benar konyol). Namun, dalam Listing 10, tidak diperlukan perubahan dalam kasus ini. Oleh karena itu, kode dalam Listing 10 tidak melanggar prinsip buka-tutup.
Dalam hal ini, aturan praktisnya adalah bahwa RTTI dapat digunakan jika prinsip keterbukaan-penutupan tidak dilanggar.
Kesimpulan
Saya bisa berbicara lama tentang prinsip keterbukaan-kedekatan. Dalam banyak hal, prinsip ini paling penting untuk pemrograman berorientasi objek. Kepatuhan dengan prinsip khusus ini memberikan keuntungan utama dari teknologi berorientasi objek, yaitu penggunaan kembali dan dukungan.
, - -. , , , , , .