Các lựa chọn thay thế cho mẫu đơn


27

Tôi đã đọc những ý kiến ​​khác nhau về mẫu singleton. Một số duy trì rằng nó nên được tránh bằng mọi giá và những người khác rằng nó có thể hữu ích trong một số tình huống.

Một tình huống trong đó tôi sử dụng singletons là khi tôi cần một nhà máy (giả sử một đối tượng f thuộc loại F) để tạo các đối tượng của một lớp nhất định A. Nhà máy được tạo một lần bằng cách sử dụng một số tham số cấu hình và sau đó được sử dụng mỗi lần một đối tượng loại A được khởi tạo. Vì vậy, mọi phần của mã muốn khởi tạo A tìm nạp singleton f và tạo cá thể mới, ví dụ:

F& f                   = F::instance();
boost::shared_ptr<A> a = f.createA();

Vì vậy, kịch bản chung của tôi là

  1. Tôi chỉ cần một phiên bản của một lớp vì lý do tối ưu hóa (tôi không cần nhiều đối tượng nhà máy) hoặc để chia sẻ trạng thái chung (ví dụ: nhà máy biết có bao nhiêu phiên bản A mà nó vẫn có thể tạo)
  2. Tôi cần một cách để có quyền truy cập vào ví dụ này của F ở những nơi khác nhau của mã.

Tôi không quan tâm đến cuộc thảo luận liệu mẫu này là tốt hay xấu, nhưng giả sử tôi muốn tránh sử dụng một singleton, tôi có thể sử dụng mẫu nào khác?

Các ý tưởng tôi đã có (1) để lấy đối tượng nhà máy từ sổ đăng ký hoặc (2) để tạo nhà máy tại một số thời điểm trong quá trình khởi động chương trình và sau đó chuyển nhà máy xung quanh làm tham số.

Trong giải pháp (1), bản thân sổ đăng ký là một đơn, vì vậy tôi vừa chuyển vấn đề không sử dụng một đơn vị từ nhà máy sang cơ quan đăng ký.

Trong trường hợp (2) Tôi cần một số nguồn (đối tượng) ban đầu mà từ đó đối tượng nhà máy xuất hiện vì vậy tôi sợ rằng tôi sẽ lại rơi vào một singleton khác (đối tượng cung cấp thể hiện nhà máy của tôi). Bằng cách theo dõi lại chuỗi singletons này, tôi có thể giảm vấn đề thành một singleton (toàn bộ ứng dụng) mà tất cả các singleton khác được quản lý trực tiếp hoặc gián tiếp.

Liệu tùy chọn cuối cùng này (sử dụng một singleton ban đầu tạo ra tất cả các đối tượng duy nhất khác và tiêm tất cả các singleton khác vào đúng chỗ) có phải là một giải pháp chấp nhận được không? Đây có phải là giải pháp được đề xuất ngầm khi một người khuyên không nên sử dụng singletons, hoặc các giải pháp khác, ví dụ như trong ví dụ minh họa ở trên là gì?

CHỈNH SỬA

Vì tôi nghĩ rằng điểm của câu hỏi của tôi đã bị một số người hiểu lầm, đây là một số thông tin. Như đã giải thích ví dụ ở đây , từ singleton có thể chỉ ra (a) một lớp với một đối tượng cá thể và (b) một mẫu thiết kế được sử dụng để tạo và truy cập một đối tượng như vậy.

Để làm cho mọi thứ rõ ràng hơn, chúng ta hãy sử dụng thuật ngữ đối tượng duy nhất cho (a) và mẫu đơn cho (b). Vì vậy, tôi biết mô hình singleton và tiêm phụ thuộc là gì (BTW, gần đây tôi đã sử dụng DI rất nhiều để loại bỏ các trường hợp của mẫu singleton khỏi một số mã tôi đang làm việc).

Quan điểm của tôi là trừ khi toàn bộ biểu đồ đối tượng được khởi tạo từ một đối tượng duy nhất sống trên ngăn xếp của phương thức chính, sẽ luôn có nhu cầu truy cập một số đối tượng duy nhất thông qua mẫu singleton.

