Như đã viết, nó "có mùi", nhưng đó có thể chỉ là những ví dụ bạn đưa ra. Lưu trữ dữ liệu trong các thùng chứa đối tượng chung, sau đó truyền dữ liệu để có quyền truy cập vào dữ liệu không phải là mã tự động. Bạn sẽ thấy nó được sử dụng trong nhiều tình huống. Tuy nhiên, khi bạn sử dụng nó, bạn nên biết những gì bạn đang làm, cách bạn đang làm và tại sao. Khi tôi nhìn vào ví dụ, việc sử dụng các phép so sánh dựa trên chuỗi để cho tôi biết đối tượng nào là thứ làm vấp phải máy đo mùi cá nhân của tôi. Điều đó cho thấy bạn không hoàn toàn chắc chắn những gì bạn đang làm ở đây (điều này tốt, vì bạn đã có sự khôn ngoan khi đến đây với các lập trình viên. Hãy nói và nói "này, tôi không nghĩ tôi thích những gì tôi đang làm, hãy giúp đỡ tôi ra ngoài! ").
Vấn đề cơ bản với mô hình truyền dữ liệu từ các thùng chứa chung như thế này là nhà sản xuất dữ liệu và người tiêu dùng dữ liệu phải làm việc cùng nhau, nhưng có thể không rõ ràng rằng thoạt nhìn họ làm như vậy. Trong mọi ví dụ về mẫu này, có mùi hay không có mùi, đây là vấn đề cơ bản. Nó là rất tốt cho các nhà phát triển bên cạnh hoàn toàn không biết rằng bạn đang làm mô hình này và phá vỡ nó một cách tình cờ, vì vậy nếu bạn sử dụng mô hình này, bạn phải chăm sóc để giúp các nhà phát triển ra tiếp theo. Bạn phải làm cho anh ta dễ dàng hơn để không phá vỡ mã vô ý do một số chi tiết anh ta có thể không biết tồn tại.
Ví dụ, nếu tôi muốn sao chép một người chơi thì sao? Nếu tôi chỉ nhìn vào nội dung của đối tượng người chơi, nó có vẻ khá dễ dàng. Tôi chỉ có sao chép attack
, defense
và tools
các biến. Dễ như ăn bánh! Chà, tôi sẽ nhanh chóng phát hiện ra rằng việc bạn sử dụng các con trỏ làm cho nó khó hơn một chút (tại một số điểm, đáng để xem các con trỏ thông minh, nhưng đó là một chủ đề khác). Điều đó dễ dàng được giải quyết. Tôi sẽ chỉ tạo các bản sao mới của mỗi công cụ và đưa chúng vào tools
danh sách mới của tôi . Rốt cuộc, Tool
là một lớp học thực sự đơn giản chỉ có một thành viên. Vì vậy, tôi tạo ra một loạt các bản sao, bao gồm một bản sao của nó Sword
, nhưng tôi không biết đó là một thanh kiếm, vì vậy tôi chỉ sao chép nó name
. Sau đó, attack()
hàm nhìn vào tên, thấy rằng đó là một "thanh kiếm", sử dụng nó và những điều tồi tệ sẽ xảy ra!
Chúng ta có thể so sánh trường hợp này với trường hợp khác trong lập trình socket, sử dụng cùng một mẫu. Tôi có thể thiết lập chức năng ổ cắm UNIX như thế này:
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));
Tại sao đây là mô hình tương tự? Bởi vì bind
không chấp nhận a sockaddr_in*
, nó chấp nhận chung chung hơn sockaddr*
. Nếu bạn nhìn vào các định nghĩa cho các lớp đó, chúng tôi thấy rằng sockaddr
chỉ có một thành viên mà gia đình chúng tôi gán cho sin_family
*. Gia đình nói rằng bạn nên chọn kiểu con sockaddr
nào. AF_INET
cho bạn biết rằng cấu trúc địa chỉ thực sự là a sockaddr_in
. Nếu đúng như vậy AF_INET6
, địa chỉ sẽ là một sockaddr_in6
, có các trường lớn hơn để hỗ trợ các địa chỉ IPv6 lớn hơn.
Điều này giống hệt với Tool
ví dụ của bạn , ngoại trừ nó sử dụng một số nguyên để chỉ định họ nào hơn là a std::string
. Tuy nhiên, tôi sẽ tuyên bố rằng nó không có mùi, và cố gắng làm như vậy vì những lý do khác ngoài "đó là một cách tiêu chuẩn để làm ổ cắm, vì vậy nó không nên" ngửi. "" Rõ ràng là cùng một kiểu, đó là tại sao tôi cho rằng việc lưu trữ dữ liệu trong các đối tượng chung và truyền dữ liệu không phải là mã tự động , nhưng có một số khác biệt trong cách họ làm điều đó giúp an toàn hơn.
Khi sử dụng mẫu này, thông tin quan trọng nhất là nắm bắt việc truyền tải thông tin về phân lớp từ nhà sản xuất đến người tiêu dùng. Đây là những gì bạn đang làm với name
trường và các socket UNIX làm với sin_family
trường của chúng . Lĩnh vực đó là thông tin người tiêu dùng cần để hiểu những gì nhà sản xuất đã thực sự tạo ra. Trong tất cả các trường hợp của mẫu này, nó phải là một phép liệt kê (hoặc ít nhất, một số nguyên hoạt động như một phép liệt kê). Tại sao? Hãy suy nghĩ về những gì người tiêu dùng của bạn sẽ làm với thông tin. Họ sẽ cần phải viết ra một số if
tuyên bố lớn hoặc mộtswitch
câu lệnh, như bạn đã làm, nơi họ xác định kiểu con chính xác, bỏ nó và sử dụng dữ liệu. Theo định nghĩa, chỉ có thể có một số lượng nhỏ các loại này. Bạn có thể lưu trữ nó trong một chuỗi, như bạn đã làm, nhưng điều đó có nhiều nhược điểm:
- Chậm -
std::string
thường phải thực hiện một số bộ nhớ động để giữ chuỗi. Bạn cũng phải thực hiện một so sánh toàn văn bản để khớp với tên mỗi khi bạn muốn tìm ra lớp con nào bạn có.
- Quá linh hoạt - Có điều gì đó để nói về việc đặt ra những ràng buộc cho chính bạn khi bạn đang làm điều gì đó cực kỳ nguy hiểm. Tôi đã có những hệ thống như thế này để tìm một chuỗi con để cho nó biết loại vật thể mà nó đang nhìn. Điều này hoạt động rất tốt cho đến khi tên của một đối tượng vô tình chứa chuỗi con đó và tạo ra một lỗi khó hiểu khủng khiếp. Vì, như chúng tôi đã trình bày ở trên, chúng tôi chỉ cần một số ít trường hợp, không có lý do gì để sử dụng một công cụ bị áp đảo ồ ạt như chuỗi. Điều này dẫn đến...
- Dễ bị lỗi - Chúng ta chỉ cần nói rằng bạn sẽ muốn tiếp tục một vụ giết người điên cuồng cố gắng gỡ lỗi tại sao mọi thứ không hoạt động khi một người tiêu dùng vô tình đặt tên của một miếng vải ma thuật
MagicC1oth
. Nghiêm túc mà nói, những con bọ như thế có thể mất nhiều ngày để gãi đầu trước khi bạn nhận ra chuyện gì đã xảy ra.
Một bảng liệt kê hoạt động tốt hơn nhiều. Nó nhanh, rẻ và dễ bị lỗi hơn nhiều:
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!");
}
}
};
Ví dụ này cũng cho thấy một switch
tuyên bố liên quan đến các enum, với phần quan trọng nhất của mẫu này: một default
trường hợp ném. Bạn không bao giờ nên gặp tình huống đó nếu bạn làm mọi thứ một cách hoàn hảo. Tuy nhiên, nếu ai đó thêm một loại công cụ mới và bạn quên cập nhật mã của mình để hỗ trợ nó, bạn sẽ muốn một cái gì đó để bắt lỗi. Trên thực tế, tôi khuyên họ rất nhiều vì vậy bạn nên thêm chúng ngay cả khi bạn không cần chúng.
Ưu điểm lớn khác của enum
là nó cung cấp cho các nhà phát triển tiếp theo một danh sách đầy đủ các loại công cụ có hiệu lực, phải lên phía trước. Không cần phải lội qua mã để tìm lớp Sáo chuyên dụng của Bob mà anh ta sử dụng trong trận đấu trùm hoành tráng của mình.
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
}
}
Có, tôi đã đưa ra một tuyên bố mặc định "trống rỗng", chỉ để làm cho nó rõ ràng cho nhà phát triển tiếp theo những gì tôi dự kiến sẽ xảy ra nếu một số loại bất ngờ mới xuất hiện theo cách của tôi.
Nếu bạn làm điều này, mô hình sẽ ít mùi hơn. Tuy nhiên, để không có mùi, điều cuối cùng bạn cần làm là xem xét các lựa chọn khác. Những diễn viên này là một số công cụ mạnh mẽ và nguy hiểm hơn mà bạn có trong tiết mục C ++. Bạn không nên sử dụng chúng trừ khi bạn có lý do chính đáng.
Một thay thế rất phổ biến là cái mà tôi gọi là "union struct" hoặc "class union". Ví dụ của bạn, điều này thực sự sẽ rất phù hợp. Để tạo một trong số này, bạn tạo một Tool
lớp, với một bảng liệt kê như trước đây, nhưng thay vì phân lớpTool
, chúng tôi chỉ đặt tất cả các trường từ mỗi kiểu con trên đó.
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
int attack;
int defense;
};
Bây giờ bạn không cần lớp con nào cả. Bạn chỉ cần nhìn vàotype
trường để xem trường nào khác thực sự hợp lệ. Điều này là an toàn hơn và dễ hiểu hơn. Tuy nhiên, nó có nhược điểm. Có những lúc bạn không muốn sử dụng cái này:
- Khi các đối tượng quá khác nhau - Bạn có thể kết thúc với một danh sách các lĩnh vực giặt ủi và có thể không rõ những đối tượng nào áp dụng cho từng loại đối tượng.
- Khi hoạt động trong tình huống quan trọng về bộ nhớ - Nếu bạn cần tạo 10 công cụ, bạn có thể lười biếng với bộ nhớ. Khi bạn cần tạo ra 500 triệu công cụ, bạn sẽ bắt đầu quan tâm đến bit và byte. Cấu trúc liên minh luôn lớn hơn mức cần thiết.
Giải pháp này không được sử dụng bởi các socket UNIX vì vấn đề không giống nhau được kết hợp bởi tính kết thúc mở của API. Mục đích của các socket UNIX là tạo ra thứ gì đó mà mọi hương vị của UNIX có thể hoạt động. Mỗi hương vị có thể xác định danh sách các gia đình họ hỗ trợ, như AF_INET
, và sẽ có một danh sách ngắn cho mỗi gia đình. Tuy nhiên, nếu một giao thức mới xuất hiện, nhưAF_INET6
đã làm, bạn có thể cần thêm các trường mới. Nếu bạn đã làm điều này với một cấu trúc hợp nhất, cuối cùng bạn sẽ tạo ra một phiên bản mới của cấu trúc có cùng tên, tạo ra các vấn đề không tương thích vô tận. Đây là lý do tại sao các socket UNIX chọn sử dụng mô hình đúc thay vì cấu trúc hợp. Tôi chắc rằng họ đã xem xét nó và thực tế là họ nghĩ về nó là một phần lý do tại sao nó không có mùi khi họ sử dụng nó.
Bạn cũng có thể sử dụng một liên minh thực sự. Các công đoàn tiết kiệm bộ nhớ, bằng cách chỉ lớn hơn thành viên lớn nhất, nhưng họ đi kèm với các vấn đề riêng của họ. Đây có thể không phải là một tùy chọn cho mã của bạn, nhưng nó luôn là một tùy chọn bạn nên xem xét.
Một giải pháp thú vị khác là boost::variant
. Boost là một thư viện tuyệt vời với đầy đủ các giải pháp đa nền tảng có thể tái sử dụng. Đây có lẽ là một số mã C ++ tốt nhất từng được viết. Boost.Variant về cơ bản là phiên bản C ++ của các hiệp hội. Nó là một thùng chứa có thể chứa nhiều loại khác nhau, nhưng chỉ có một loại tại một thời điểm. Bạn có thể làm cho bạn Sword
, Shield
và MagicCloth
các lớp học, sau đó làm công cụ là một bằng khá một chút!), Nhưng mô hình này có thể vô cùng hữu ích. Biến thể thường được sử dụng, ví dụ, trong các cây phân tích, lấy một chuỗi văn bản và phá vỡ nó bằng cách sử dụng một ngữ pháp cho các quy tắc.boost::variant<Sword, Shield, MagicCloth>
, có nghĩa là nó có chứa một trong ba loại. Điều này vẫn gặp phải vấn đề tương tự với khả năng tương thích trong tương lai ngăn các socket UNIX sử dụng nó (chưa kể các socket UNIX là C, dự đoánboost
Giải pháp cuối cùng mà tôi khuyên bạn nên xem xét trước khi lao vào và sử dụng phương pháp đúc đối tượng chung là mẫu thiết kế của Khách truy cập . Khách truy cập là một mẫu thiết kế mạnh mẽ, tận dụng lợi thế của việc quan sát rằng việc gọi một chức năng ảo thực sự có hiệu quả mà bạn cần và nó thực hiện điều đó cho bạn. Bởi vì trình biên dịch làm điều đó, nó không bao giờ có thể sai. Do đó, thay vì lưu trữ một enum, Khách truy cập sử dụng một lớp cơ sở trừu tượng, có một vtable để biết loại đối tượng là gì. Sau đó, chúng tôi tạo ra một cuộc gọi gián tiếp đôi nhỏ gọn thực hiện công việc:
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;
};
Vì vậy, mô hình thần khủng khiếp này là gì? Vâng, Tool
có một chức năng ảo accept
,. Nếu bạn vượt qua nó một khách truy cập, dự kiến sẽ quay lại và gọi đúng visit
chức năng trên khách truy cập đó cho loại. Đây là những gì visitor.visit(*this);
làm trên mỗi kiểu con. Phức tạp, nhưng chúng tôi có thể hiển thị điều này với ví dụ của bạn ở trên:
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
}
Vậy chuyện gì xảy ra ở đây? Chúng tôi tạo ra một khách truy cập sẽ thực hiện một số công việc cho chúng tôi, khi họ biết loại đối tượng mà nó truy cập. Chúng tôi sau đó lặp đi lặp lại danh sách các công cụ. Để tranh luận, giả sử đối tượng đầu tiên là một Shield
, nhưng mã của chúng tôi chưa biết điều đó. Nó gọi t->accept(v)
, một chức năng ảo. Bởi vì đối tượng đầu tiên là một lá chắn, cuối cùng nó gọi void Shield::accept(ToolVisitor& visitor)
, mà gọi visitor.visit(*this);
. Bây giờ, khi chúng tôi tìm kiếm visit
để gọi, chúng tôi đã biết rằng chúng tôi có Khiên (vì chức năng này đã được gọi), vì vậy chúng tôi sẽ kết thúc cuộc gọi void ToolVisitor::visit(Shield* shield)
của chúng tôi AttackVisitor
. Điều này bây giờ chạy mã chính xác để cập nhật quốc phòng của chúng tôi.
Khách truy cập là cồng kềnh. Thật là rắc rối đến nỗi tôi gần như nghĩ rằng nó có mùi của riêng nó. Rất dễ dàng để viết các mẫu khách truy cập xấu. Tuy nhiên, nó có một lợi thế rất lớn mà không ai khác có được. Nếu chúng ta thêm một loại công cụ mới, chúng ta phải thêm một ToolVisitor::visit
chức năng mới cho nó. Ngay khi chúng tôi thực hiện việc này, mọi ToolVisitor
chương trình sẽ từ chối biên dịch vì nó thiếu chức năng ảo. Điều này làm cho nó rất dễ dàng để bắt tất cả các trường hợp chúng ta bỏ lỡ một cái gì đó. Sẽ khó hơn nhiều để đảm bảo rằng nếu bạn sử dụng if
hoặc switch
tuyên bố để thực hiện công việc. Những lợi thế này là đủ tốt để Khách truy cập đã tìm thấy một vị trí nhỏ đẹp trong trình tạo cảnh đồ họa 3d. Họ tình cờ cần chính xác loại hành vi mà Khách cung cấp để nó hoạt động tuyệt vời!
Trong tất cả, hãy nhớ rằng các mẫu này làm khó nhà phát triển tiếp theo. Dành thời gian để làm cho họ dễ dàng hơn và mã sẽ không có mùi!
* Về mặt kỹ thuật, nếu bạn nhìn vào thông số kỹ thuật, sockaddr có một thành viên được đặt tên sa_family
. Có một số khó khăn được thực hiện ở đây ở cấp độ C không quan trọng đối với chúng tôi. Bạn có thể xem xét việc triển khai thực tế , nhưng đối với câu trả lời này tôi sẽ sử dụng sa_family
sin_family
và những người khác hoàn toàn có thể thay thế cho nhau, sử dụng bất cứ ai trực quan nhất cho văn xuôi, tin rằng thủ thuật C quan tâm đến các chi tiết không quan trọng.