Cách triển khai mẫu phương thức nhà máy trong C ++ một cách chính xác


329

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 consthoặ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.


7
@Zac, mặc dù tiêu đề rất giống nhau, nhưng các câu hỏi thực tế là IMHO khác nhau.
Péter Török

2
Tốt trùng lặp nhưng nội dung của này câu hỏi là có giá trị trong và của chính nó.
dmckee --- ex-moderator mèo con

7
Hai năm sau khi hỏi điều này, tôi có một số điểm cần thêm: 1) Câu hỏi này có liên quan đến một số mẫu thiết kế ([trừu tượng] nhà máy, nhà xây dựng, bạn đặt tên cho nó, tôi không thích đào sâu vào phân loại của họ). 2) Vấn đề thực tế đang được thảo luận ở đây là "làm thế nào để tách sạch phân bổ lưu trữ đối tượng khỏi việc xây dựng đối tượng?".
Kos

1
@Dennis: chỉ khi bạn không làm điều 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).
Boris Dalstein

1
@Boris @Dennis bạn cũng có thể làm cho nó rất rõ ràng bằng cách trả lại unique_ptr<T>thay vì T*.
Kos

Câu trả lời:


107

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.

Tôi tin rằng điểm này là không chính xác. Sự phức tạp không thực sự quan trọng. Sự liên quan là những gì không. Nếu một đối tượng có thể được xây dựng trong một bước (không giống như trong mẫu trình xây dựng), thì hàm tạo là nơi thích hợp để thực hiện. Nếu bạn thực sự cần một lớp khác để thực hiện công việc, thì đó phải là một lớp trợ giúp được sử dụng từ hàm tạo.

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

Có một cách giải quyết dễ dàng cho việc này:

struct Cartesian {
  inline Cartesian(float x, float y): x(x), y(y) {}
  float x, y;
};
struct Polar {
  inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
  float angle, magnitude;
};
Vec2(const Cartesian &cartesian);
Vec2(const Polar &polar);

Nhược điểm duy nhất là nó trông hơi dài dòng:

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));

Nhưng điều tốt là bạn có thể thấy ngay loại tọa độ bạn đang sử dụng, đồng thời bạn không phải lo lắng về việc sao chép. Nếu bạn muốn sao chép và nó rất tốn kém (tất nhiên như đã được chứng minh bằng cách định hình), bạn có thể muốn sử dụng một cái gì đó như các lớp chia sẻ của Qt để tránh sao chép chi phí.

Đối với loại phân bổ, lý do chính để sử dụng mẫu nhà máy thường là đa hình. Các nhà xây dựng không thể là ảo, và thậm chí nếu họ có thể, nó sẽ không có ý nghĩa nhiều. Khi sử dụng phân bổ tĩnh hoặc ngăn xếp, bạn không thể tạo các đối tượng theo cách đa hình vì trình biên dịch cần biết kích thước chính xác. Vì vậy, nó chỉ hoạt động với con trỏ và tài liệu tham khảo. Và việc trả về một tham chiếu từ một nhà máy cũng không hoạt động, bởi vì trong khi một đối tượng về mặt kỹ thuật thể bị xóa bởi tham chiếu, nó có thể khá khó hiểu và dễ bị lỗi, hãy xem Cách thực hành trả về biến tham chiếu C ++ có phải là xấu không?ví dụ. Vì vậy, con trỏ là thứ duy nhất còn lại và bao gồm cả con trỏ thông minh. Nói cách khác, các nhà máy rất hữu ích khi được sử dụng với phân bổ động, vì vậy bạn có thể làm những việc như thế này:

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();

Trong các trường hợp khác, các nhà máy chỉ giúp giải quyết các vấn đề nhỏ như những vấn đề quá tải mà bạn đã đề cập. Sẽ thật tuyệt nếu có thể sử dụng chúng một cách thống nhất, nhưng nó không gây hại gì nhiều mà có lẽ là không thể.


21
+1 cho các cấu trúc Cartesian và Polar. Nói chung, tốt nhất là tạo các lớp và cấu trúc đại diện trực tiếp cho dữ liệu mà chúng được dự định (trái ngược với cấu trúc Vec chung). Nhà máy của bạn cũng là một ví dụ tốt, nhưng ví dụ của bạn không minh họa ai sở hữu con trỏ 'a'. Nếu Nhà máy 'f' sở hữu nó, thì có lẽ nó sẽ bị phá hủy khi 'f' rời khỏi phạm vi, nhưng nếu 'f' không sở hữu nó, điều quan trọng là nhà phát triển phải nhớ giải phóng bộ nhớ đó nếu không có thể rò rỉ bộ nhớ xảy ra
David Peterson