Câu hỏi của tôi là liệu việc tạo và kết nối đồ thị đối tượng hoàn chỉnh có phụ thuộc vào phương thức chính hay không (ví dụ thông qua một khung DI mạnh mẽ không sử dụng chính mẫu đó) là giải pháp miễn phí duy nhất cho mẫu đơn .


8
Phụ thuộc tiêm ...
Falcon

1
@Falcon: Tiêm phụ thuộc ... cái gì? DI không làm gì để giải quyết vấn đề này, mặc dù nó thường cung cấp cho bạn một cách tiện dụng để che giấu nó.
pdr

@Falcon có nghĩa là container IoC? Điều này sẽ cho phép bạn giải quyết các phụ thuộc trong một dòng duy nhất. Ví dụ: Phụ thuộc.Resolve <IFoo> (); hoặc Dependency.Resolve <IFoo> (). DoS Something ();
CodeART

Đây là một câu hỏi khá cũ, và đã được trả lời. Mặc dù vậy, tôi vẫn rất tốt khi liên kết cái này: stackoverflow.com/questions/162042/NH
TheSilverBONS

Câu trả lời:


7

Tùy chọn thứ hai của bạn là một cách tốt để đi - đó là một kiểu tiêm phụ thuộc, là mô hình được sử dụng để chia sẻ trạng thái trên toàn bộ chương trình của bạn khi bạn muốn tránh các singletons và các biến toàn cục.

Bạn không thể hiểu được thực tế là có thứ gì đó phải tạo ra nhà máy của bạn. Nếu cái gì đó xảy ra là ứng dụng, vì vậy hãy là nó. Điểm quan trọng là nhà máy của bạn không nên quan tâm đối tượng nào đã tạo ra nó và các đối tượng nhận nhà máy không nên phụ thuộc vào nơi nhà máy xuất phát. Đừng để các đối tượng của bạn có được một con trỏ tới singleton ứng dụng và yêu cầu nó cho nhà máy; có ứng dụng của bạn tạo ra nhà máy và cung cấp cho các đối tượng sẽ cần nó.


17

Mục đích của một người độc thân là để thực thi rằng chỉ có một trường hợp có thể tồn tại trong một lĩnh vực nhất định. Điều này có nghĩa là một singleton rất hữu ích nếu bạn có lý do chính đáng để thực thi hành vi của singleton; trong thực tế, điều này hiếm khi xảy ra, và đa xử lý và đa luồng chắc chắn làm mờ ý nghĩa của "duy nhất" - nó có phải là một ví dụ cho mỗi máy, trên mỗi quy trình, trên mỗi luồng, cho mỗi yêu cầu không? Và việc thực hiện singleton của bạn có quan tâm đến các điều kiện chủng tộc không?

Thay vì singleton, tôi thích sử dụng một trong hai:

  • các trường hợp địa phương tồn tại trong thời gian ngắn, ví dụ cho một nhà máy: các lớp nhà máy điển hình có số lượng trạng thái tối thiểu, nếu có, và không có lý do thực sự để giữ cho họ sống sau khi họ phục vụ mục đích của họ; chi phí chung của việc tạo và xóa các lớp không có gì đáng lo ngại trong 99% của tất cả các kịch bản trong thế giới thực
  • chuyển một cá thể xung quanh, ví dụ cho một người quản lý tài nguyên: chúng phải tồn tại lâu dài, bởi vì việc tải tài nguyên rất tốn kém và bạn muốn giữ chúng trong bộ nhớ, nhưng hoàn toàn không có lý do gì để ngăn chặn các trường hợp khác được tạo ra - ai biết được, có lẽ sẽ có ý nghĩa khi có người quản lý tài nguyên thứ hai vài tháng sau ...

