Nếu Singletons là xấu thì tại sao Service Container lại tốt?


91

Tất cả chúng ta đều biết Singleton tồi tệ như thế nào vì họ che giấu sự phụ thuộc và vì những lý do khác .

Nhưng trong một khuôn khổ, có thể có nhiều đối tượng chỉ cần được khởi tạo một lần và được gọi từ mọi nơi (logger, db, v.v.).

Để giải quyết vấn đề này, tôi đã được yêu cầu sử dụng cái gọi là "Objects Manager" (hoặc Service Container như symfony) lưu trữ nội bộ mọi tham chiếu đến Services (logger, v.v.).

Nhưng tại sao một Nhà cung cấp Dịch vụ không tệ như một Singleton thuần túy?

Nhà cung cấp dịch vụ cũng ẩn các phụ thuộc và họ chỉ hoàn thành việc tạo ra istance đầu tiên. Vì vậy, tôi thực sự đang đấu tranh để hiểu lý do tại sao chúng ta nên sử dụng một nhà cung cấp dịch vụ thay vì đơn lẻ.

Tái bút. Tôi biết rằng để không ẩn các phụ thuộc, tôi nên sử dụng DI (như Misko đã nêu)

Thêm vào

Tôi muốn nói thêm: Ngày nay, các singleton không phải là điều xấu xa, người tạo ra PHPUnit đã giải thích điều đó ở đây:

DI + Singleton giải quyết vấn đề:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

điều đó khá thông minh ngay cả khi điều này không giải quyết được mọi vấn đề.

Ngoài DI và Vùng chứa dịch vụ, giải pháp nào tốt được chấp nhận để truy cập các đối tượng trợ giúp này không?


2
@yes Chỉnh sửa của bạn đang tạo ra những giả định sai lầm. Không có cách nào Sebastian gợi ý rằng đoạn mã đang làm cho việc sử dụng Singleons ít gặp vấn đề hơn. Đó chỉ là một cách để tạo mã mà nếu không sẽ không thể kiểm tra được nhiều hơn. Nhưng nó vẫn là mã có vấn đề. Trên thực tế, anh ấy ghi chú một cách rõ ràng: "Chỉ vì bạn có thể, không có nghĩa là bạn nên". Giải pháp chính xác vẫn là không sử dụng Singletons.
Gordon

3
@yes tuân theo nguyên tắc SOLID.
Gordon

19
Tôi phản đối khẳng định rằng những người độc thân là xấu. Chúng có thể bị lạm dụng, có nhưng bất kỳ công cụ nào cũng vậy . Một con dao có thể được sử dụng để cứu một mạng sống hoặc kết thúc nó. Một chiếc cưa máy có thể dọn rừng để ngăn cháy rừng hoặc nó có thể làm mất một phần khá lớn cánh tay của bạn nếu bạn không biết mình đang làm gì. Học cách sử dụng các công cụ của bạn một cách khôn ngoan và đừng coi lời khuyên là phúc âm - theo cách đó là tâm trí thiếu suy nghĩ.
paxdiablo

4
@paxdiablo nhưng họ xấu. Singleton vi phạm SRP, OCP và DIP. Họ giới thiệu trạng thái toàn cầu và kết hợp chặt chẽ vào ứng dụng của bạn và sẽ khiến API của bạn nói dối về sự phụ thuộc của nó. Tất cả điều này sẽ ảnh hưởng tiêu cực đến khả năng bảo trì, khả năng đọc và khả năng kiểm tra mã của bạn. Có thể có một số trường hợp hiếm hoi mà những nhược điểm này lớn hơn những lợi ích nhỏ, nhưng tôi cho rằng 99% bạn không cần Singleton. Đặc biệt là trong PHP, nơi mà Singletons chỉ là duy nhất cho Yêu cầu dù sao và thật đơn giản để tập hợp các biểu đồ cộng tác từ một Builder.
Gordon

