C ++: Sınıf bağımlılıklarına sahip olmalı veya bağımlılıklarına dikkat etmeli mi?


16

Diyelim ki sınıfı Foobarkullanan (bağlı) bir sınıfım var Widget. İyi günlerde, polimorfik davranış gerekiyorsa Widgetwolud bir alan olarak Foobarveya belki de akıllı bir işaretçi olarak ilan edilir ve yapıcıda başlatılır:

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)) {}
    (…)
};

Ve hepimiz hazır olurduk. Ne yazık ki, bugün Java çocuklar gördükten sonra bize ve çiftler Foobarve Widgetbirlikte haklı olarak gülecekler . Çözüm basit görünüyor: Bağımlılıkların inşasını Foobarsınıftan çıkarmak için Bağımlılık Enjeksiyonu uygulayın . Ama sonra, C ++ bizi bağımlılıkların sahibi olmayı düşünmeye zorlar. Üç çözüm akla geliyor:

Benzersiz işaretçi

class Foobar {
    std::unique_ptr<Widget> widget;
    public:
    Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
    (…)
}

Foobarsadece mülkiyetin Widgetkendisine geçildiğini iddia ediyor . Bunun aşağıdaki avantajları vardır:

  1. Performans üzerindeki etkisi ihmal edilebilir düzeydedir.
  2. FoobarKontroller ömür boyu güvenli Widgetolduğundan, Widgetaniden kaybolmadığından emin olur .
  3. WidgetSızıntı yapmayacak ve artık ihtiyaç duyulmadığında düzgün şekilde tahrip edilecek güvenlidir .

Ancak, bunun bir maliyeti vardır:

  1. WidgetÖrneklerin nasıl kullanılabileceği konusunda kısıtlamalar getirir, örneğin yığın tahsis Widgetsedilemez, Widgetpaylaşılamaz.

Paylaşılan işaretçi

class Foobar {
    std::shared_ptr<Widget> widget;
    public:
    Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
    (…)
}

Bu muhtemelen Java ve çöp toplanan diğer dillere en yakın eşdeğerdir. Avantajları:

  1. Bağımlılık paylaşımına izin verdiği için daha evrensel.
  2. unique_ptrÇözeltinin güvenliğini (madde 2 ve 3) korur .

Dezavantajları:

  1. Paylaşım olmadığında kaynakları israf eder.
  2. Yine de yığın ayırma gerektirir ve yığınla ayrılmış nesneleri devre dışı bırakır.

Düz ol 'gözlem işaretçisi

class Foobar {
    Widget *widget;
    public:
    Foobar(Widget *w) : widget(w) {}
    (…)
}

Ham işaretçiyi sınıfın içine yerleştirin ve sahip olma yükünü bir başkasına kaydırın. Artıları:

  1. Olabildiğince basit.
  2. Universal, sadece herhangi birini kabul eder Widget.

Eksileri:

  1. Artık güvenli değil.
  2. Her ikisinin Foobarve sahipliğinden sorumlu olan başka bir varlık tanıtır Widget.

Bazı çılgın şablon meta programlaması

Düşünebileceğim tek avantaj, yazılımım derlenirken zaman bulamadığım tüm kitapları okuyabileceğim;)

Üçüncü çözüme yaslanıyorum, en evrensel olduğu gibi, bir şey Foobarsyine de yönetmek zorunda , bu yüzden yönetmek Widgetsbasit bir değişiklik. Ancak, ham işaretçiler kullanmak beni rahatsız ediyor, diğer taraftan akıllı işaretçi çözümü bana yanlış geliyor, çünkü bağımlılık tüketicisini bu bağımlılığın nasıl yaratıldığını kısıtlıyorlar.

Bir şey mi kaçırıyorum? Yoksa sadece C ++ bağımlılık enjeksiyon önemsiz değil mi? Sınıf bağımlılıklarına mı sahip olmalı yoksa sadece gözlemlemeli mi?


1
En başından itibaren, C ++ 11 altında derleme yapabiliyorsanız ve / veya asla, bir sınıftaki kaynakları işlemenin bir yolu olarak düz işaretçiye sahip olmak artık önerilen yol değildir. Foobar sınıfı kaynağın tek sahibiyse ve kaynağın serbest bırakılması gerekiyorsa, Foobar kapsam dışına çıktığında std::unique_ptrgidilecek yoldur. std::move()Bir kaynağın sahipliğini üst kapsamdan sınıfa aktarmak için kullanabilirsiniz .
Andy