1
Tất nhiên một đối tượng có thể bị xóa bằng cách tham khảo! Xem stackoverflow.com/a/752699/404734 Điều đó tất nhiên đặt ra câu hỏi nếu trả lại bộ nhớ động bằng cách tham chiếu, vì vấn đề về khả năng gán giá trị trả về bằng cách sao chép (người gọi dĩ nhiên cũng có thể làm gì đó như int a = * returnAPoninterToInt () và sau đó sẽ đối mặt với cùng một vấn đề, nếu bộ nhớ được đồng bộ hóa động trở lại, như đối với các tham chiếu, nhưng trong phiên bản con trỏ, người dùng phải hoàn toàn không tham chiếu thay vì chỉ quên tham chiếu rõ ràng, là sai) .
Kaiserludi

1
@Kaiserludi, điểm hay. Tôi đã không nghĩ về điều đó, nhưng nó vẫn là một cách "xấu xa" để làm mọi việc. Chỉnh sửa câu trả lời của tôi để phản ánh điều đó.
Sergei Tachenov

Điều gì về việc tạo ra các lớp không đa hình khác nhau là bất biến? Là một mô hình nhà máy sau đó thích hợp để sử dụng trong C ++?
daaxix

@daaxix, tại sao bạn cần một nhà máy để tạo các thể hiện của một lớp không đa hình? Tôi không thấy những gì bất biến phải làm với bất cứ điều gì về điều này.
Sergei Tachenov

49

Nhà máy đơn giản Ví dụ:

// Factory returns object and ownership
// Caller responsible for deletion.
#include <memory>
class FactoryReleaseOwnership{
  public:
    std::unique_ptr<Foo> createFooInSomeWay(){
      return std::unique_ptr<Foo>(new Foo(some, args));
    }
};

// Factory retains object ownership
// Thus returning a reference.
#include <boost/ptr_container/ptr_vector.hpp>
class FactoryRetainOwnership{
  boost::ptr_vector<Foo>  myFoo;
  public:
    Foo& createFooInSomeWay(){
      // Must take care that factory last longer than all references.
      // Could make myFoo static so it last as long as the application.
      myFoo.push_back(new Foo(some, args));
      return myFoo.back();
    }
};

2
@LokiAstari Bởi vì sử dụng con trỏ thông minh là cách đơn giản nhất để mất kiểm soát bộ nhớ. Việc kiểm soát các lang C / C ++ được biết đến là tối cao so với các ngôn ngữ khác và từ đó chúng có được lợi thế lớn nhất. Không đề cập đến thực tế là con trỏ thông minh tạo ra chi phí bộ nhớ tương tự như các ngôn ngữ được quản lý khác. Nếu bạn muốn sự tiện lợi của việc quản lý bộ nhớ tự động, hãy bắt đầu lập trình bằng Java hoặc C # nhưng đừng đặt mớ hỗn độn đó vào C / C ++.
luke1985

45
@ lukasz1985 unique_ptrtrong ví dụ đó không có chi phí hoạt động. Quản lý tài nguyên, bao gồm bộ nhớ, là một trong những lợi thế tối cao của C ++ so với bất kỳ ngôn ngữ nào khác bởi vì bạn có thể làm điều đó mà không bị phạt hiệu suất và dứt khoát, mà không mất kiểm soát, nhưng bạn nói hoàn toàn ngược lại. Một số người không thích những thứ mà C ++ thực hiện, như quản lý bộ nhớ thông qua các con trỏ thông minh, nhưng nếu những gì bạn muốn là mọi thứ phải rõ ràng, hãy sử dụng C; sự đánh đổi là những mệnh lệnh có cường độ ít hơn. Tôi nghĩ rằng đó là không công bằng bạn bỏ phiếu khuyến nghị tốt.
TheCppZoo

1
@EdMaster: Trước đây tôi không trả lời vì rõ ràng anh ta đang troll. Xin đừng cho troll.
Martin York

17
@LokiAstari anh ta có thể là một kẻ troll, nhưng những gì anh ta nói có thể khiến mọi người nhầm lẫn
TheCppZoo