5
Không, tôi không tin như vậy. Một công cụ là một phương tiện để thực hiện một chức năng, thường là bằng cách nào đó làm cho nó dễ dàng hơn, mặc dù một số (emacs?) Có sự khác biệt hiếm hoi là làm cho nó khó hơn :-) Trong điều này, một singleton không khác gì một cây cân bằng hoặc một trình biên dịch . Nếu bạn cần đảm bảo chỉ một bản sao của một đối tượng, một singleton sẽ thực hiện điều này. Liệu nó có hoạt động tốt hay không có thể được tranh luận nhưng tôi không tin bạn có thể tranh luận rằng nó hoàn toàn không làm được. Và có thể có nhiều cách tốt hơn, chẳng hạn như cưa máy nhanh hơn cưa tay, hoặc súng bắn đinh so với búa. Điều đó không làm cho cưa tay / búa trở thành một công cụ ít hơn.
paxdiablo

Câu trả lời:


76

Service Locator chỉ là một trong hai tệ nạn để nói. "Ít hơn" sôi lên với bốn sự khác biệt này ( ít nhất tôi không thể nghĩ về bất kỳ khác ngay bây giờ ):

Nguyên tắc trách nhiệm đơn lẻ

Vùng chứa dịch vụ không vi phạm Nguyên tắc trách nhiệm đơn lẻ như Singleton làm. Singletons kết hợp việc tạo đối tượng và logic nghiệp vụ, trong khi Vùng chứa dịch vụ chịu trách nhiệm quản lý nghiêm ngặt các vòng đời đối tượng trong ứng dụng của bạn. Về mặt đó Service Container tốt hơn.

Khớp nối

Singleton thường được mã hóa cứng vào ứng dụng của bạn do các lệnh gọi phương thức tĩnh, dẫn đến các phụ thuộc được ghép nối chặt chẽ và khó bắt chước trong mã của bạn. Mặt khác SL chỉ là một lớp và nó có thể được tiêm vào. Vì vậy, mặc dù tất cả những gì được phân loại của bạn sẽ phụ thuộc vào nó, nhưng ít nhất nó là sự phụ thuộc được kết hợp lỏng lẻo. Vì vậy, trừ khi bạn triển khai ServiceLocator như một Singleton, điều đó có phần tốt hơn và cũng dễ kiểm tra hơn.

Tuy nhiên, tất cả các lớp sử dụng ServiceLocator bây giờ sẽ phụ thuộc vào ServiceLocator, đây cũng là một dạng kết hợp. Điều này có thể được giảm thiểu bằng cách sử dụng một giao diện cho ServiceLocator, do đó bạn không bị ràng buộc với việc triển khai ServiceLocator cụ thể nhưng các lớp của bạn sẽ phụ thuộc vào sự tồn tại của một số loại Locator trong khi việc không sử dụng ServiceLocator sẽ làm tăng việc tái sử dụng đáng kể.

Phụ thuộc ẩn

Mặc dù vậy, vấn đề ẩn các phụ thuộc vẫn tồn tại rất nhiều. Khi bạn chỉ đưa bộ định vị vào các lớp tiêu dùng của mình, bạn sẽ không biết bất kỳ sự phụ thuộc nào. Nhưng trái ngược với Singleton, SL thường sẽ khởi tạo tất cả các phụ thuộc cần thiết đằng sau hậu trường. Vì vậy, khi bạn tìm nạp một Dịch vụ, bạn sẽ không giống như Misko Hevery trong ví dụ về thẻ CreditCard , ví dụ như bạn không phải khởi tạo tất cả các phần phụ thuộc bằng tay.

Tìm nạp các phụ thuộc từ bên trong phiên bản cũng vi phạm Luật Demeter , quy định rằng bạn không nên đào sâu vào các cộng tác viên. Một phiên bản chỉ nên nói chuyện với các cộng tác viên trực tiếp của nó. Đây là một vấn đề với cả Singleton và ServiceLocator.

Trạng thái toàn cầu

Vấn đề của Global State cũng được giảm nhẹ phần nào vì khi bạn khởi tạo Bộ định vị dịch vụ mới giữa các lần kiểm tra, tất cả các phiên bản đã tạo trước đó cũng bị xóa (trừ khi bạn mắc lỗi và lưu chúng vào thuộc tính tĩnh trong SL). Tất nhiên, điều đó không đúng với bất kỳ trạng thái toàn cục nào trong các lớp do SL quản lý.

Ngoài ra, hãy xem Fowler trên Service Locator vs Dependency Injection để có một cuộc thảo luận chuyên sâu hơn.


