Katakanlah saya memiliki kelas Foobar
yang menggunakan (tergantung pada) kelas Widget
. Pada hari-hari baik, Widget
wolud dinyatakan sebagai bidang Foobar
, atau mungkin sebagai penunjuk pintar jika perilaku polimorfik diperlukan, dan itu akan diinisialisasi dalam konstruktor:
class Foobar {
Widget widget;
public:
Foobar() : widget(blah blah blah) {}
// or
std::unique_ptr<Widget> widget;
public:
Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
(…)
};
Dan kita akan siap dan selesai. Sayangnya, hari ini, anak-anak Jawa akan menertawakan kita begitu mereka melihatnya, dan memang seharusnya, karena pasangan Foobar
dan Widget
bersama - sama. Solusinya tampaknya sederhana: gunakan Injeksi Ketergantungan untuk mengambil konstruksi dependensi di luar Foobar
kelas. Tapi kemudian, C ++ memaksa kita untuk berpikir tentang kepemilikan dependensi. Tiga solusi muncul dalam pikiran:
Pointer unik
class Foobar {
std::unique_ptr<Widget> widget;
public:
Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
(…)
}
Foobar
mengklaim kepemilikan satu-satunya Widget
yang diberikan padanya. Ini memiliki keuntungan sebagai berikut:
- Dampak pada kinerja dapat diabaikan.
- Ini aman, karena
Foobar
mengontrol masa pakaiWidget
, jadi itu memastikan bahwaWidget
tidak tiba-tiba menghilang. - Aman
Widget
tidak akan bocor dan akan dihancurkan dengan benar ketika tidak lagi dibutuhkan.
Namun, ini harus dibayar:
- Ini menempatkan pembatasan pada bagaimana
Widget
instance dapat digunakan, misalnya tidak ada stack-dialokasikanWidgets
dapat digunakan, tidakWidget
dapat dibagikan.
Pointer yang dibagikan
class Foobar {
std::shared_ptr<Widget> widget;
public:
Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
(…)
}
Ini mungkin setara atau paling dekat dengan Jawa dan bahasa pengumpulan sampah lainnya. Keuntungan:
- Lebih universal, karena memungkinkan berbagi dependensi.
- Menjaga keamanan (poin 2 dan 3) dari
unique_ptr
solusi.
Kekurangan:
- Menghabiskan sumber daya saat tidak ada pembagian yang terlibat.
- Masih membutuhkan alokasi tumpukan dan melarang objek yang dialokasikan tumpukan.
Pointer pengamatan biasa
class Foobar {
Widget *widget;
public:
Foobar(Widget *w) : widget(w) {}
(…)
}
Tempatkan pointer mentah di dalam kelas dan mengalihkan beban kepemilikan kepada orang lain. Pro:
- Sesederhana itu bisa.
- Universal, menerima sembarang saja
Widget
.
Cons:
- Tidak aman lagi.
- Memperkenalkan entitas lain yang bertanggung jawab atas kepemilikan keduanya
Foobar
danWidget
.
Beberapa metaprogram template gila
Satu-satunya keuntungan yang dapat saya pikirkan adalah bahwa saya dapat membaca semua buku yang saya tidak punya waktu untuk sementara perangkat lunak saya builing;)
Saya condong ke solusi ketiga, karena paling universal, Foobars
bagaimanapun juga, ada sesuatu yang harus dikelola , jadi mengelola Widgets
adalah perubahan sederhana. Namun, menggunakan raw pointer mengganggu saya, di sisi lain solusi smart pointer merasa salah bagi saya, karena mereka membuat konsumen ketergantungan membatasi bagaimana ketergantungan itu dibuat.
Apakah saya melewatkan sesuatu? Atau apakah Injeksi Ketergantungan pada C ++ tidak sepele? Haruskah kelas memiliki dependensi atau hanya mengamati mereka?
sumber
std::unique_ptr
adalah cara untuk pergi. Anda dapat menggunakanstd::move()
untuk mentransfer kepemilikan sumber daya dari ruang lingkup atas ke kelas.Foobar
satu-satunya pemilik? Dalam kasus lama itu sederhana. Tetapi masalah dengan DI, seperti yang saya lihat, adalah karena ia memisahkan kelas dari konstruksi dependensinya, ia juga memisahkannya dari kepemilikan dependensi tersebut (karena kepemilikan terkait dengan konstruksi). Di lingkungan sampah yang dikumpulkan seperti Jawa, itu tidak masalah. Di C ++, ini.Jawaban:
Saya bermaksud menulis ini sebagai komentar, tetapi ternyata terlalu lama.
Apakah Anda harus menggunakan
std::unique_ptr<Widget>
ataustd::shared_ptr<Widget>
, itu terserah Anda untuk memutuskan dan datang dari fungsi Anda.Mari kita asumsikan Anda memiliki
Utilities::Factory
, yang bertanggung jawab atas pembuatan blok Anda, sepertiFoobar
. Mengikuti prinsip DI, Anda perluWidget
instance, untuk menyuntikkannya menggunakanFoobar
konstruktor, artinya di dalam salah satuUtilities::Factory
metode, misalnyacreateWidget(const std::vector<std::string>& params)
, Anda membuat Widget dan menyuntikkannya keFoobar
objek.Sekarang Anda memiliki
Utilities::Factory
metode, yang menciptakanWidget
objek. Apakah ini berarti, metode yang harus saya jawab untuk penghapusannya? Tentu saja tidak. Itu hanya ada untuk membuat Anda menjadi contoh.Bayangkan, Anda sedang mengembangkan aplikasi, yang akan memiliki banyak jendela. Setiap jendela direpresentasikan menggunakan
Foobar
kelas, jadi sebenarnyaFoobar
bertindak seperti pengontrol.Pengontrol mungkin akan menggunakan sebagian milik Anda
Widget
dan Anda harus bertanya pada diri sendiri:Jika saya membuka jendela khusus ini di aplikasi saya, saya akan membutuhkan Widget ini. Apakah widget ini dibagikan di antara jendela aplikasi lain? Jika demikian, saya seharusnya tidak mungkin membuatnya lagi, karena mereka akan selalu terlihat sama, karena mereka dibagikan.
std::shared_ptr<Widget>
adalah cara untuk pergi.Anda juga memiliki jendela aplikasi, di mana hanya ada jendela
Widget
khusus yang terikat di sini, artinya tidak akan ditampilkan di tempat lain. Jadi, jika Anda menutup jendela, Anda tidak perlu lagi diWidget
mana pun dalam aplikasi Anda, atau setidaknya instansinya.Di situlah
std::unique_ptr<Widget>
datang untuk mengklaim takhtanya.Memperbarui:
Saya benar-benar tidak setuju dengan @DominicMcDonnell , tentang masalah seumur hidup. Memanggil
std::move
padastd::unique_ptr
benar-benar mentransfer kepemilikan, sehingga bahkan jika Anda membuatobject A
metode dan menyebarkannya ke yang lainobject B
sebagai dependensi, yangobject B
sekarang akan bertanggung jawab untuk sumber dayaobject A
dan benar akan menghapusnya, ketikaobject B
keluar dari ruang lingkup.sumber
unique_ptr
adalah pengujian unit (DI diiklankan sebagai ramah pengujian): Saya ingin mengujiFoobar
, jadi saya membuatWidget
, meneruskannyaFoobar
, berolahragaFoobar
dan kemudian saya ingin memeriksanyaWidget
, tetapi kecualiFoobar
entah bagaimana memaparkannya, saya dapat sejak itu telah diklaim olehFoobar
.Foobar
memiliki sumber daya tidak berarti, tidak ada orang lain yang harus menggunakannya. Anda bisa menerapkanFoobar
metode sepertiWidget* getWidget() const { return this->_widget.get(); }
, yang akan mengembalikan Anda pointer mentah yang dapat Anda kerjakan. Anda kemudian dapat menggunakan metode ini sebagai input untuk pengujian unit Anda, ketika Anda ingin mengujiWidget
kelas.Saya akan menggunakan pointer mengamati dalam bentuk referensi. Ini memberikan sintaks yang jauh lebih baik ketika Anda menggunakannya dan memiliki keunggulan semantik yang tidak menyiratkan kepemilikan, yang dapat pointer sederhana.
Masalah terbesar dengan pendekatan ini adalah masa hidup. Anda harus memastikan bahwa dependensi dibangun sebelumnya, dan dihancurkan setelah kelas dependen Anda. Itu bukan masalah sederhana. Menggunakan shared pointer (sebagai penyimpanan dependensi dan di semua kelas yang bergantung padanya, opsi 2 di atas) dapat menghapus masalah ini, tetapi juga memperkenalkan masalah dependensi melingkar yang juga non-sepele, dan menurut saya kurang jelas dan karenanya lebih sulit untuk mendeteksi sebelum menyebabkan masalah. Itulah sebabnya saya lebih suka untuk tidak melakukannya secara otomatis dan manual mengatur masa hidup dan pesanan konstruksi. Saya juga melihat sistem yang menggunakan pendekatan templat cahaya yang membangun daftar objek dalam urutan mereka dibuat dan menghancurkannya dalam urutan yang berlawanan, itu tidak sangat mudah, tetapi itu membuat banyak hal lebih sederhana.
Memperbarui
Tanggapan David Packer membuat saya berpikir sedikit tentang pertanyaan itu. Jawaban asli berlaku untuk dependensi bersama dalam pengalaman saya, yang merupakan salah satu keuntungan dari injeksi dependensi, Anda dapat memiliki beberapa instance menggunakan satu instance dari dependensi. Namun jika kelas Anda perlu memiliki turunannya sendiri dari contoh tertentu maka itu
std::unique_ptr
adalah jawaban yang benar.sumber
Pertama-tama - ini adalah C ++, bukan Java - dan di sini banyak hal berjalan berbeda. Orang-orang Jawa tidak memiliki masalah tentang kepemilikan karena ada pengumpulan sampah otomatis yang memecahkannya.
Kedua: Tidak ada jawaban umum untuk pertanyaan ini - tergantung pada apa persyaratannya!
Apa masalah tentang menggabungkan FooBar dan Widget? FooBar ingin menggunakan Widget, dan jika setiap instance FooBar selalu memiliki Widget sendiri dan yang sama, biarkan digabungkan ...
Dalam C ++, Anda bahkan dapat melakukan hal-hal "aneh" yang sama sekali tidak ada di Jawa, misalnya memiliki konstruktor template variadic (well, ada ... notasi di Jawa, yang dapat digunakan dalam konstruktor juga, tetapi itu adalah hanya gula sintaksis untuk menyembunyikan array objek, yang sebenarnya tidak ada hubungannya dengan templat variadik sungguhan!) - kategori 'Some metaprogramming templat gila':
Suka itu?
Tentu saja, ada yang alasan ketika Anda inginkan atau butuhkan untuk memisahkan kedua kelas - misalnya jika Anda perlu untuk membuat widget pada waktu jauh sebelum setiap contoh FooBar ada, jika Anda ingin atau perlu menggunakan kembali widget, ..., atau hanya karena untuk masalah saat ini lebih tepat (misalnya jika Widget adalah elemen GUI dan FooBar harus / bisa / tidak boleh menjadi salah satu).
Kemudian, kita kembali ke poin kedua: Tidak ada jawaban umum. Anda perlu memutuskan, apa - untuk masalah aktual - adalah solusi yang lebih tepat. Saya suka pendekatan referensi DominicMcDonnell, tetapi hanya bisa diterapkan jika kepemilikan tidak akan diambil oleh FooBar (well, sebenarnya, Anda bisa, tetapi itu menyiratkan kode yang sangat, sangat kotor ...). Selain itu, saya bergabung dengan jawaban David Packer (yang dimaksudkan untuk ditulis sebagai komentar - tetapi jawaban yang bagus, toh).
sumber
class
es dengan metode statis, bahkan jika itu hanya pabrik seperti pada contoh Anda. Itu melanggar prinsip-prinsip OO. Gunakan ruang nama saja dan kelompokkan fungsi Anda menggunakannya.Anda kehilangan setidaknya dua opsi lagi yang tersedia untuk Anda di C ++:
Salah satunya adalah menggunakan injeksi dependensi 'statis' di mana dependensi Anda adalah parameter templat. Ini memberi Anda pilihan untuk memegang dependensi Anda berdasarkan nilai sambil tetap memungkinkan injeksi dependensi waktu kompilasi. Wadah STL menggunakan pendekatan ini untuk pengalokasi dan fungsi perbandingan dan hash misalnya.
Lain adalah untuk mengambil objek polimorfik dengan nilai menggunakan salinan yang dalam. Cara tradisional untuk melakukan ini adalah menggunakan metode klon virtual, opsi lain yang menjadi populer adalah dengan menggunakan tipe erasure untuk membuat tipe nilai yang berperilaku polimorfis.
Opsi mana yang paling tepat sangat tergantung pada use case Anda, sulit memberikan jawaban umum. Jika Anda hanya perlu polimorfisme statis meskipun saya akan mengatakan template adalah cara paling C ++ untuk pergi.
sumber
Anda mengabaikan jawaban keempat yang mungkin, yang menggabungkan kode pertama yang Anda posting ("hari baik" menyimpan anggota dengan nilai) dengan injeksi ketergantungan:
Kode klien kemudian dapat menulis:
Mewakili sambungan antara objek tidak boleh dilakukan (murni) pada kriteria yang Anda cantumkan (yaitu, tidak didasarkan murni pada "yang lebih baik untuk manajemen seumur hidup yang aman").
Anda juga dapat menggunakan kriteria konseptual ("mobil memiliki empat roda" vs. "mobil menggunakan empat roda yang harus dibawa oleh pengemudi").
Anda dapat memiliki kriteria yang dipaksakan oleh API lain (jika apa yang Anda dapatkan dari API adalah pembungkus khusus atau std :: unique_ptr misalnya, opsi dalam kode klien Anda juga terbatas).
sumber