1
@yau: Vâng. Nhưng: boost::ptr_vector<>hiệu quả hơn một chút vì nó hiểu rằng nó sở hữu con trỏ hơn là ủy thác công việc cho một lớp con. NHƯNG ưu điểm chính của boost::ptr_vector<>nó là hiển thị các thành viên của nó bằng tham chiếu (không phải con trỏ), do đó nó thực sự dễ sử dụng với các thuật toán trong thư viện chuẩn.
Martin York

41

Bạn đã nghĩ về việc không sử dụng một nhà máy nào cả, và thay vào đó sử dụng tốt hệ thống loại? Tôi có thể nghĩ về hai cách tiếp cận khác nhau để thực hiện điều này:

Lựa chọn 1:

struct linear {
    linear(float x, float y) : x_(x), y_(y){}
    float x_;
    float y_;
};

struct polar {
    polar(float angle, float magnitude) : angle_(angle),  magnitude_(magnitude) {}
    float angle_;
    float magnitude_;
};


struct Vec2 {
    explicit Vec2(const linear &l) { /* ... */ }
    explicit Vec2(const polar &p) { /* ... */ }
};

Cho phép bạn viết những thứ như:

Vec2 v(linear(1.0, 2.0));

Lựa chọn 2:

bạn có thể sử dụng "thẻ" giống như STL thực hiện với các trình vòng lặp, v.v. Ví dụ:

struct linear_coord_tag linear_coord {}; // declare type and a global
struct polar_coord_tag polar_coord {};

struct Vec2 {
    Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
    Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
};

Cách tiếp cận thứ hai này cho phép bạn viết mã trông như thế này:

Vec2 v(1.0, 2.0, linear_coord);

đó cũng là tốt đẹp và biểu cảm trong khi cho phép bạn có các nguyên mẫu duy nhất cho mỗi nhà xây dựng.


29

Bạn có thể đọc một giải pháp rất tốt trong: http://www.codeproject.com/Articles/363338/Factory-Potype-in-Cplusplus

Giải pháp tốt nhất là về "bình luận và thảo luận", xem phần "Không cần phương thức Tạo tĩnh".

Từ ý tưởng này, tôi đã thực hiện một nhà máy. Lưu ý rằng tôi đang sử dụng Qt, nhưng bạn có thể thay đổi QMap và QString cho tương đương tiêu chuẩn.

#ifndef FACTORY_H
#define FACTORY_H

#include <QMap>
#include <QString>

template <typename T>
class Factory
{
public:
    template <typename TDerived>
    void registerType(QString name)
    {
        static_assert(std::is_base_of<T, TDerived>::value, "Factory::registerType doesn't accept this type because doesn't derive from base class");
        _createFuncs[name] = &createFunc<TDerived>;
    }

    T* create(QString name) {
        typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name);
        if (it != _createFuncs.end()) {
            return it.value()();
        }
        return nullptr;
    }

private:
    template <typename TDerived>
    static T* createFunc()
    {
        return new TDerived();
    }

    typedef T* (*PCreateFunc)();
    QMap<QString,PCreateFunc> _createFuncs;
};

#endif // FACTORY_H

Sử dụng mẫu:

Factory<BaseClass> f;
f.registerType<Descendant1>("Descendant1");
f.registerType<Descendant2>("Descendant2");
Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1"));
Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2"));
BaseClass *b1 = f.create("Descendant1");
BaseClass *b2 = f.create("Descendant2");

17

Tôi hầu hết đồng ý với câu trả lời được chấp nhận, nhưng có một tùy chọn C ++ 11 chưa được đề cập trong các câu trả lời hiện có:

  • Trả về kết quả phương thức nhà máy theo giá trị
  • Cung cấp một nhà xây dựng di chuyển giá rẻ .

Thí dụ:

struct sandwich {
  // Factory methods.
  static sandwich ham();
  static sandwich spam();
  // Move constructor.
  sandwich(sandwich &&);
  // etc.
};

Sau đó, bạn có thể xây dựng các đối tượng trên ngăn xếp:

sandwich mine{sandwich::ham()};

Là tiểu dự án của những thứ khác:

auto lunch = std::make_pair(sandwich::spam(), apple{});

Hoặc được phân bổ động:

auto ptr = std::make_shared<sandwich>(sandwich::ham());

Khi nào tôi có thể sử dụng cái này?

Nếu, trên một hàm tạo công khai, không thể đưa ra các trình khởi tạo có ý nghĩa cho tất cả các thành viên lớp mà không có một số tính toán sơ bộ, thì tôi có thể chuyển đổi hàm tạo đó thành một phương thức tĩnh. Phương thức tĩnh thực hiện các tính toán sơ bộ, sau đó trả về kết quả giá trị thông qua một hàm tạo riêng, chỉ thực hiện khởi tạo thành viên khôn ngoan.