Lý do là một singleton ở trạng thái toàn cầu được ngụy trang, điều đó có nghĩa là nó giới thiệu mức độ khớp nối cao trong toàn bộ ứng dụng - bất kỳ phần nào trong mã của bạn có thể lấy ví dụ singleton từ bất cứ đâu với nỗ lực tối thiểu. Nếu bạn sử dụng các đối tượng cục bộ hoặc vượt qua các trường hợp xung quanh, bạn có nhiều quyền kiểm soát hơn và bạn có thể giữ phạm vi của mình nhỏ và các phụ thuộc của bạn được thu hẹp.


3
Đây là một câu trả lời rất hay nhưng tôi không thực sự hỏi tại sao tôi nên / không nên sử dụng mẫu đơn. Tôi biết những rắc rối người ta có thể có với nhà nước toàn cầu. Dù sao, chủ đề của câu hỏi là làm thế nào để có các đối tượng độc đáo và thực hiện miễn phí mẫu đơn.
Giorgio

3

Hầu hết mọi người (bao gồm cả bạn) hoàn toàn hiểu sai về mẫu Singleton thực sự là gì. Mẫu Singleton chỉ có nghĩa là một thể hiện của một lớp tồn tại và có một số cơ chế mã trên toàn bộ ứng dụng để có được một tham chiếu đến thể hiện đó.

Trong sách GoF, phương thức nhà máy tĩnh trả về tham chiếu đến trường tĩnh chỉ là một ví dụ về cách cơ chế đó có thể trông như thế nào và một phương pháp có nhược điểm nghiêm trọng. Thật không may, tất cả mọi người và con chó của họ đã bám vào cơ chế đó và nghĩ rằng đó là tất cả những gì về Singleton.

Các lựa chọn thay thế mà bạn trích dẫn trên thực tế cũng là Singletons, chỉ với một cơ chế khác để có được tài liệu tham khảo. (2) rõ ràng dẫn đến quá nhiều xung quanh, trừ khi bạn chỉ cần tham chiếu ở một vài nơi gần gốc của ngăn xếp cuộc gọi. (1) nghe có vẻ như một khung tiêm phụ thuộc thô - vậy tại sao không sử dụng nó?

Ưu điểm là các khung DI hiện có rất linh hoạt, mạnh mẽ và được kiểm tra tốt. Họ có thể làm nhiều hơn là chỉ quản lý Singletons. Tuy nhiên, để sử dụng đầy đủ các khả năng của chúng, chúng hoạt động tốt nhất nếu bạn cấu trúc ứng dụng của mình theo một cách nhất định, điều này không phải lúc nào cũng có thể: lý tưởng nhất là có các đối tượng trung tâm được thu nhận thông qua khung DI và có tất cả các phụ thuộc của chúng được di chuyển liên tục và sau đó thực hiện.

Chỉnh sửa: Cuối cùng, mọi thứ vẫn phụ thuộc vào phương thức chính. Nhưng bạn đã đúng: cách duy nhất để tránh hoàn toàn việc sử dụng toàn cầu / tĩnh là bằng cách thiết lập mọi thứ từ phương thức chính. Lưu ý rằng DI phổ biến nhất trong môi trường máy chủ nơi phương thức chính mờ và thiết lập mã máy chủ và ứng dụng bao gồm các cuộc gọi lại được khởi tạo và gọi bằng mã máy chủ. Nhưng hầu hết các khung DI cũng cho phép truy cập trực tiếp vào sổ đăng ký trung tâm của họ, ví dụ như ApplicationContext của Spring, đối với "các trường hợp đặc biệt".

Vì vậy, về cơ bản, điều tốt nhất mà mọi người nghĩ ra cho đến nay là sự kết hợp thông minh của hai lựa chọn bạn đã đề cập.


Bạn có nghĩa là tiêm phụ thuộc ý tưởng cơ bản về cơ bản là cách tiếp cận đầu tiên tôi đã phác thảo ở trên (ví dụ: sử dụng sổ đăng ký), cuối cùng là ý tưởng cơ bản?
Giorgio

@Giorgio: DI luôn bao gồm một sổ đăng ký như vậy, nhưng điểm chính làm cho nó tốt hơn là thay vì truy vấn sổ đăng ký bất cứ nơi nào bạn cần một đối tượng, bạn có các đối tượng trung tâm được cư trú quá mức, như tôi đã viết ở trên.
Michael Borgwardt

