C ++: Haruskah kelas memiliki atau mengamati dependensinya?

16

Katakanlah saya memiliki kelas Foobaryang menggunakan (tergantung pada) kelas Widget. Pada hari-hari baik, Widgetwolud 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 Foobardan Widgetbersama - sama. Solusinya tampaknya sederhana: gunakan Injeksi Ketergantungan untuk mengambil konstruksi dependensi di luar Foobarkelas. 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) {}
    (…)
}

Foobarmengklaim kepemilikan satu-satunya Widgetyang diberikan padanya. Ini memiliki keuntungan sebagai berikut:

  1. Dampak pada kinerja dapat diabaikan.
  2. Ini aman, karena Foobarmengontrol masa pakai Widget, jadi itu memastikan bahwa Widgettidak tiba-tiba menghilang.
  3. Aman Widgettidak akan bocor dan akan dihancurkan dengan benar ketika tidak lagi dibutuhkan.

Namun, ini harus dibayar:

  1. Ini menempatkan pembatasan pada bagaimana Widgetinstance dapat digunakan, misalnya tidak ada stack-dialokasikan Widgetsdapat digunakan, tidak Widgetdapat 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:

  1. Lebih universal, karena memungkinkan berbagi dependensi.
  2. Menjaga keamanan (poin 2 dan 3) dari unique_ptrsolusi.

Kekurangan:

  1. Menghabiskan sumber daya saat tidak ada pembagian yang terlibat.
  2. 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:

  1. Sesederhana itu bisa.
  2. Universal, menerima sembarang saja Widget.

Cons:

  1. Tidak aman lagi.
  2. Memperkenalkan entitas lain yang bertanggung jawab atas kepemilikan keduanya Foobardan Widget.

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, Foobarsbagaimanapun juga, ada sesuatu yang harus dikelola , jadi mengelola Widgetsadalah 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?

el.pescado
sumber
1
Sejak awal, jika Anda dapat mengkompilasi di bawah C ++ 11 dan / atau tidak pernah, memiliki pointer polos sebagai cara untuk menangani sumber daya di kelas bukan cara yang disarankan lagi. Jika kelas Foobar adalah satu-satunya pemilik sumber daya dan sumber daya harus dibebaskan, ketika Foobar keluar dari ruang lingkup, std::unique_ptradalah cara untuk pergi. Anda dapat menggunakan std::move()untuk mentransfer kepemilikan sumber daya dari ruang lingkup atas ke kelas.
Andy
1
Bagaimana saya tahu itu Foobarsatu-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.
el.pescado
1
@ el.pescado Ada juga masalah yang sama di Jawa, memori bukan satu-satunya sumber daya. Ada juga file dan koneksi misalnya. Cara biasa untuk mengatasi ini di Jawa adalah memiliki wadah yang mengelola siklus hidup semua objek yang terkandung. Oleh karena itu Anda tidak perlu khawatir tentang itu di benda-benda yang terkandung dalam wadah DI. Ini juga bisa berfungsi untuk aplikasi c ++ jika ada cara bagi wadah DI untuk mengetahui kapan harus beralih ke fase siklus hidup mana.
SpaceTrucker
3
Tentu saja, Anda bisa melakukannya dengan cara kuno tanpa semua kerumitan dan memiliki kode yang berfungsi dan cara ini lebih mudah untuk dipelihara. (Perhatikan bahwa anak-anak lelaki Jawa mungkin tertawa, tetapi mereka selalu membicarakan tentang refactoring, jadi memisahkan diri tidak banyak membantu mereka)
gbjbaanb
3
Ditambah lagi membuat orang lain menertawakan Anda bukan alasan untuk menjadi seperti mereka. Dengarkan argumen mereka (selain "karena kami disuruh") dan terapkan DI jika itu membawa manfaat nyata bagi proyek Anda. Dalam kasus ini, pikirkan pola sebagai aturan yang sangat umum, yang harus Anda terapkan dengan cara yang spesifik untuk proyek - ketiga pendekatan (dan semua pendekatan potensial lainnya yang belum Anda pikirkan) valid diberikan mereka membawa manfaat secara keseluruhan (yaitu memiliki lebih banyak sisi positif daripada sisi buruknya).

