Có một điều trong C ++, điều này khiến tôi cảm thấy khó chịu trong một thời gian dài, bởi vì tôi thực sự không biết làm thế nào, mặc dù nghe có vẻ đơn giản:
Làm cách nào để triển khai Phương thức Factory trong C ++ chính xác?
Mục tiêu: để có thể cho phép khách hàng khởi tạo một số đối tượng bằng các phương thức xuất xưởng thay vì các hàm tạo của đối tượng, mà không có hậu quả không thể chấp nhận và ảnh hưởng đến hiệu suất.
Theo "Mẫu phương thức nhà máy", ý tôi là cả hai phương thức nhà máy tĩnh bên trong một đối tượng hoặc các phương thức được định nghĩa trong một lớp khác hoặc các hàm toàn cục. Nói chung là "khái niệm chuyển hướng cách khởi tạo thông thường của lớp X sang bất kỳ nơi nào khác ngoài hàm tạo".
Hãy để tôi lướt qua một số câu trả lời có thể mà tôi đã nghĩ đến.
0) Đừng làm nhà máy, làm nhà xây dựng.
Điều này nghe có vẻ tốt (và thực sự thường là giải pháp tốt nhất), nhưng không phải là một biện pháp khắc phục chung. Trước hết, có những trường hợp khi việc xây dựng đối tượng là một nhiệm vụ đủ phức tạp để biện minh cho việc trích xuất của nó sang một lớp khác. Nhưng ngay cả đặt thực tế đó sang một bên, ngay cả đối với các đối tượng đơn giản chỉ sử dụng các hàm tạo thường không làm được.
Ví dụ đơn giản nhất mà tôi biết là lớp Vector 2 chiều. Vì vậy, đơn giản, nhưng khó khăn. Tôi muốn có thể xây dựng nó từ cả hai tọa độ Descartes và cực. Rõ ràng, tôi không thể làm:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Cách suy nghĩ tự nhiên của tôi là:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Điều đó, thay vì các nhà xây dựng, dẫn tôi đến việc sử dụng các phương thức nhà máy tĩnh ... điều đó có nghĩa là tôi đang thực hiện mô hình nhà máy, theo một cách nào đó ("lớp trở thành nhà máy riêng của nó"). Điều này có vẻ tốt (và sẽ phù hợp với trường hợp cụ thể này), nhưng không thành công trong một số trường hợp, mà tôi sẽ mô tả ở điểm 2. Hãy đọc tiếp.
một trường hợp khác: cố gắng quá tải bởi hai typedef mờ của một số API (chẳng hạn như GUID của các miền không liên quan hoặc GUID và bitfield), loại hoàn toàn khác nhau về mặt ngữ nghĩa (về lý thuyết - quá tải hợp lệ) nhưng thực tế hóa ra là điều tương tự - như ints không dấu hoặc con trỏ void.
1) Cách Java
Java có nó đơn giản, vì chúng ta chỉ có các đối tượng được phân bổ động. Làm cho một nhà máy là tầm thường như:
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);
}
}
Trong C ++, điều này dịch là:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Mát mẻ? Thường, thực sự. Nhưng sau đó - điều này buộc người dùng chỉ sử dụng phân bổ động. Phân bổ tĩnh là những gì làm cho C ++ phức tạp, nhưng cũng là thứ thường làm cho nó trở nên mạnh mẽ. Ngoài ra, tôi tin rằng tồn tại một số mục tiêu (từ khóa: được nhúng) không cho phép phân bổ động. Và điều đó không có nghĩa là người dùng của các nền tảng đó muốn viết sạch OOP.
Dù sao, triết lý sang một bên: Trong trường hợp chung, tôi không muốn buộc người dùng của nhà máy phải hạn chế phân bổ động.
2) Trả về theo giá trị
OK, vì vậy chúng tôi biết rằng 1) là tuyệt vời khi chúng tôi muốn phân bổ động. Tại sao chúng ta sẽ không thêm phân bổ tĩnh lên trên đó?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Gì? Chúng ta không thể quá tải bởi loại trả lại? Ồ, tất nhiên là không thể. Vì vậy, hãy thay đổi tên phương thức để phản ánh điều đó. Và vâng, tôi đã viết ví dụ mã không hợp lệ ở trên chỉ để nhấn mạnh rằng tôi không thích thay đổi tên phương thức, chẳng hạn vì chúng tôi không thể triển khai thiết kế nhà máy không biết ngôn ngữ ngay bây giờ, vì chúng tôi phải thay đổi tên - và mỗi người dùng mã này sẽ cần phải nhớ sự khác biệt của việc triển khai từ đặc tả.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK ... chúng tôi có nó. Thật xấu xí, vì chúng ta cần thay đổi tên phương thức. Nó không hoàn hảo, vì chúng ta cần viết cùng một mã hai lần. Nhưng một khi được thực hiện, nó hoạt động. Đúng?
Vâng, thông thường. Nhưng đôi khi không. Khi tạo Foo, chúng tôi thực sự phụ thuộc vào trình biên dịch để thực hiện tối ưu hóa giá trị trả về cho chúng tôi, vì tiêu chuẩn C ++ đủ nhân từ để các nhà cung cấp trình biên dịch không chỉ định khi nào đối tượng được tạo tại chỗ và khi nào nó sẽ được sao chép khi trả về đối tượng tạm thời theo giá trị trong C ++. Vì vậy, nếu Foo đắt tiền để sao chép, phương pháp này có rủi ro.
Và nếu Foo hoàn toàn không thể thay đổi thì sao? Vâng, doh. ( Lưu ý rằng trong C ++ 17 với bản sửa đổi bản sao được bảo đảm, việc không thể sao chép được không còn là vấn đề nữa đối với mã ở trên )
Kết luận: Tạo một nhà máy bằng cách trả lại một đối tượng thực sự là một giải pháp cho một số trường hợp (chẳng hạn như vectơ 2 chiều đã đề cập trước đó), nhưng vẫn không phải là sự thay thế chung cho các nhà xây dựng.
3) Thi công hai pha
Một điều nữa mà ai đó có thể sẽ đưa ra là tách biệt vấn đề phân bổ đối tượng và khởi tạo nó. Điều này thường dẫn đến mã như thế này:
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());
// ...
}
Người ta có thể nghĩ rằng nó hoạt động như một nét duyên dáng. Giá duy nhất chúng tôi phải trả trong mã của chúng tôi ...
Vì tôi đã viết tất cả những điều này và để lại đây là lần cuối cùng, tôi cũng không thích nó. :) Tại sao?
Trước hết ... Tôi thực sự không thích khái niệm xây dựng hai pha và tôi cảm thấy có lỗi khi sử dụng nó. Nếu tôi thiết kế các đối tượng của mình với xác nhận rằng "nếu nó tồn tại, nó ở trạng thái hợp lệ", tôi cảm thấy rằng mã của tôi an toàn hơn và ít bị lỗi hơn. Tôi thích nó theo cách đó.
Phải bỏ quy ước đó và thay đổi thiết kế đối tượng của tôi chỉ với mục đích làm cho nhà máy của nó là .. tốt, khó sử dụng.
Tôi biết rằng những điều trên sẽ không thuyết phục được nhiều người, vì vậy hãy để tôi đưa ra một số lập luận chắc chắn hơn. Sử dụng cấu trúc hai pha, bạn không thể:
- khởi tạo
const
hoặc tham chiếu biến thành viên, - truyền đối số cho các hàm tạo lớp cơ sở và các hàm tạo đối tượng thành viên.
Và có lẽ có thể có một số nhược điểm nữa mà tôi không thể nghĩ ra ngay bây giờ, và tôi thậm chí không cảm thấy đặc biệt bắt buộc vì các gạch đầu dòng ở trên đã thuyết phục tôi.
Vì vậy: thậm chí không gần với một giải pháp chung tốt để thực hiện một nhà máy.
Kết luận:
Chúng tôi muốn có một cách khởi tạo đối tượng sẽ:
- cho phép khởi tạo thống nhất bất kể phân bổ,
- đặt tên khác nhau, có ý nghĩa cho các phương thức xây dựng (do đó không dựa vào quá tải đối số),
- không giới thiệu một lần truy cập hiệu năng đáng kể và tốt nhất là một lần truy cập mã đáng kể, đặc biệt là ở phía máy khách,
- nói chung, như trong: có thể được giới thiệu cho bất kỳ lớp nào.
Tôi tin rằng tôi đã chứng minh rằng những cách tôi đã đề cập không đáp ứng những yêu cầu đó.
Có gợi ý nào không? Vui lòng cung cấp cho tôi một giải pháp, tôi không muốn nghĩ rằng ngôn ngữ này sẽ không cho phép tôi thực hiện đúng một khái niệm tầm thường như vậy.
delete
đó. Các loại phương thức này hoàn toàn tốt, miễn là "tài liệu" (mã nguồn là tài liệu ;-)) rằng người gọi có quyền sở hữu con trỏ (đọc: chịu trách nhiệm xóa nó khi thích hợp).
unique_ptr<T>
thay vì T*
.