1
Bunun Foobartek sahibi olduğunu nasıl bilebilirim ? Eski durumda basit. Ancak DI ile ilgili sorun, gördüğüm gibi, sınıfı bağımlılıklarının inşasından ayırdığı için, aynı zamanda bu bağımlılıkların sahipliğinden de ayrıştırmasıdır (sahiplik inşaata bağlı olduğu için). Java gibi çöp toplanmış ortamlarda bu bir sorun değildir. C ++ 'da, bu.
el.pescado

1
@ el.pescado Java'da da aynı sorun var, bellek tek kaynak değil. Örneğin dosyalar ve bağlantılar da vardır. Java'da bunu çözmenin olağan yolu, içerdiği tüm nesnelerin yaşam döngüsünü yöneten bir konteynere sahip olmaktır. Bu nedenle bir DI kabında bulunan nesneler için endişelenmenize gerek yoktur. DI kapsayıcısının hangi yaşam döngüsü fazına ne zaman geçeceğini bilmesi için bir yol varsa, bu aynı zamanda c ++ uygulamaları için de işe yarayabilir.
SpaceTrucker

3
Elbette, tüm karmaşıklık olmadan eski moda bir şekilde yapabilir ve çalışan ve bakımı çok daha kolay bir koda sahip olabilirsiniz. (Java erkeklerinin gülebileceğini unutmayın, ancak her zaman yeniden düzenleme hakkında devam ediyorlar, bu yüzden ayırma tam olarak onlara çok fazla yardımcı olmuyor)
gbjbaanb

3
Ayrıca başkalarının size gülmesini sağlamak onlar gibi olmanın bir nedeni değildir. Onların argümanlarını dinleyin ("bize söylendiğinden" hariç) ve projenize somut bir fayda sağlıyorsa DI'yi uygulayın. Bu durumda, deseni projeye özgü bir şekilde uygulamak zorunda olduğunuz çok genel bir kural olarak düşünün - üç yaklaşımın hepsi (ve henüz düşünmediğiniz diğer tüm potansiyel yaklaşımlar) geçerlidir genel bir fayda sağlarlar (yani olumsuz yanlardan daha fazla olumsuzlukları vardır).

Yanıtlar:


2

Bunu bir yorum olarak yazmak istedim, ama çok uzun olduğu ortaya çıktı.

Bunun Foobartek sahibi olduğunu nasıl bilebilirim ? Eski durumda basit. Ancak DI ile ilgili sorun, gördüğüm gibi, sınıfı bağımlılıklarının inşasından ayırdığı için, aynı zamanda bu bağımlılıkların sahipliğinden de ayrıştırmasıdır (sahiplik inşaata bağlı olduğu için). Java gibi çöp toplanmış ortamlarda bu bir sorun değildir. C ++ 'da, bu.

Kullanmanız gereken İster std::unique_ptr<Widget>ya std::shared_ptr<Widget>, o karar vermek size kalmış ve işlevsellik geliyor.

Diyelim Utilities::Factoryki, bloklarınızın oluşturulmasından sorumlu olan bir Foobar. DI prensibini Widgetizleyerek , örneğin Foobar'yapıcısını kullanarak enjekte etmek için örneğe ihtiyacınız olacak, örneğin Utilities::Factory' yöntemlerinden birinin içine , örneğin createWidget(const std::vector<std::string>& params), Widget'ı yaratıp Foobarnesneye enjekte edersiniz .

Şimdi nesneyi Utilities::Factoryoluşturan bir yöntem var Widget. Bunun anlamı, silinmesinden sorumlu olan yöntem mi? Tabii ki değil. Seni sadece örnek yapmak için orada.


Hayal edelim, birden fazla pencereye sahip bir uygulama geliştiriyorsunuz. Her pencere Foobarsınıf kullanılarak temsil edilir , bu yüzden aslında Foobarbir denetleyici gibi davranır.

Denetleyici muhtemelen bazı cihazlarınızdan faydalanır Widgetve kendinize şunu sormanız gerekir:

Uygulamamda bu pencereye gidersem, bu Widget'lara ihtiyacım olacak. Bu widget'lar diğer uygulama pencereleri arasında paylaşılıyor mu? Eğer öyleyse, muhtemelen hepsini bir daha yaratmamalıyım, çünkü her zaman aynı görünecekler, çünkü paylaşılıyorlar.

std::shared_ptr<Widget> gitmek için bir yoldur.

Ayrıca, Widgetyalnızca bu pencereye özel olarak bağlı bir uygulama pencereniz de vardır , yani başka bir yerde görüntülenmez. Pencereyi kapatırsanız, Widgetartık uygulamanızda herhangi bir yere veya en azından örneğine ihtiyacınız yoktur.

