Mẫu thiết kế Dependency Injection & Singleton


88

Làm cách nào để chúng tôi xác định khi nào sử dụng phương pháp tiêm phụ thuộc hoặc mô hình singleton. Tôi đã đọc trên rất nhiều trang web nơi họ nói "Sử dụng tiêm phụ thuộc trên mô hình singleton". Nhưng tôi không chắc mình có hoàn toàn đồng ý với họ hay không. Đối với các dự án quy mô vừa hoặc nhỏ của tôi, tôi chắc chắn thấy việc sử dụng mô hình singleton rất đơn giản.

Ví dụ Logger. Tôi có thể sử dụng Logger.GetInstance().Log(...) Nhưng, thay vì điều này, tại sao tôi cần phải đưa mọi lớp tôi tạo, với phiên bản của trình ghi nhật ký ?.

Câu trả lời:


65

Nếu bạn muốn xác minh những gì được đăng nhập trong một bài kiểm tra, bạn cần tiêm phụ thuộc. Hơn nữa, trình ghi nhật ký hiếm khi là một đơn vị - nói chung bạn có một trình ghi nhật ký cho mỗi lớp của mình.

Xem bài thuyết trình này về thiết kế hướng đối tượng để biết khả năng kiểm tra và bạn sẽ thấy tại sao các singleton lại tệ.

Vấn đề với các singleton là chúng đại diện cho một trạng thái toàn cầu khó dự đoán, đặc biệt là trong các thử nghiệm.

Hãy nhớ rằng một đối tượng có thể là một đơn vị thực tế nhưng vẫn được lấy bằng cách tiêm phụ thuộc, thay vì thông qua Singleton.getInstance().

Tôi chỉ liệt kê một số điểm quan trọng được Misko Hevery đưa ra trong bài thuyết trình của mình. Sau khi xem nó, bạn sẽ đạt được góc nhìn đầy đủ về lý do tại sao nó là tốt hơn để có một đối tượng xác định những gì phụ thuộc của nó là, nhưng không xác định một cách làm thế nào để tạo ra chúng.


89

Singleton giống như chủ nghĩa cộng sản: cả hai đều có vẻ tuyệt vời trên giấy tờ, nhưng lại bùng nổ với các vấn đề trong thực tế.

Mô hình singleton nhấn mạnh không cân xứng vào việc dễ dàng truy cập các đối tượng. Nó hoàn toàn tránh né ngữ cảnh bằng cách yêu cầu mọi người tiêu dùng phải sử dụng đối tượng thuộc phạm vi AppDomain, không để lại tùy chọn cho các triển khai khác nhau. Nó nhúng kiến ​​thức cơ sở hạ tầng vào các lớp của bạn (lệnh gọi đến GetInstance()) đồng thời bổ sung sức mạnh biểu đạt chính xác bằng 0 . Nó thực sự làm giảm sức mạnh biểu đạt của bạn, bởi vì bạn không thể thay đổi việc triển khai được sử dụng bởi một lớp mà không thay đổi nó cho tất cả chúng. Bạn chỉ đơn giản là không thể thêm một lần các phần chức năng.

Ngoài ra, khi lớp Foophụ thuộc vào Logger.GetInstance(), Foođang che giấu sự phụ thuộc của nó khỏi người tiêu dùng một cách hiệu quả. Điều này có nghĩa là bạn không thể hiểu hoàn toàn Foohoặc sử dụng nó một cách tự tin trừ khi bạn đọc nguồn của nó và khám phá ra sự thật mà nó phụ thuộc vào Logger. Nếu bạn không có nguồn, điều đó sẽ hạn chế mức độ bạn có thể hiểu và sử dụng hiệu quả mã mà bạn phụ thuộc vào.

Mô hình singleton, như được triển khai với các thuộc tính / phương thức tĩnh, không chỉ đơn giản là hack xung quanh việc triển khai cơ sở hạ tầng. Nó giới hạn bạn theo vô số cách trong khi không mang lại lợi ích rõ ràng so với các lựa chọn thay thế. Bạn có thể sử dụng nó theo bất kỳ cách nào bạn muốn, nhưng vì có những lựa chọn thay thế khả thi giúp thúc đẩy thiết kế tốt hơn, đó không nên là một phương pháp được khuyến khích.


