Yazıldığı gibi, "kokuyor" ama bu sadece verdiğiniz örnekler olabilir. Verileri genel nesne konteynerlerinde saklamak, daha sonra verilere erişmek için yayın yapmak otomatik olarak kod kokusu değildir . Birçok durumda kullanıldığını göreceksiniz. Ancak, onu kullanırken, ne yaptığınızı, nasıl yaptığınızı ve nedenini bilmelisiniz. Örneğe baktığımda, bana hangi nesnenin ne olduğunu söylemek için dizge bazlı karşılaştırmaların kullanılması, kişisel koku sayacımı ne kadar tetikler ki. Burada ne yaptığınızdan tam olarak emin olmadığınızı gösterir (bu iyi, çünkü programcılar için buraya gelmek konusunda bilgeliğiniz var. " beni dışarı! ").
Bunun gibi jenerik konteynerlerden veri dökümü modelinin temel sorunu, verinin üreticisi ve verinin tüketicisinin birlikte çalışması gerektiğidir, ancak ilk bakışta yaptıkları açık olmayabilir. Bu paternin her örneğinde, koklamak ya da koklamak değil, temel sorun budur. Bir sonraki geliştiricinin bu kalıbı yaptığınızdan habersiz olması ve kazara kırması çok olasıdır, bu yüzden bu kalıbı kullanırsanız bir sonraki geliştiriciye yardım etmeye özen göstermelisiniz. Var olmadığını bilmesi gereken bazı detaylar nedeniyle, istemeden kodları kırmamasını kolaylaştırmanız gerekir.
Örneğin, bir oyuncu kopyalamak istersem? Sadece oynatıcı nesnesinin içeriğine bakarsam, oldukça kolay görünüyor. Sadece kopyalamak zorunda attack
, defense
ve tools
değişkenleri. Pasta kadar kolay! İşaretçileri kullanımınızın onu biraz zorlaştırdığını çabucak öğreneceğim (bir noktada akıllı işaretçilere bakmaya değer, ama bu başka bir konu). Bu kolayca çözüldü. Her aracın yeni kopyalarını oluşturacağım ve bunları yeni tools
listeme koyacağım . Ne de olsa, Tool
sadece bir üyeyle gerçekten basit bir sınıftır. Bu yüzden bir kopyasını da içeren bir sürü kopya oluşturdum Sword
, ama bunun bir kılıç olduğunu bilmiyordum, o yüzden sadece kopyaladım name
. Daha sonra, attack()
fonksiyon isme bakar, bir "kılıç" olduğunu görür, atar ve kötü şeyler olur!
Bu örneği, aynı modeli kullanan soket programlamasında başka bir vaka ile karşılaştırabiliriz. Bunun gibi bir UNIX soket işlevi kurabilirim:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
Neden bu aynı kalıp? Çünkü bind
a kabul etmiyor sockaddr_in*
, daha genel kabul ediyor sockaddr*
. Bu sınıfların tanımlarına bakarsanız, * sockaddr
ailesine yalnızca bir üyemizin atandığını görüyoruz sin_family
. Aile, hangi alt türü kullanmanız gerektiğini söylüyor sockaddr
. AF_INET
Adres yapısının gerçekte bir olduğunu söyler sockaddr_in
. Öyle AF_INET6
olsaydı, adres sockaddr_in6
daha büyük IPv6 adreslerini desteklemek için daha geniş alanlara sahip bir a olurdu .
Bu, Tool
örneğinizle aynıdır , ancak hangi aileyi a'dan ziyade belirlemek için bir tam sayı kullanır std::string
. Ancak, koku almadığını iddia edeceğim ve bunu "soket yapmanın standart bir yolu, bu yüzden" kokmamalı "dı. Nedenler dışında yapmaya çalışacağım. neden genel nesnelerde veri depolamanın ve onu yayınlamanın otomatik olarak kod kokusu olmadığını iddia ediyorum , ancak nasıl güvenli hale getireceklerini yapmada bazı farklılıklar var.
Bu modeli kullanırken, en önemli bilgi, alt sınıfla ilgili bilgilerin üreticiden tüketiciye aktarımını yakalamaktır. Bu, name
alanla yaptığınız şeydir ve UNIX soketleri kendi sin_family
alanlarıyla yapar. Bu alan, üreticinin gerçekte ne yarattığını anlamak için tüketicinin ihtiyaç duyduğu bilgidir. Olarak tüm bu desen durumlarda, bir numaralandırma olmalıdır (ya da en azından, bir tam sayıdır, bir numaralandırma gibi hareket). Niye ya? Tüketicinizin bilgilerle ne yapacağını düşünün. Büyük bir if
açıklama yazmaları gerekecek ya daswitch
deyim, yaptığınız gibi, doğru alt türü belirledikleri yerde, onu kullanıp verileri kullanmaktadır. Tanım olarak, bu türlerden yalnızca az sayıda olabilir. Yaptığınız gibi bir dizgide saklayabilirsiniz, ancak bunun birçok dezavantajı vardır:
- Yavaş -
std::string
dizgiyi korumak için tipik olarak biraz dinamik bellek gerekir. Ayrıca, hangi alt sınıfa sahip olduğunuzu bulmak istediğinizde, adı eşleştirmek için tam metin karşılaştırması yapmanız gerekir.
- Çok yönlü - Aşırı derecede tehlikeli bir şey yaparken kendinize kısıtlamalar koymak için söylenecek bir şey var. Ne tür bir nesneye baktığını anlatmak için bir alt dize arayan bu tür sistemlere sahibim . Bu , alt nesneyi yanlışlıkla bu alt dizgiyi içeren ve korkunç şifreli bir hata oluşturana kadar çok çalıştı . Yukarıda da belirttiğimiz gibi, sadece az sayıda vakaya ihtiyacımız olduğundan, dizgiler gibi devasa bir şekilde güçlendirilmiş bir araç kullanmak için hiçbir neden yoktur. Bu yol açar ...
- Hata eğilimli - Diyelim ki, bir tüketici yanlışlıkla bir sihirli bez adını koyduğunda işlerin neden işe yaramadığını ayıklamaya çalışan cinayet dolu bir öfkeye gitmek isteyeceksiniz
MagicC1oth
. Ciddi, böyle hatalar alabilir gün ne olduğunu fark etmeden önce kafa kaşıma.
Bir numaralandırma çok daha iyi çalışıyor. Hızlı, ucuz ve daha az hata eğilimli:
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
std::string typeName() const {
switch(type) {
case kSword: return "Sword";
case kSheild: return "Sheild";
case kMagicCloth: return "Magic Cloth";
default:
throw std::runtime_error("Invalid enum!");
}
}
};
Bu örnek ayrıca, switch
bu kalıbın en önemli kısmı olan enums ile ilgili bir ifadeyi gösterir: fırlayan bir default
durum. İşleri mükemmel yaparsan , asla böyle bir duruma girmemelisin. Ancak, birisi yeni bir araç türü eklerse ve kodunuzu desteklemek için güncellemeyi unutursanız, hatayı yakalamak için bir şey istersiniz. Aslında, onlara ihtiyacım olmasa bile onları eklemeniz gerekenleri tavsiye ederim.
Bunun diğer büyük avantajı enum
, bir sonraki geliştiriciye, tam olarak geçerli araç tiplerinin tam bir listesini vermesidir. Bob'un epik patron savaşında kullandığı uzman Flüt sınıfını bulmak için koda girmeye gerek yok.
void damageWargear(Tool* tool)
{
switch(tool->type)
{
case Tool::kSword:
static_cast<Sword*>(tool)->damageSword();
break;
case Tool::kShield:
static_cast<Sword*>(tool)->damageShield();
break;
default:
break; // Ignore all other objects
}
}
Evet, "boş" bir varsayılan ifade koydum, yalnızca yeni bir beklenmeyen tür benim için geldiğinde ne olacağını umduğum bir sonraki geliştiriciye açık yapmak için.
Bunu yaparsanız, desen daha az kokacak. Ancak, kokusuz olmak için yapmanız gereken son şey diğer seçenekleri göz önünde bulundurmaktır. Bu yayınlar, C ++ repertuarında sahip olduğunuz daha güçlü ve tehlikeli araçlardan bazılarıdır. İyi bir nedeniniz yoksa, onları kullanmamalısınız.
Çok popüler bir alternatif, "sendika yapısı" veya "sendika sınıfı" dediğim şey. Örneğin, bu aslında çok iyi bir seçim olacaktır. Bunlardan birini yapmak için, daha Tool
önce olduğu gibi bir numaralandırma ile bir sınıf yaratırsınız, ancak alt sınıflandırma yerine, Tool
tüm alanları sadece her alt tipten koyarız.
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
int attack;
int defense;
};
Şimdi hiç alt sınıfa ihtiyacınız yok. type
Hangi alanların gerçekte geçerli olduğunu görmek için alana bakmanız yeterlidir. Bu çok daha güvenli ve anlaşılması daha kolay. Ancak, dezavantajları vardır. Bunu kullanmak istemediğin zamanlar vardır:
- Nesneler birbirinden çok farklı olduğunda - Bir çamaşırhane listesi ile sonuçlanabilir ve hangilerinin her nesne türü için geçerli olduğu belirsiz olabilir.
- Hafıza kritik bir durumda çalışırken - 10 araç yapmanız gerekiyorsa, hafızayla tembel olabilirsiniz. 500 milyon araç yapmanız gerektiğinde, bit ve baytları önemsemeye başlayacaksınız. Sendika yapıları her zaman olması gerekenden daha büyüktür.
Bu çözüm, API'nin açık uçluluğu ile birleştirilen farklılık sorunu nedeniyle UNIX soketleri tarafından kullanılmaz. UNIX soketlerinin amacı, UNIX'in her lezzetinin işe yarayabileceği bir şey yaratmaktı. Her lezzet, destekledikleri ailelerin listesini tanımlayabilir, beğenir AF_INET
ve her biri için kısa bir liste olur. Bununla birlikte, yeni bir protokol gelirse, olduğu gibi AF_INET6
, yeni alanlar eklemeniz gerekebilir. Bunu bir sendika yapısıyla yaptıysanız, yapının yeni bir sürümünü aynı ada sahip yeni bir sürüm oluşturup bitmeyen uyumsuzluk sorunları yaratabileceksiniz. Bu nedenle UNIX soketleri, birleşim yapısı yerine döküm kalıbını kullanmayı seçti. Bunu düşündüklerinden eminim ve bunu düşündükleri gerçeği, kullandıklarında koku almadıklarının bir parçası.
Bir sendikayı da gerçek anlamda kullanabilirsiniz. Sendikalar, yalnızca en büyük üye kadar büyük olmakla hafızayı korurlar, ancak kendi sorunlarıyla gelirler. Bu muhtemelen kodunuz için bir seçenek değildir, ancak her zaman göz önünde bulundurmanız gereken bir seçenek.
Başka bir ilginç çözüm boost::variant
. Boost , yeniden kullanılabilir çapraz platform çözümleriyle dolu harika bir kütüphanedir. Muhtemelen şimdiye kadar yazılmış en iyi C ++ kodlarından bazıları. Boost.Variant , temelde sendikaların C ++ versiyonudur. Birçok farklı tipte olabilen, ancak bir seferde yalnızca bir tanesi bulunan bir kaptır. Sen yapabilir Sword
, Shield
ve MagicCloth
daha sonra aracı bir olmak yapmak, sınıfları boost::variant<Sword, Shield, MagicCloth>
bu o üç türden birini ihtiva anlam. Bu, UNIX soketlerinin kullanılmasını önleyen gelecekteki uyumluluk ile aynı sorundan muzdariptir (UNIX soketlerinin C olduğundan bahsetmeden değilboost
biraz tarafından!), ama bu desen inanılmaz derecede yararlı olabilir. Varyant, örneğin, bir metin dizisi alan ve kurallar için bir gramer kullanarak ayrılan ayrıştırma ağaçlarında sıklıkla kullanılır.
Bir dalma yapmadan ve genel nesne döküm yaklaşımını kullanmadan önce bakmayı önerdiğim son çözüm Ziyaretçi tasarım deseni. Ziyaretçi, sanal bir işlev çağırmanın, ihtiyaç duyduğunuz döküm işlemini etkili bir şekilde yaptığı ve sizin için yaptığı gözleminden yararlanan güçlü bir tasarım desenidir. Derleyici bunu yaptığından, asla yanlış olamaz. Bu nedenle, bir enum depolamak yerine, Ziyaretçi nesnenin ne tür olduğunu bilen bir oy hakkına sahip olan soyut bir temel sınıf kullanır. Daha sonra işi yapan, temiz, küçük, çift yönlü bir çağrı yaratırız:
class Tool;
class Sword;
class Shield;
class MagicCloth;
class ToolVisitor {
public:
virtual void visit(Sword* sword) = 0;
virtual void visit(Shield* shield) = 0;
virtual void visit(MagicCloth* cloth) = 0;
};
class Tool {
public:
virtual void accept(ToolVisitor& visitor) = 0;
};
lass Sword : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
};
class Shield : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int defense;
};
class MagicCloth : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
int defense;
};
Peki bu tanrı aweful deseni nedir? Eh, Tool
bir sanal işlevi vardır accept
. Bir ziyaretçi iletirseniz, geri dönmesi ve visit
bu ziyaretçi için türdeki doğru işlevi çağırması beklenir . Bu ne visitor.visit(*this);
her alt tipine yapar. Karmaşık, ancak bunu yukarıdaki örneğinizle gösterebiliriz:
class AttackVisitor : public ToolVisitor
{
public:
int& currentAttack;
int& currentDefense;
AttackVisitor(int& currentAttack_, int& currentDefense_)
: currentAttack(currentAttack_)
, currentDefense(currentDefense_)
{ }
virtual void visit(Sword* sword)
{
currentAttack += sword->attack;
}
virtual void visit(Shield* shield)
{
currentDefense += shield->defense;
}
virtual void visit(MagicCloth* cloth)
{
currentAttack += cloth->attack;
currentDefense += cloth->defense;
}
};
void Player::attack()
{
int currentAttack = this->attack;
int currentDefense = this->defense;
AttackVisitor v(currentAttack, currentDefense);
for (Tool* t: tools) {
t->accept(v);
}
//some other functions to start attack
}
Peki burada ne olacak? Ne tür bir nesne ziyaret ettiğini bildiğinde, bizim için bazı işler yapacak bir ziyaretçi yaratıyoruz. Daha sonra araçların listesini tekrarlıyoruz. Argüman uğruna, diyelim ki ilk nesne bir diğeridir Shield
, fakat kodumuz henüz bunu bilmiyor. Çağrıyor t->accept(v)
, sanal bir işlev. İlk nesne bir kalkan olduğu için, çağırarak biter void Shield::accept(ToolVisitor& visitor)
, hangi aramaları visitor.visit(*this);
. Biz hangi yukarı ararken Şimdi visit
aramaya biz çağırarak sona erecek bu yüzden, zaten, (bu fonksiyon çağrıldım çünkü) biz Kalkanı olduğunu biliyoruz void ToolVisitor::visit(Shield* shield)
sayfamızda yer AttackVisitor
. Bu şimdi savunmamızı güncellemek için doğru kodu çalıştırıyor.
Ziyaretçi hacimlidir. O kadar tuhaf ki neredeyse kendine has bir kokusu olduğunu düşünüyorum. Kötü ziyaretçi kalıpları yazmak çok kolaydır. Ancak, diğerlerinin hiçbirinin sahip olmadığı büyük bir avantaja sahiptir. Yeni bir araç tipi eklersek ToolVisitor::visit
, bunun için yeni bir işlev eklemeliyiz . Bunu yaptığımız anda , programdaki her ToolVisitor
biri derleme yapmayı reddedecek çünkü sanal bir işlevi eksik. Bu, bir şeyi özlediğimiz tüm olayları yakalamayı çok kolaylaştırıyor. İşi yapmak için açıklamalar if
veya switch
ifadeler kullanırsanız garanti etmek daha zordur . Bu avantajlar, Ziyaretçi'nin 3d grafik sahne jeneratörlerinde hoş küçük bir yer bulmasına yetecek kadar iyi. Tam olarak Ziyaretçi'nin sunduğu davranışa ihtiyaç duyuyorlar, bu yüzden harika çalışıyor!
Sonuç olarak, bu kalıpların bir sonraki geliştiriciyi zorlaştırdığını unutmayın. Onlar için kolaylaştırmak için zaman harcayın ve kod kokmaz!
* Teknik olarak, spekere bakarsanız, sockaddr isimli bir üyeye sahiptir sa_family
. Burada bizim için önemli olmayan C düzeyinde bazı hileler yapılıyor. Asıl uygulamaya bakmaya açıksınız, ancak bu cevap sa_family
sin_family
için, C kandırmasının önemsiz ayrıntılarla ilgilendiğine güvenerek, hangisini nesir için en sezgisel olanı kullanarak tamamen birbirinin yerine kullanacağım .