Tôi nói ' có thể ' bởi vì nó phụ thuộc vào cách tiếp cận nào đưa ra mã rõ ràng nhất mà không hiệu quả một cách không cần thiết.


1
Tôi đã sử dụng rộng rãi khi gói tài nguyên OpenGL. Đã xóa các hàm tạo sao chép và gán sao chép buộc sử dụng ngữ nghĩa di chuyển. Sau đó tôi đã tạo ra một loạt các phương thức nhà máy tĩnh để tạo từng loại tài nguyên. Điều này dễ đọc hơn rất nhiều so với công văn thời gian chạy dựa trên enum của OpenGL thường có một loạt các tham số hàm dự phòng tùy thuộc vào enum được truyền. Đó là một mô hình rất hữu ích, ngạc nhiên khi câu trả lời này không cao hơn.
Sợi

11

Loki có cả Phương thức xuất xưởngNhà máy trừu tượng . Cả hai đều được ghi lại (rộng rãi) trong Thiết kế C ++ hiện đại , bởi Andei Alexandrescu. Phương thức xuất xưởng có thể gần với những gì bạn dường như sau đó, mặc dù nó vẫn hơi khác một chút (ít nhất là nếu bộ nhớ phục vụ, nó yêu cầu bạn phải đăng ký một loại trước khi nhà máy có thể tạo các đối tượng thuộc loại đó).


1
Ngay cả khi nó đã lỗi thời (mà tôi tranh chấp), nó vẫn hoàn toàn có thể sử dụng được. Tôi vẫn sử dụng Factory dựa trên MC ++ D trong một dự án C ++ 14 mới để đạt hiệu quả cao! Hơn nữa, các mẫu Factory và Singleton có lẽ là những phần ít lỗi thời nhất. Trong khi các phần của Loki thích Functionvà các thao tác loại có thể được thay thế bằng std::function<type_traits>trong khi lambdas, xâu chuỗi, giới thiệu giá trị có hàm ý có thể yêu cầu một số điều chỉnh nhỏ, không có sự thay thế tiêu chuẩn nào cho các nhà máy đơn lẻ như ông mô tả.
kim loại

5

Tôi không cố gắng trả lời tất cả các câu hỏi của mình, vì tôi tin rằng nó quá rộng. Chỉ cần một vài lưu ý:

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 nó sang một lớp khác.

Lớp học đó thực tế là một Builder , chứ không phải là Factory.

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.

Sau đó, bạn có thể có nhà máy của bạn gói gọn nó trong một con trỏ thông minh. Tôi tin rằng theo cách này bạn có thể có bánh của bạn và ăn nó quá.

Điều này cũng giúp loại bỏ các vấn đề liên quan đến lợi nhuậ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.

Thật. Tất cả các mẫu thiết kế đều có những hạn chế và hạn chế (ngôn ngữ cụ thể). Bạn chỉ nên sử dụng chúng khi chúng giúp bạn giải quyết vấn đề của bạn chứ không phải vì lợi ích của chúng.

Nếu bạn sau khi thực hiện nhà máy "hoàn hảo", tốt, chúc may mắn.


Cảm ơn câu trả lời! Nhưng bạn có thể giải thích làm thế nào sử dụng một con trỏ thông minh sẽ giải phóng sự hạn chế của phân bổ động? Tôi đã không hoàn toàn có được phần này.
Kos

@Kos, với các con trỏ thông minh, bạn có thể ẩn phân bổ / phân bổ đối tượng thực tế khỏi người dùng của mình. Họ chỉ nhìn thấy con trỏ thông minh được đóng gói, mà thế giới bên ngoài hoạt động giống như một đối tượng được phân bổ tĩnh.
Péter Török

@Kos, không phải theo nghĩa chặt chẽ, AFAIR. Bạn truyền vào đối tượng cần bọc, mà bạn có thể đã phân bổ động tại một số điểm. Sau đó, con trỏ thông minh sẽ sở hữu nó và đảm bảo rằng nó bị phá hủy đúng cách khi không cần thiết nữa (thời gian được quyết định khác nhau cho các loại con trỏ thông minh khác nhau).
Péter Török

3

Đây là giải pháp phong cách c ++ 11 của tôi. tham số 'cơ sở' dành cho lớp cơ sở của tất cả các lớp con. người tạo, là các đối tượng hàm std :: để tạo các thể hiện của lớp con, có thể là một ràng buộc với 'lớp thành viên tĩnh' của lớp con 'tạo (một số đối số)'. Điều này có thể không hoàn hảo nhưng làm việc cho tôi. Và đó là giải pháp 'chung chung'.