@Giorgio: Dễ hiểu hơn với một ví dụ cụ thể: giả sử bạn có ứng dụng Java EE. Logic mặt trước của bạn nằm trong một phương thức hành động của Bean được quản lý JSF. Khi người dùng nhấn một nút, bộ chứa JSF tạo một thể hiện của Managed Bean và gọi phương thức hành động. Nhưng trước đó, thành phần DI xem xét các trường của lớp Managed Bean và những trường có chú thích nhất định được điền với tham chiếu đến một đối tượng Dịch vụ theo cấu hình DI và các objec Service đó có cùng một điều xảy ra với chúng . Phương thức hành động sau đó có thể gọi các dịch vụ.
Michael Borgwardt

Một số người (bao gồm cả bạn) không đọc kỹ câu hỏi và hiểu sai những gì thực sự được hỏi.
Giorgio

1
@Giorgio: không có gì trong câu hỏi ban đầu của bạn chỉ ra rằng bạn đã quen với việc tiêm phụ thuộc. Và xem câu trả lời cập nhật.
Michael Borgwardt

3

Có một vài lựa chọn thay thế:

Phụ thuộc tiêm

Mọi đối tượng đều có các phụ thuộc được truyền cho nó khi nó được tạo. Thông thường, một khung hoặc một số lượng nhỏ các lớp nhà máy chịu trách nhiệm tạo và nối dây cho các đối tượng

Đăng ký dịch vụ

Mỗi đối tượng được thông qua một đối tượng đăng ký dịch vụ. Nó cung cấp các phương thức để yêu cầu các đối tượng khác nhau cung cấp các dịch vụ khác nhau. Khung Android sử dụng mẫu này

Quản lý gốc

Có một đối tượng là gốc của biểu đồ đối tượng. Bằng cách theo các liên kết từ đối tượng này, bất kỳ đối tượng nào khác cuối cùng cũng có thể được tìm thấy. Mã có xu hướng trông giống như:

GetSomeManager()->GetRootManager()->GetAnotherManager()->DoActualThingICareAbout()

Ngoài việc chế giễu, DI còn có lợi thế gì khi sử dụng singleton? Đây là một câu hỏi đã gây ra cho tôi một thời gian rất dài. Đối với sổ đăng ký và trình quản lý gốc, không phải họ đã triển khai dưới dạng đơn lẻ hay thông qua DI? (Trên thực tế, bộ chứa IoC một sổ đăng ký, phải không?)
Konrad Rudolph

1
@KonradRudolph, với DI, các phụ thuộc là rõ ràng và đối tượng không đưa ra giả định nào về cách cung cấp tài nguyên đã cho. Với singletons, các phụ thuộc là ẩn và mỗi lần sử dụng làm cho giả định rằng sẽ chỉ có một đối tượng như vậy.
Winston Ewert

@KonradRudolph, các phương thức khác được triển khai bằng Singletons hay DI? Có và không. Cuối cùng, bạn phải truyền các đối tượng vào hoặc lưu trữ các đối tượng ở trạng thái toàn cầu. Nhưng có những khác biệt tinh tế trong cách bạn làm điều này. Ba phiên bản được đề cập ở trên đều tránh sử dụng trạng thái toàn cầu; nhưng cách thức mà mã cuối nhận được các tham chiếu là khác nhau.
Winston Ewert

Việc sử dụng singletons giả sử không có một đối tượng nào nếu singleton thực hiện một giao diện (hoặc thậm chí nếu không) và bạn truyền đối tượng singleton cho các phương thức, thay vì truy vấn Singleton::instancemọi nơi trong mã máy khách.
Konrad Rudolph

@KonradRudolph, sau đó bạn thực sự sử dụng một số phương pháp lai Singleton / DI. Mặt thuần túy của tôi nghĩ rằng bạn nên đi theo cách tiếp cận DI thuần túy. Tuy nhiên, hầu hết các vấn đề với singleton không thực sự áp dụng ở đó.
Winston Ewert