Lưu ý về bản cập nhật của bạn và bài viết được liên kết của Sebastian Bergmann về mã thử nghiệm sử dụng Singletons : Không có cách nào khác, Sebastian gợi ý rằng giải pháp được đề xuất giúp việc sử dụng Singleons ít gặp vấn đề hơn. Đó chỉ là một cách để tạo mã mà nếu không sẽ không thể kiểm tra được nhiều hơn. Nhưng nó vẫn là mã có vấn đề. Trên thực tế, anh ấy ghi chú một cách rõ ràng: "Chỉ vì bạn có thể, không có nghĩa là bạn nên".


1
Đặc biệt là khả năng kiểm tra nên được thực thi ở đây. Bạn không thể giả lập các cuộc gọi phương thức tĩnh. Tuy nhiên, bạn có thể mô phỏng các dịch vụ được đưa vào thông qua phương thức khởi tạo hoặc trình cài đặt.
David

44

Mẫu định vị dịch vụ là mẫu chống. Nó không giải quyết được vấn đề để lộ các phụ thuộc (bạn không thể biết khi nhìn vào định nghĩa của một lớp rằng các phụ thuộc của nó là gì vì chúng không được đưa vào, thay vào đó chúng đang bị kéo ra khỏi bộ định vị dịch vụ).

Vì vậy, câu hỏi của bạn là: tại sao các bộ định vị dịch vụ lại tốt? Câu trả lời của tôi là: họ không.

Tránh, tránh, tránh.


6
Có vẻ như bạn không biết gì về giao diện. Lớp chỉ mô tả giao diện cần thiết trong chữ ký của phương thức khởi tạo - và đó là tất cả những gì anh ta cần biết. Bộ định vị dịch vụ được thông qua sẽ triển khai giao diện, vậy thôi. Và nếu IDE kiểm tra việc triển khai giao diện, thì sẽ khá dễ dàng để kiểm soát bất kỳ thay đổi nào.
OZ_

4
@ yes123: Những người nói như vậy là sai, và họ đã sai vì SL là phản mẫu. Câu hỏi của bạn là "tại sao SL tốt?" Câu trả lời của tôi là: họ không.
thợ nề

5
Tôi sẽ không tranh luận về việc SL có phải là một mẫu anit hay không, nhưng những gì tôi sẽ nói là nó ít tệ hơn rất nhiều khi so sánh với singleton và cầu. Bạn không thể kiểm tra một lớp phụ thuộc vào một singleton, nhưng bạn chắc chắn có thể kiểm tra một lớp phụ thuộc vào SL (bạch tạng, bạn có thể vặn thiết kế SL đến mức nó không hoạt động) ... Vì vậy, điều đó đáng lưu ý ...
ircmaxell

3
@Jason bạn cần truyền đối tượng triển khai Giao diện - và đó chỉ là những gì bạn cần biết. Bạn đang tự giới hạn mình bằng cách chỉ định nghĩa về hàm tạo lớp và muốn viết trong hàm tạo tất cả các lớp (không phải giao diện) - đó là một ý tưởng ngu ngốc. Tất cả những gì bạn cần là Giao diện. Bạn có thể kiểm tra thành công lớp này bằng mocks, bạn có thể dễ dàng thay đổi hành vi mà không cần thay đổi mã, không có thêm phụ thuộc và khớp nối - đó là tất cả (nói chung) những gì chúng tôi muốn có trong Dependency Injection.
OZ_

2
Chắc chắn, tôi sẽ chỉ tập hợp Cơ sở dữ liệu, Trình ghi, Đĩa, Mẫu, Bộ nhớ cache và Người dùng vào một đối tượng "Đầu vào" duy nhất, chắc chắn sẽ dễ dàng hơn để biết đối tượng của tôi dựa vào phụ thuộc nào hơn là nếu tôi đã sử dụng một vùng chứa.
Mahn

4

Vùng chứa dịch vụ ẩn các phần phụ thuộc như mẫu Singleton. Bạn có thể muốn đề xuất sử dụng vùng chứa phụ thuộc để thay thế, vì nó có tất cả các ưu điểm của vùng chứa dịch vụ nhưng không có (theo tôi biết) nhược điểm mà vùng chứa dịch vụ có.