9
@BryanWatts tất cả những gì đã nói, Singletons vẫn nhanh hơn nhiều và ít bị lỗi hơn trong một ứng dụng quy mô trung bình thông thường. Thông thường, tôi không có nhiều hơn một triển khai khả thi cho Trình ghi nhật ký (tùy chỉnh), vậy tại sao ** tôi cần: 1. Tạo giao diện cho giao diện đó và thay đổi giao diện đó mỗi khi tôi cần thêm / thay đổi thành viên công khai 2. Duy trì một cấu hình DI 3. Che giấu sự thật rằng có một đối tượng duy nhất thuộc loại này trong toàn bộ hệ thống 4. Tự ràng buộc mình với một chức năng nghiêm ngặt gây ra bởi sự Tách rời Mối quan tâm sớm.
Uri Abramson

@UriAbramson: Có vẻ như bạn đã đưa ra quyết định về sự cân bằng mà bạn thích, vì vậy tôi sẽ không cố gắng thuyết phục bạn.
Bryan Watts

@BryanWatts của nó không phải là trường hợp, có lẽ nhận xét của tôi là một chút quá khắc nghiệt nhưng nó thực sự không quan tâm tôi rất nhiều để nghe những gì bạn có thể nói về điểm tôi mang lên ...
Uri Abramson

1
@UriAbramson: Đủ công bằng. Ít nhất bạn có đồng ý rằng việc hoán đổi các triển khai để cô lập trong quá trình thử nghiệm là quan trọng không?
Bryan Watts

5
Việc sử dụng các singleton và tiêm phụ thuộc không loại trừ lẫn nhau. Một singleton có thể triển khai một giao diện, do đó có thể được sử dụng để đáp ứng sự phụ thuộc vào một lớp khác. Thực tế là nó là một singleton không buộc mọi người tiêu dùng phải lấy tham chiếu thông qua phương thức / thuộc tính "GetInstance" của nó.
Oliver

18

Những người khác đã giải thích rất tốt vấn đề với các singlet nói chung. Tôi chỉ muốn thêm một lưu ý về trường hợp cụ thể của Logger. Tôi đồng ý với bạn rằng thường không có vấn đề gì khi truy cập Logger (hay chính xác là root log) dưới dạng singleton, thông qua static getInstance()hoặc getRootLogger()method. (trừ khi bạn muốn xem những gì được ghi lại bởi lớp bạn đang kiểm tra - nhưng theo kinh nghiệm của tôi, tôi khó có thể nhớ lại những trường hợp như vậy khi điều này là cần thiết. Sau đó, một lần nữa, đối với những người khác, đây có thể là một mối quan tâm cấp bách hơn).

IMO thường không phải là một trình ghi nhật ký singleton, vì nó không chứa bất kỳ trạng thái nào liên quan đến lớp bạn đang kiểm tra. Nghĩa là, trạng thái của bộ ghi (và những thay đổi có thể có của nó) không ảnh hưởng gì đến trạng thái của lớp được kiểm tra. Vì vậy, nó không làm cho bài kiểm tra đơn vị của bạn khó khăn hơn nữa.

Giải pháp thay thế sẽ là đưa trình ghi nhật ký qua hàm tạo, vào (gần như) mọi lớp đơn lẻ trong ứng dụng của bạn. Để có tính nhất quán của các giao diện, nó nên được đưa vào ngay cả khi lớp được đề cập không ghi nhật ký bất kỳ thứ gì hiện tại - giải pháp thay thế sẽ là khi bạn phát hiện ra tại một thời điểm nào đó rằng bây giờ bạn cần ghi lại thứ gì đó từ lớp này, bạn cần một trình ghi nhật ký, do đó bạn cần thêm một tham số phương thức khởi tạo cho DI, phá vỡ tất cả mã máy khách. Tôi không thích cả hai tùy chọn này và tôi cảm thấy rằng việc sử dụng DI để ghi nhật ký sẽ chỉ làm phức tạp cuộc sống của tôi để tuân thủ một quy tắc lý thuyết mà không có bất kỳ lợi ích cụ thể nào.

Vì vậy, điểm mấu chốt của tôi là: một lớp được sử dụng (hầu như) phổ biến, nhưng không chứa trạng thái liên quan đến ứng dụng của bạn, có thể được triển khai một cách an toàn dưới dạng Singleton .


1
Ngay cả khi đó, một singleton có thể là một nỗi đau. Nếu bạn nhận thấy trình ghi của mình cần một số tham số bổ sung cho một số trường hợp, bạn có thể tạo một phương pháp mới (và để trình ghi xấu hơn) hoặc phá vỡ tất cả những người sử dụng trình ghi, ngay cả khi họ không quan tâm.
kyoryu