1

Nếu nhu cầu của bạn từ singleton có thể được đưa vào một chức năng duy nhất, tại sao bạn không sử dụng một chức năng đơn giản? Các hàm toàn cầu (có thể là một phương thức tĩnh của lớp F trong ví dụ của bạn) vốn là các singletons, với tính duy nhất được thi hành bởi trình biên dịch và trình liên kết.

class Factory
{
public:
    static Object* createObject(...);
};

Object* obj = Factory::createObject(...);

Phải thừa nhận rằng điều này bị hỏng khi hoạt động của singleton không thể được giảm xuống thành một cuộc gọi chức năng duy nhất, mặc dù có lẽ một nhóm nhỏ các chức năng liên quan có thể giúp bạn thực hiện.

Tất cả những gì đang được nói, mục 1 và 2 trong câu hỏi của bạn cho thấy rõ rằng bạn thực sự chỉ muốn một trong những điều đó. Tùy thuộc vào định nghĩa của bạn về mẫu singleton, bạn đã hoặc đang sử dụng nó hoặc rất gần. Tôi không nghĩ rằng bạn có thể có một thứ gì đó mà không phải là một người độc thân hoặc ít nhất là rất gần với một thứ gì đó. Nó quá gần với ý nghĩa của singleton.

Như bạn đề xuất, đến một lúc nào đó bạn phải có một thứ gì đó, vì vậy có lẽ vấn đề không phải là có một ví dụ duy nhất, mà là thực hiện các bước để ngăn chặn (hoặc ít nhất là không khuyến khích hoặc giảm thiểu) lạm dụng nó. Di chuyển càng nhiều trạng thái ra khỏi "singleton" và vào các tham số càng tốt là một khởi đầu tốt. Thu hẹp giao diện cho singleton cũng có ích, vì có ít cơ hội lạm dụng theo cách đó. Đôi khi bạn chỉ cần mút nó và làm cho singleton rất mạnh mẽ, như heap hoặc hệ thống tập tin.


4
Cá nhân tôi thấy đây chỉ là một cách thực hiện khác của mẫu singleton. Trong trường hợp này, cá thể singleton là lớp chính nó. Cụ thể: nó có tất cả các nhược điểm giống như một singleton "thực sự".
Joachim Sauer

@Randall Cook: Tôi nghĩ rằng câu trả lời của bạn nắm bắt được quan điểm của tôi. Tôi đọc rất thường xuyên rằng singleton rất rất xấu và nên tránh bằng mọi giá. Tôi đồng ý về điều này nhưng mặt khác tôi nghĩ rằng về mặt kỹ thuật là không thể luôn luôn tránh nó. Khi bạn cần "một trong những thứ", bạn phải tạo nó ở đâu đó và truy cập nó theo một cách nào đó.
Giorgio

1

Nếu bạn đang sử dụng một ngôn ngữ đa biến như C ++ hoặc Python, một thay thế cho một lớp singleton là một tập hợp các hàm / biến được gói trong một không gian tên.

Nói một cách khái niệm, một tệp C ++ với các biến toàn cục miễn phí, các hàm toàn cầu miễn phí và các biến tĩnh được sử dụng để ẩn thông tin, tất cả được gói trong một không gian tên, mang lại cho bạn hiệu ứng gần giống như một "lớp" đơn lẻ.

Nó chỉ bị phá vỡ nếu bạn muốn thừa kế. Tôi đã thấy rất nhiều singletons sẽ tốt hơn theo cách này.


0

Đóng gói Singletons

Kịch bản trường hợp. Ứng dụng của bạn là Hướng đối tượng và yêu cầu 3 hoặc 4 singletons cụ thể, ngay cả khi bạn có nhiều lớp hơn.

Trước ví dụ (C ++ như mã giả):

// network info
class Session {
  // ...
};

// current user connection info
class CurrentUser {
  // ...
};

// configuration upon user
class UserConfiguration {
  // ...
};