Theo như tôi hiểu, sự khác biệt duy nhất giữa hai cái đó là trong vùng chứa dịch vụ, vùng chứa dịch vụ là đối tượng được đưa vào (do đó ẩn các phụ thuộc), khi bạn sử dụng DIC, DIC sẽ tiêm các phụ thuộc thích hợp cho bạn. Lớp được quản lý bởi DIC hoàn toàn không biết thực tế là nó được quản lý bởi DIC, do đó bạn có ít khớp nối hơn, phụ thuộc rõ ràng và kiểm tra đơn vị vui vẻ.

Đây là một câu hỏi hay tại SO giải thích sự khác biệt của cả hai: Sự khác biệt giữa các mẫu Dependency Injection và Service Locator là gì?


"DIC đưa các phụ thuộc thích hợp cho bạn" Điều này cũng không xảy ra với Singleton phải không?
động

5
@ yes123 - Nếu bạn đang sử dụng Singleton, bạn sẽ không đưa nó vào, hầu hết các lần bạn chỉ truy cập nó trên toàn cầu (đó là điểm của Singleton). Tôi cho rằng nếu bạn nói rằng nếu bạn đưa Singleton vào, nó sẽ không ẩn các phụ thuộc, nhưng nó loại bỏ mục đích ban đầu của mẫu Singleton - bạn sẽ tự hỏi mình, nếu tôi không cần lớp này được truy cập trên toàn cầu, tại sao tôi có cần phải biến nó thành Singleton không?
rickchristie

2

Bởi vì bạn có thể dễ dàng thay thế các đối tượng trong Service Container bằng cách
1) kế thừa (lớp Object Manager có thể được kế thừa và các phương thức có thể bị ghi đè)
2) thay đổi cấu hình (trong trường hợp với Symfony)

Và, các Singleton xấu không chỉ vì độ ghép nối cao, mà bởi vì chúng là _ Đơn _tons. Đó là kiến ​​trúc sai đối với hầu hết mọi loại đối tượng.

Với DI 'thuần túy' (trong các hàm tạo), bạn sẽ phải trả một cái giá rất lớn - tất cả các đối tượng phải được tạo trước khi được chuyển vào trong hàm tạo. Nó sẽ có nghĩa là bộ nhớ được sử dụng nhiều hơn và hiệu suất kém hơn. Ngoài ra, không phải lúc nào đối tượng cũng có thể được tạo và truyền trong hàm khởi tạo - chuỗi các phụ thuộc có thể được tạo ... Tiếng Anh của tôi không đủ tốt để thảo luận về điều đó hoàn toàn, hãy đọc về nó trong tài liệu Symfony.


0

Đối với tôi, tôi cố gắng tránh các hằng số toàn cục, các đơn lẻ vì một lý do đơn giản, có những trường hợp tôi có thể cần các API đang chạy.

Ví dụ: tôi có giao diện người dùng và quản trị viên. Bên trong quản trị viên, tôi muốn họ có thể đăng nhập với tư cách người dùng. Xem xét mã bên trong quản trị viên.

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

Điều này có thể thiết lập kết nối cơ sở dữ liệu mới, trình ghi nhật ký mới, v.v. để khởi tạo giao diện người dùng và kiểm tra xem người dùng có thực sự tồn tại, hợp lệ hay không, v.v. Nó cũng sẽ sử dụng các dịch vụ vị trí và cookie riêng biệt thích hợp.

Ý tưởng của tôi về singleton là - Bạn không thể thêm cùng một đối tượng vào bên trong cha hai lần. Ví dụ

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

sẽ để lại cho bạn một thể hiện duy nhất và cả hai biến đều trỏ đến nó.

Cuối cùng, nếu bạn muốn sử dụng phát triển hướng đối tượng, thì hãy làm việc với các đối tượng, không phải với các lớp.


1
vì vậy phương pháp của bạn là truyền $api var xung quanh khuôn khổ của bạn? Tôi không hiểu chính xác những gì bạn muốn nói. Ngoài ra, nếu cuộc gọi add('Logger')trả về cùng một phiên bản về cơ bản, bạn có một bộ lưu trữ dịch vụ
động

Vâng đúng rồi. Tôi gọi chúng là "Bộ điều khiển hệ thống" và chúng nhằm mục đích nâng cao chức năng của API. Theo cách tương tự, việc thêm bộ điều khiển "Auditable" vào một mô hình hai lần sẽ hoạt động theo cùng một cách - chỉ tạo một phiên bản và chỉ một tập hợp các trường kiểm tra.
romaninsh
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.