Jawaban:

2

Saya bermaksud menulis ini sebagai komentar, tetapi ternyata terlalu lama.

Bagaimana saya tahu itu Foobarsatu-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.

Apakah Anda harus menggunakan std::unique_ptr<Widget>atau std::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, seperti Foobar. Mengikuti prinsip DI, Anda perlu Widgetinstance, untuk menyuntikkannya menggunakan Foobarkonstruktor, artinya di dalam salah satu Utilities::Factorymetode, misalnya createWidget(const std::vector<std::string>& params), Anda membuat Widget dan menyuntikkannya ke Foobarobjek.

Sekarang Anda memiliki Utilities::Factorymetode, yang menciptakan Widgetobjek. 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 Foobarkelas, jadi sebenarnya Foobarbertindak seperti pengontrol.

Pengontrol mungkin akan menggunakan sebagian milik Anda Widgetdan 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 Widgetkhusus yang terikat di sini, artinya tidak akan ditampilkan di tempat lain. Jadi, jika Anda menutup jendela, Anda tidak perlu lagi di Widgetmana 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::movepada std::unique_ptrbenar-benar mentransfer kepemilikan, sehingga bahkan jika Anda membuat object Ametode dan menyebarkannya ke yang lain object Bsebagai dependensi, yang object Bsekarang akan bertanggung jawab untuk sumber daya object Adan benar akan menghapusnya, ketika object Bkeluar dari ruang lingkup.

Andy
sumber
Gagasan umum tentang kepemilikan dan petunjuk pintar jelas bagi saya. Bagiku sepertinya tidak cocok dengan ide DI. Dependency Injection, bagi saya, tentang memisahkan kelas dari dependensinya, namun dalam hal ini kelas masih mempengaruhi bagaimana dependensinya dibuat.
el.pescado
Misalnya, masalah yang saya miliki unique_ptradalah pengujian unit (DI diiklankan sebagai ramah pengujian): Saya ingin menguji Foobar, jadi saya membuat Widget, meneruskannya Foobar, berolahraga Foobardan kemudian saya ingin memeriksanya Widget, tetapi kecuali Foobarentah bagaimana memaparkannya, saya dapat sejak itu telah diklaim oleh Foobar.
el.pescado
@ el.pescado Anda mencampur dua hal menjadi satu. Anda mencampur aksesibilitas dan kepemilikan. Hanya karena Foobarmemiliki sumber daya tidak berarti, tidak ada orang lain yang harus menggunakannya. Anda bisa menerapkan Foobarmetode seperti Widget* 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 menguji Widgetkelas.
Andy
Poin bagus, itu masuk akal.
el.pescado
1
Jika Anda mengonversi shared_ptr ke unique_ptr, bagaimana Anda ingin menjamin unique_ness ???
Aconcagua
2

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_ptradalah jawaban yang benar.

Dominique McDonnell
sumber
1

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':

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

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).

Aconcagua
sumber
1
Dalam C ++, jangan gunakan classes 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.
Andy
@ DavidvidPacker Hmm, tentu saja, Anda benar - Saya diam-diam mengasumsikan kelas memiliki internal pribadi, tapi itu tidak terlihat atau disebutkan di tempat lain ...
Aconcagua
1

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.

mattnewport
sumber
0

Anda mengabaikan jawaban keempat yang mungkin, yang menggabungkan kode pertama yang Anda posting ("hari baik" menyimpan anggota dengan nilai) dengan injeksi ketergantungan:

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

Kode klien kemudian dapat menulis:

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

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).

utnapistim
sumber
1
Ini menghalangi perilaku polimorfik, yang sangat membatasi nilai tambah.
el.pescado
Benar. Saya hanya berpikir untuk menyebutkannya karena ini adalah bentuk yang paling saya gunakan (saya hanya menggunakan pewarisan dalam pengkodean untuk perilaku polimorfik - bukan kode yang digunakan kembali, jadi saya tidak punya banyak kasus yang memerlukan perilaku polimorfik) ..
utnapistim