C ++ 'da beni oldukça uzun bir süre rahatsız hissettiren tek bir şey var, çünkü dürüstçe basit görünsem bile nasıl yapılacağını bilmiyorum:
Fabrika yöntemini C ++ 'da doğru bir şekilde nasıl uygularım?
Amaç: istemcinin, kabul edilemez sonuçlar ve bir performans isabeti olmadan, nesnenin yapıcıları yerine fabrika yöntemlerini kullanarak bir nesneyi başlatmasını mümkün kılmak.
"Fabrika yöntemi kalıbı" ile, bir nesnenin içindeki statik fabrika yöntemlerini veya başka bir sınıfta tanımlanan yöntemleri veya genel işlevleri kastediyorum. Sadece genel olarak "X sınıfının normal örnekleme yolunu kurucudan başka bir yere yönlendirme kavramı".
Düşündüğüm bazı olası cevapları gözden geçirmeme izin verin.
0) fabrikalar yapmayın, yapıcılar yapmak.
Bu kulağa hoş geliyor (ve çoğu zaman en iyi çözüm), ancak genel bir çözüm değildir. Her şeyden önce, nesne inşasının başka bir sınıfa çıkarılmasını haklı çıkaracak bir görev kompleksi olduğu durumlar vardır. Ancak bu gerçeği bir kenara bırakmak, sadece kurucuları kullanan basit nesneler için bile çoğu zaman yapmaz.
Bildiğim en basit örnek 2-D Vector sınıfıdır. Çok basit, ama zor. Hem Kartezyen hem de kutupsal koordinatlardan inşa edebilmek istiyorum. Açıkçası yapamam:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Benim doğal düşünme şeklim şu:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Bu, inşaatçılar yerine beni statik fabrika yöntemlerinin kullanımına götürüyor ... bu da esasen fabrika modelini bir şekilde uyguladığım anlamına geliyor ("sınıf kendi fabrikası oluyor"). Bu güzel görünüyor (ve bu özel duruma uygun olacaktır), ancak bazı durumlarda başarısız olur. 2. noktada açıklayacağım.
başka bir durum: bazı API'ların (ilgisiz alanların GUID'leri veya bir GUID ve bit alanı gibi) iki opak tip tanımı ile aşırı yüklemeye çalışmak, anlamsal olarak tamamen farklı türler (yani - teoride - geçerli aşırı yükler) aynı şey - imzasız ints veya geçersiz işaretçiler gibi.
1) Java Yolu
Yalnızca dinamik olarak ayrılmış nesnelerimiz olduğu için Java'nın basit bir özelliği var. Bir fabrika yapmak şu kadar önemsiz:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
C ++ 'da bu şu anlama gelir:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Güzel? Genellikle. Ancak bu, kullanıcıyı yalnızca dinamik ayırmayı kullanmaya zorlar. Statik ayırma, C ++ 'ı karmaşık yapan şeydir, ancak aynı zamanda onu güçlü kılan şeydir. Ayrıca, dinamik ayırmaya izin vermeyen bazı hedefler (anahtar kelime: katıştırılmış) olduğuna inanıyorum. Ve bu, bu platformların kullanıcılarının temiz OOP yazmaktan hoşlandığı anlamına gelmez.
Her neyse, felsefe bir yana: Genel durumda, fabrika kullanıcılarını dinamik ayırmaya zorlanmaya zorlamak istemiyorum.
2) Değere göre dönüş
Tamam, bu yüzden dinamik ayırma istediğimizde 1) iyi olduğunu biliyoruz. Bunun üstüne neden statik ayırma eklemiyoruz?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Ne? Dönüş tipine göre aşırı yüklenemiyoruz? Tabii ki yapamayız. Öyleyse bunu yansıtacak yöntem adlarını değiştirelim. Ve evet, yukarıdaki yöntem kodunu değiştirme ihtiyacını ne kadar sevmediğimi vurgulamak için yukarıdaki geçersiz kod örneğini yazdım, örneğin, adları değiştirmek zorunda olduğumuz için, şu anda bir dil agnostik fabrika tasarımını düzgün bir şekilde uygulayamıyoruz - ve bu kodun her kullanıcısı, uygulamanın şartname ile arasındaki farkı hatırlamalıdır.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
Tamam ... işte burada. Yöntem adını değiştirmemiz gerektiğinden çirkin. Aynı kodu iki kez yazmamız gerektiğinden kusurlu. Ama bir kez yapıldığında, işe yarıyor. Sağ?
Genellikle. Ama bazen değil. Foo oluştururken, derleyicinin bizim için dönüş değeri optimizasyonunu yapmasına bağlıyız, çünkü C ++ standardı, derleyici satıcıları için nesnenin ne zaman yerinde oluşturulacağını ve ne zaman döndürülürken kopyalanacağını belirtmeyecek kadar yardımseverdir. C ++ 'da değere göre geçici nesne. Foo'nun kopyalanması pahalıysa, bu yaklaşım risklidir.
Ya Foo hiç kopyalanamazsa? Şey, doh. ( Garantili kopya seçimine sahip C ++ 17'de, kopyalanamaz olmanın yukarıdaki kod için artık sorun olmadığını unutmayın )
Sonuç: Bir nesneyi döndürerek bir fabrika yapmak gerçekten de bazı durumlarda (daha önce sözü edilen 2-D vektörü gibi) bir çözümdür, ancak yine de inşaatçılar için genel bir yedek değildir.
3) İki fazlı yapı
Birinin muhtemelen ortaya çıkaracağı başka bir şey, nesne tahsisi ve başlatılması konusunu ayırmaktır. Bu genellikle şöyle bir kodla sonuçlanır:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
Bir cazibe gibi çalıştığını düşünebilirsiniz. Kodumuzda ödediğimiz tek fiyat ...
Tüm bunları yazdığım ve son olarak bıraktığım için, ben de sevmemeliyim. :) Neden?
Her şeyden önce ... İki aşamalı inşaat kavramını içtenlikle sevmiyorum ve kullandığımda kendimi suçlu hissediyorum. "Varsa, geçerli durumda" iddiası ile benim nesneleri tasarlarsanız, benim kod daha güvenli ve daha az hata eğilimli olduğunu hissediyorum. Bu şekilde beğendim.
Bu sözleşmeyi bırakmak ve sadece fabrikayı yapmak amacıyla nesnemin tasarımını değiştirmek zorunda kaldım .. şey, hantal.
Yukarıdakilerin pek çok insanı ikna edemeyeceğini biliyorum, bu yüzden biraz daha sağlam argümanlar vereyim. İki fazlı yapı kullanarak şunları yapamazsınız:
- başlangıç
const
veya referans değişkenleri, - temel sınıf yapıcılarına ve üye nesne yapıcılarına argümanlar iletir.
Ve muhtemelen şu anda düşünemediğim bazı dezavantajlar olabilir ve yukarıdaki mermi noktaları beni zaten ikna ettiğinden özellikle mecbur hissetmiyorum bile.
Yani: bir fabrikayı uygulamak için iyi bir genel çözüme bile yakın değil.
Sonuç:
Aşağıdakileri yapacak bir nesne somutlaştırma yoluna sahip olmak istiyoruz:
- dağıtımdan bağımsız olarak tekdüze örneklemeye izin vermek,
- inşaat yöntemlerine farklı, anlamlı isimler verin (böylelikle by-argüman aşırı yüklenmesine dayanmayan),
- özellikle müşteri tarafında önemli bir performans isabeti ve tercihen önemli bir kod bloat isabeti sunmamak,
- Genel olarak, herhangi bir sınıf için tanıtılabilir.
Bahsettiğim yolların bu gereksinimleri karşılamadığını kanıtladığımı düşünüyorum.
İpucu var mı? Lütfen bana bir çözüm sağlayın, bu dilin bu kadar önemsiz bir konsepti doğru bir şekilde uygulamama izin vermeyeceğini düşünmek istemiyorum.
delete
. Bu tür yöntemler, çağıranın işaretçinin mülkiyetini aldığı (belgelendiğinde (kaynak kodu dokümantasyon ;-)) olduğu sürece (okundu: uygun olduğunda silmekten sorumludur) mükemmel bir şekilde uygundur.
unique_ptr<T>
yerine bir döndürerek çok açık yapabilirsiniz T*
.