// configuration upon P.C.,
// example: monitor resolution
class TerminalConfiguration {
  // ...
};

Một cách là loại bỏ một phần một số singletons (toàn cầu), bằng cách gói gọn tất cả chúng thành một singleton duy nhất, có thành viên, các singletons khác.

Bằng cách này, thay vì "một số singletons, cách tiếp cận, so với, không có singleton nào cả, cách tiếp cận", chúng ta có "gói gọn tất cả các singletons thành một, cách tiếp cận".

Sau ví dụ (C ++ như mã giả):

// network info
class NetworkInfoClass {
  // ...
};

// current user connection info
class CurrentUserClass {
  // ...
};

// configuration upon user
// favorite options menues
class UserConfigurationClass {
  // ...
};

// configuration upon P.C.,
// example: monitor resolution
class TerminalConfigurationClass {
  // ...
};

// this class is instantiated as a singleton
class SessionClass {
  public: NetworkInfoClass NetworkInfo;
  public: CurrentUserClass CurrentUser;
  public: UserConfigurationClass UserConfiguration;
  public: TerminalConfigurationClass TerminalConfiguration;

  public: static SessionClass Instance();
};

Xin lưu ý rằng các ví dụ giống mã giả hơn và bỏ qua các lỗi nhỏ hoặc lỗi cú pháp và xem xét giải pháp cho câu hỏi.

Ngoài ra còn có một điều khác cần xem xét, bởi vì, ngôn ngữ lập trình được sử dụng, có thể ảnh hưởng đến cách triển khai thực hiện đơn lẻ hoặc không đơn lẻ của bạn.

Chúc mừng.


0

Yêu cầu ban đầu của bạn:

Vì vậy, kịch bản chung của tôi là

  1. Tôi chỉ cần một phiên bản của một lớp vì lý do tối ưu hóa (tôi không cần> nhiều đối tượng nhà máy) hoặc để chia sẻ trạng thái chung (ví dụ: nhà máy biết có bao nhiêu> phiên bản A mà nó vẫn có thể tạo)
  2. Tôi cần một cách để có quyền truy cập vào ví dụ này của F ở những nơi khác nhau của mã.

Không xếp hàng với định nghĩa của một singleton (và những gì bạn muốn đề cập sau). Từ GoF (phiên bản của tôi là 1995) trang 127:

Đảm bảo một lớp chỉ có một thể hiện và cung cấp một điểm truy cập toàn cầu vào nó.

Nếu bạn chỉ cần một ví dụ, điều đó không ngăn cản bạn tạo thêm.

Nếu bạn muốn một thể hiện duy nhất, có thể truy cập toàn cầu thì hãy tạo một thể hiện có thể truy cập toàn cầu . Không cần phải có một mô hình được đặt tên cho tất cả mọi thứ bạn làm. Và có, các trường hợp truy cập toàn cầu duy nhất thường là xấu. Đặt cho họ một cái tên không làm cho họ bớt xấu đi .

Để tránh khả năng tiếp cận toàn cầu, cách phổ biến 'tạo một ví dụ và chuyển nó đi' hoặc 'có chủ sở hữu của hai đối tượng dán chúng lại với nhau' hoạt động tốt.


0

Làm thế nào về việc sử dụng container IoC? Với một số suy nghĩ, bạn có thể kết thúc với một cái gì đó trên dòng:

Dependency.Resolve<IFoo>(); 

Bạn sẽ phải chỉ định triển khai IFoosử dụng khi khởi động, nhưng đây là cấu hình tắt mà bạn có thể dễ dàng thay thế sau này. Thời gian tồn tại của một cá thể được giải quyết có thể được kiểm soát bình thường bởi một bộ chứa IoC.

Phương pháp void singleton có thể được thay thế bằng:

Dependency.Resolve<IFoo>().DoSomething();

Phương thức getleton tĩnh có thể được thay thế bằng:

var result = Dependency.Resolve<IFoo>().GetFoo();

Ví dụ có liên quan đến .NET, nhưng tôi chắc chắn rất giống với các ngôn ngữ khác.

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.