İşte bu noktada std::unique_ptr<Widget>onun tahtını iddia geliyor.


Güncelleme:

Yaşam boyu sorun hakkında @DominicMcDonnell ile gerçekten aynı fikirde değilim . Arama std::moveüzerinde std::unique_ptrtamamen sahipliğini aktarır Bir yaratmak böylece bile, object Abir yöntemde ve başka geçmek object Bbir bağımlılık olarak, object Bşimdi kaynak için sorumlu olacak object Ave ne zaman doğru, onu silecektir object Bkapsam dışına gider.


Genel mülkiyet ve akıllı işaretçiler fikri benim için açık. DI fikri ile iyi oynamak bana öyle gelmiyor. Bağımlılık Enjeksiyonu benim için sınıfı bağımlılıklarından ayırmakla ilgilidir, ancak bu durumda sınıf hala bağımlılıklarının nasıl yaratıldığını etkiler.
el.pescado

Örneğin, yaşadığım sorun unique_ptrbirim test (DI test dostu olarak tanıtıldı): Test etmek istiyorum Foobar, bu yüzden oluşturmak , egzersiz Widgetyapmak Foobar, egzersiz yapmak Foobarve sonra incelemek istiyorum Widget, ancak bir Foobarşekilde ortaya çıkarmadıkça, iddia edildiğinden beri Foobar.
el.pescado

@ el.pescado İki şeyi birlikte karıştırıyorsunuz. Erişilebilirliği ve sahipliği karıştırıyorsunuz. Çünkü Foobarortalama değil, başka kimse kullanmalıdır gelmez kaynağın sahibi. Çalışabileceğiniz ham işaretçiyi döndürecek Foobargibi bir yöntem uygulayabilirsiniz Widget* getWidget() const { return this->_widget.get(); }. Daha sonra Widgetsınıfı test etmek istediğinizde bu yöntemi birim testleriniz için bir girdi olarak kullanabilirsiniz .
Andy

İyi bir nokta, mantıklı.
el.pescado

1
Paylaşılan_ptr değerini unique_ptr biçimine dönüştürdüyseniz, unique_ness'i nasıl garanti etmek istersiniz ???
Aconcagua

2

Referans şeklinde gözlemci bir işaretçi kullanırdım. Kullandığınız zaman çok daha iyi bir sözdizimi verir ve düz bir işaretçinin sahipliğini ima etmediği anlamsal avantaja sahiptir.

Bu yaklaşımla ilgili en büyük sorun yaşamlardır. Bağımlılığın daha önce yapılandırıldığından ve bağımlı sınıfınızdan sonra yıkıldığından emin olmalısınız. Bu basit bir sorun değil. Paylaşılan işaretçiler kullanmak (bağımlılık depolaması ve ona bağlı tüm sınıflarda, yukarıdaki seçenek 2) bu sorunu ortadan kaldırabilir, ancak aynı zamanda önemsiz olmayan ve bence daha az belirgin ve bu nedenle daha zor olan dairesel bağımlılıklar sorununu da ortaya koyar. sorunlara neden olmadan önce tespit edin. Bu yüzden yaşamları ve inşaat sırasını otomatik ve manuel olarak yapmamayı tercih ediyorum. Ayrıca, nesnelerin bir listesini oluşturuldukları sırayla oluşturan ve onları ters sırayla yok eden hafif bir şablon yaklaşımı kullanan sistemleri gördüm, kusursuz değildi, ama işleri çok daha basit hale getirdi.

Güncelleme

David Packer'ın yanıtı bu soru hakkında biraz daha düşünmemi sağladı. Orijinal cevap, bağımlılık enjeksiyonunun avantajlarından biri olan deneyimimdeki paylaşılan bağımlılıklar için geçerlidir, bir bağımlılığın bir örneğini kullanarak birden fazla örneğiniz olabilir. Bununla birlikte, sınıfınızın belirli bir bağımlılığın kendi örneğine sahip olması gerekiyorsa std::unique_ptr, doğru cevaptır.


1

Her şeyden önce - bu C ++ değil, Java değil - ve burada birçok şey farklı gidiyor. Java çalışanları sahiplikle ilgili bu tür sorunlara sahip değildir, çünkü bunları kendileri çözen otomatik çöp toplama vardır.

İkincisi: Bu sorunun genel bir cevabı yok - gereksinimlerin ne olduğuna bağlı!

FooBar ve Widget'ı birleştirmeyle ilgili sorun nedir? FooBar bir Widget kullanmak istiyor ve her FooBar örneği her zaman kendi ve aynı Widget'a sahip olacaksa, onu bağlı bırakın ...