template <class base, class... params> class factory {
public:
  factory() {}
  factory(const factory &) = delete;
  factory &operator=(const factory &) = delete;

  auto create(const std::string name, params... args) {
    auto key = your_hash_func(name.c_str(), name.size());
    return std::move(create(key, args...));
  }

  auto create(key_t key, params... args) {
    std::unique_ptr<base> obj{creators_[key](args...)};
    return obj;
  }

  void register_creator(const std::string name,
                        std::function<base *(params...)> &&creator) {
    auto key = your_hash_func(name.c_str(), name.size());
    creators_[key] = std::move(creator);
  }

protected:
  std::unordered_map<key_t, std::function<base *(params...)>> creators_;
};

Một ví dụ về cách sử dụng.

class base {
public:
  base(int val) : val_(val) {}

  virtual ~base() { std::cout << "base destroyed\n"; }

protected:
  int val_ = 0;
};

class foo : public base {
public:
  foo(int val) : base(val) { std::cout << "foo " << val << " \n"; }

  static foo *create(int val) { return new foo(val); }

  virtual ~foo() { std::cout << "foo destroyed\n"; }
};

class bar : public base {
public:
  bar(int val) : base(val) { std::cout << "bar " << val << "\n"; }

  static bar *create(int val) { return new bar(val); }

  virtual ~bar() { std::cout << "bar destroyed\n"; }
};

int main() {
  common::factory<base, int> factory;

  auto foo_creator = std::bind(&foo::create, std::placeholders::_1);
  auto bar_creator = std::bind(&bar::create, std::placeholders::_1);

  factory.register_creator("foo", foo_creator);
  factory.register_creator("bar", bar_creator);

  {
    auto foo_obj = std::move(factory.create("foo", 80));
    foo_obj.reset();
  }

  {
    auto bar_obj = std::move(factory.create("bar", 90));
    bar_obj.reset();
  }
}

Có vẻ tốt với tôi. Làm thế nào bạn sẽ thực hiện (có thể một số ma thuật vĩ mô) đăng ký tĩnh? Chỉ cần tưởng tượng lớp cơ sở là một số lớp phục vụ cho các đối tượng. Các lớp dẫn xuất cung cấp một loại dịch vụ đặc biệt cho các đối tượng đó. Và bạn muốn tăng dần các loại dịch vụ khác nhau bằng cách thêm một lớp xuất phát từ cơ sở cho từng loại dịch vụ đó.
St0fF

2

Mô hình nhà máy

class Point
{
public:
  static Point Cartesian(double x, double y);
private:
};

Và nếu trình biên dịch của bạn không hỗ trợ Tối ưu hóa giá trị trả về, hãy bỏ qua nó, có lẽ nó không chứa nhiều tối ưu hóa ...


Điều này thực sự có thể được coi là một triển khai của mô hình nhà máy?
Dennis

1
@Dennis: Là một trường hợp thoái hóa, tôi sẽ nghĩ như vậy. Vấn đề với Factorynó là nó khá chung chung và bao quát rất nhiều nền tảng; một nhà máy có thể thêm các đối số (tùy thuộc vào môi trường / thiết lập) hoặc cung cấp một số bộ đệm (liên quan đến Flykg / Pools), nhưng những trường hợp này chỉ có ý nghĩa trong một số tình huống.
Matthieu M.

Nếu chỉ thay đổi trình biên dịch sẽ dễ như bạn phát ra âm thanh :)
rozina

@rozina: :) Nó hoạt động tốt trong Linux (gcc / clang tương thích đáng kể); Tôi thừa nhận Windows vẫn còn tương đối kín, mặc dù vậy nó sẽ trở nên tốt hơn trên nền tảng 64 bit (ít bằng sáng chế hơn, nếu tôi nhớ chính xác).
Matthieu M.

Và sau đó bạn có toàn bộ thế giới nhúng với một số trình biên dịch phụ .. :) Tôi đang làm việc với một thế giới không có tối ưu hóa giá trị trả về. Tôi ước nó có mặc dù. Thật không may, chuyển đổi nó không phải là một lựa chọn tại thời điểm này. Hy vọng trong tương lai nó sẽ được cập nhật hoặc chúng tôi sẽ thực hiện chuyển đổi cho sth khác :)
rozina

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.