4
@kyoryu, tôi đang nói về trường hợp "thông thường", mà IMHO ngụ ý sử dụng khung ghi nhật ký chuẩn (trên thực tế). (Thường có thể định cấu hình thông qua thuộc tính / tệp XML btw.) Tất nhiên vẫn có ngoại lệ - như mọi khi. Nếu tôi biết ứng dụng của mình đặc biệt về mặt này, tôi sẽ không thực sự sử dụng Singleton. Tuy nhiên, việc nói rằng "điều này có thể hữu ích vào lúc nào đó" hầu như luôn luôn là một nỗ lực lãng phí.
Péter Török

Nếu bạn đã sử dụng DI, nó không phải là kỹ thuật bổ sung nhiều. (BTW, tôi không đồng ý với bạn, tôi là người ủng hộ). Nhiều trình ghi nhật ký yêu cầu một số thông tin "danh mục" hoặc một số thông tin đại loại, và việc thêm các thông số bổ sung có thể gây đau đớn. Ẩn nó sau một giao diện có thể giúp giữ cho mã tiêu thụ sạch sẽ và có thể dễ dàng chuyển sang một khung ghi nhật ký khác.
kyoryu

1
@kyoryu, xin lỗi vì đã giả định sai. Tôi nhìn thấy một phản đối và một nhận xét, vì vậy tôi đã kết nối các dấu chấm - sai cách, hóa ra là :-( Tôi chưa bao giờ có kinh nghiệm cần chuyển sang một khung ghi nhật ký khác, nhưng sau đó, tôi hiểu rằng nó có thể là hợp pháp mối quan tâm trong một số dự án.
Péter Török

5
Hãy sửa cho tôi nếu tôi sai, nhưng singleton sẽ dành cho LogFactory, không phải cho trình ghi nhật ký. Thêm vào đó, LogFactory có thể là dấu phẩy apache hoặc mặt tiền ghi nhật ký Slf4j. Vì vậy, việc chuyển đổi triển khai ghi nhật ký dù sao cũng không khó. Không phải vấn đề thực sự với việc sử dụng DI để đưa vào LogFactory có phải là việc bạn phải truy cập applicationContext ngay bây giờ để tạo mọi phiên bản trong ứng dụng của mình không?
HDave

9

Đó là chủ yếu, nhưng không phải về các thử nghiệm. Những chiếc đĩa đơn rất phổ biến vì nó rất dễ tiêu thụ, nhưng có một số nhược điểm đối với những chiếc đĩa đơn.

  • Khó kiểm tra. Có nghĩa là làm thế nào để tôi đảm bảo trình ghi nhật ký hoạt động đúng.
  • Khó để kiểm tra với. Có nghĩa là nếu tôi đang kiểm tra mã sử dụng trình ghi nhật ký, nhưng nó không phải là trọng tâm của kiểm tra của tôi, tôi vẫn cần đảm bảo rằng env kiểm tra của mình hỗ trợ trình ghi nhật ký
  • Đôi khi bạn không muốn một singlton, nhưng linh hoạt hơn

DI cho phép bạn dễ dàng sử dụng các lớp phụ thuộc của mình - chỉ cần đặt nó vào các hàm khởi tạo và hệ thống cung cấp nó cho bạn - đồng thời cung cấp cho bạn tính linh hoạt trong quá trình kiểm tra và xây dựng.


Không hẳn. Đó là về trạng thái có thể thay đổi được chia sẻ và các phụ thuộc tĩnh làm cho mọi thứ trở nên khó khăn về lâu dài. Thử nghiệm chỉ là một ví dụ rõ ràng và thường xuyên nhất.
kyoryu

2

Khoảng thời gian duy nhất bạn nên sử dụng Singleton thay vì Dependency Injection là nếu Singleton đại diện cho một giá trị bất biến, chẳng hạn như List.Empty hoặc tương tự (giả sử là danh sách bất biến).

Kiểm tra ruột cho một Singleton phải là "liệu tôi có ổn không nếu đây là một biến toàn cục thay vì một Singleton?" Nếu không, bạn đang sử dụng mẫu Singleton để ghi trên một biến toàn cục và nên xem xét một cách tiếp cận khác.


1

Chỉ cần xem bài báo Monostate - đó là một sự thay thế tiện lợi cho Singleton, nhưng nó có một số thuộc tính wierd:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

Đây không phải là loại đáng sợ - bởi vì Mapper thực sự phụ thuộc vào kết nối cơ sở dữ liệu để thực hiện save () - nhưng nếu một trình liên kết khác đã được tạo trước đó - nó có thể bỏ qua bước này để có được các phụ thuộc của nó. Tuy gọn gàng nhưng cũng hơi lộn xộn phải không?


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.