C ++ 'da, Java'da bulunmayan "garip" şeyler bile yapabilirsiniz, örneğin varyasyonlu bir şablon oluşturucuya sahip olabilirsiniz (Java'da, yapıcılarda da kullanılabilecek bir ... gösterim vardır, ancak bu sadece gerçek varyasyon şablonları ile ilgisi olmayan bir nesne dizisini gizlemek için sözdizimsel şeker!) - kategori 'Bazı deli şablon meta programlaması':

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

Beğendiniz mi?

Tabii ki, orada olan örneğin herhangi filanca örneğini isterseniz, var veya widget, ..., veya sadece yeniden ihtiyaç kadar önce bir defada widget oluşturmak gerekiyorsa - İstediğiniz nedenler ya da her iki sınıfları ayrıştırma ihtiyacı çünkü mevcut sorun için daha uygundur (örneğin, Widget bir GUI öğesiyse ve FooBar bir / olamaz / olmamalıdır).

Sonra ikinci noktaya dönüyoruz: Genel cevap yok. Karar vermelisiniz, - asıl sorun için - daha uygun çözüm. DominicMcDonnell'in referans yaklaşımını seviyorum, ancak yalnızca sahiplik FooBar tarafından alınmayacaksa uygulanabilir (aslında, aslında, ama bu çok, çok kirli bir kod anlamına gelir ...). Bunun dışında David Packer'in cevabına katılıyorum (cevap olarak yazılmak istedim - ama yine de iyi bir cevap).


1
C ++ 'da, classörneğin örneğinizde olduğu gibi bir fabrika olsa bile, statik yöntemlerden başka bir şey kullanmayın . OO ilkelerini ihlal eder. Bunun yerine ad alanlarını kullanın ve işlevlerinizi kullanarak bunları gruplandırın.
Andy

@DavidPacker Hmm, tabi, haklısın - Sınıfın sessizce bazı özel iç kısımlara sahip olduğunu varsaydım, ama bu ne görünür ne de başka bir yerde bahsedilmedi ...
Aconcagua

1

C ++ ile kullanabileceğiniz en az iki seçenek daha eksik:

Bunlardan biri, bağımlılıklarınızın şablon parametreleri olduğu 'statik' bağımlılık enjeksiyonunu kullanmaktır. Bu, derleme süresi bağımlılığı enjeksiyonuna izin verirken bağımlılıklarınızı değere göre tutma seçeneği sunar. STL kapları, bu yaklaşımı örneğin ayırıcılar ve karşılaştırma ve sağlama işlevleri için kullanır.

Bir diğeri, polimorfik nesneleri derin bir kopya kullanarak değere göre almaktır. Bunu yapmanın geleneksel yolu sanal bir klon yöntemi kullanmaktır, popüler hale gelen başka bir seçenek de polimorfik davranan değer türlerini yapmak için tür silme yöntemini kullanmaktır.

Hangi seçeneğin en uygun olduğu gerçekten kullanım durumunuza bağlıdır, genel bir cevap vermek zordur. Şablonlar gitmek için en C ++ yolu olduğunu söyleyebilirim ama sadece statik polimorfizm gerekiyorsa.


0

Gönderdiğiniz ilk kodu (bir üyeyi değere göre saklamanın "iyi ol 'günleri") bağımlılık enjeksiyonuyla birleştiren dördüncü olası bir cevabı göz ardı ettiniz:

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

Müşteri kodu daha sonra şunu yazabilir:

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

Nesneler arasındaki bağlantıyı temsil etmek (tamamen) listelediğiniz kriterlere göre yapılmamalıdır (yani, sadece "güvenli yaşam boyu yönetimi için daha iyi" temeline dayanmamalıdır).

Kavramsal ölçütleri de kullanabilirsiniz ("bir otomobilin dört tekerleği vardır" vs. "bir otomobil sürücünün getirmesi gereken dört teker kullanır").

Diğer API'lar tarafından uygulanan ölçütleriniz olabilir (bir API'dan aldığınız özel bir sarıcı veya örneğin bir std :: unique_ptr ise, istemci kodunuzdaki seçenekler de sınırlıdır).


1
Bu, katma değeri ciddi şekilde sınırlayan polimorfik davranışı engeller.
el.pescado

Doğru. Ben sadece (ben polimorfik davranış için kodlamada sadece miras kullanıyorum - kod yeniden kullanımı değil, bu yüzden ben polimorfik davranışa ihtiyaç duyan birçok durumda yok) çünkü ben formu sonunda olduğundan söz düşündüm ..
utnapistim
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.