Tôi nên sử dụng Dependency Injection hay các nhà máy tĩnh?


81

Khi thiết kế một hệ thống, tôi thường gặp phải vấn đề có một loạt các mô-đun (ghi nhật ký, cơ sở dữ liệu, v.v.) được sử dụng bởi các mô-đun khác. Câu hỏi là, làm thế nào để tôi cung cấp các thành phần này cho các thành phần khác. Hai câu trả lời dường như có thể tiêm phụ thuộc hoặc sử dụng mô hình nhà máy. Tuy nhiên cả hai đều có vẻ sai:

  • Các nhà máy làm cho việc kiểm tra trở thành một nỗi đau và không cho phép dễ dàng hoán đổi việc thực hiện. Họ cũng không làm cho các phụ thuộc trở nên rõ ràng (ví dụ: bạn đang kiểm tra một phương thức, không biết thực tế rằng nó gọi một phương thức gọi một phương thức gọi một phương thức sử dụng cơ sở dữ liệu).
  • Dependword tiêm ồ ạt làm phồng danh sách đối số của hàm tạo và nó làm mờ một số khía cạnh trên toàn bộ mã của bạn. Tình huống điển hình là nơi các nhà xây dựng của hơn một nửa lớp trông như thế này(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Đây là một tình huống điển hình tôi gặp vấn đề với: Tôi có các lớp ngoại lệ, sử dụng các mô tả lỗi được tải từ cơ sở dữ liệu, sử dụng truy vấn có tham số cài đặt ngôn ngữ người dùng, trong đối tượng phiên người dùng. Vì vậy, để tạo một Ngoại lệ mới, tôi cần một mô tả, yêu cầu phiên cơ sở dữ liệu và phiên người dùng. Vì vậy, tôi đã cam chịu kéo tất cả các đối tượng này qua tất cả các phương thức của mình chỉ trong trường hợp tôi có thể cần phải ném một ngoại lệ.

Làm thế nào để tôi giải quyết vấn đề như vậy ??


2
Nếu một nhà máy có thể giải quyết tất cả các vấn đề của bạn, có thể bạn chỉ cần đưa nhà máy vào các đối tượng của mình và nhận LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession từ đó.
Giorgio

1
Quá nhiều "Đầu vào" cho một phương thức, chúng được truyền hoặc tiêm, là một vấn đề của chính phương thức thiết kế. Bất cứ điều gì bạn đi với bạn có thể muốn giảm kích thước của các phương thức của bạn một chút (sẽ dễ thực hiện hơn khi bạn tiêm tại chỗ)
Bill K

Giải pháp ở đây không nên làm giảm các đối số. Thay vào đó, xây dựng các khái niệm trừu tượng xây dựng một đối tượng cấp cao hơn, thực hiện tất cả các công việc trong đối tượng và mang lại lợi ích cho bạn.
Alex

Câu trả lời:


74

Sử dụng phép nội xạ phụ thuộc, nhưng bất cứ khi nào danh sách đối số hàm tạo của bạn trở nên quá lớn, hãy cấu trúc lại nó bằng Dịch vụ mặt tiền . Ý tưởng là nhóm một số đối số của hàm tạo lại với nhau, giới thiệu một sự trừu tượng mới.

Ví dụ: bạn có thể giới thiệu một kiểu mới SessionEnvironmentđóng gói a DBSessionProvider, UserSessionvà được tải Descriptions. Để biết những gì trừu tượng có ý nghĩa nhất, tuy nhiên, người ta phải biết chi tiết về chương trình của bạn.

Một câu hỏi tương tự đã được hỏi ở đây trên SO .


9
+1: Tôi nghĩ việc nhóm các đối số của hàm tạo vào các lớp là một ý tưởng rất hay. Nó cũng buộc bạn phải tổ chức các đối số này thành các cấu trúc có ý nghĩa hơn.
Giorgio

5
Nhưng nếu kết quả không phải là một cấu trúc có ý nghĩa thì bạn chỉ đang che giấu sự phức tạp vi phạm SRP. Trong trường hợp này, một cấu trúc lại lớp nên được thực hiện.
danidacar

1
@Giorgio Tôi không đồng ý với một tuyên bố chung rằng "nhóm các đối số của hàm tạo vào các lớp là một ý tưởng rất hay." Nếu bạn đủ điều kiện này với "trong kịch bản này" thì nó sẽ khác.
tymtam

19

Dependword tiêm ồ ạt làm phồng danh sách đối số của hàm tạo và nó làm mờ một số khía cạnh trên toàn bộ mã của bạn.

Từ đó, có vẻ như bạn không hiểu DI đúng - ý tưởng là đảo ngược mô hình khởi tạo đối tượng bên trong một nhà máy.

Vấn đề cụ thể của bạn dường như là một vấn đề OOP tổng quát hơn. Tại sao các đối tượng không thể đưa ra các ngoại lệ bình thường, không thể đọc được trong thời gian chạy của họ và sau đó có một cái gì đó trước khi thử / bắt cuối cùng bắt được ngoại lệ đó và tại thời điểm đó sử dụng thông tin phiên để đưa ra một ngoại lệ mới, đẹp hơn ?

Một cách tiếp cận khác là có một nhà máy ngoại lệ, được chuyển đến các đối tượng thông qua các nhà xây dựng của họ. Thay vì ném một ngoại lệ mới, lớp có thể sử dụng một phương thức của nhà máy (ví dụ throw PrettyExceptionFactory.createException(data).

Hãy nhớ rằng các đối tượng của bạn, ngoài các đối tượng nhà máy của bạn, không bao giờ nên sử dụng newtoán tử. Các trường hợp ngoại lệ thường là một trường hợp đặc biệt, nhưng trong trường hợp của bạn, chúng có thể là một ngoại lệ!


1
Tôi đã đọc ở đâu đó rằng khi danh sách tham số của bạn quá dài không phải vì bạn đang sử dụng tiêm phụ thuộc mà vì bạn cần tiêm phụ thuộc nhiều hơn.
Giorgio

Đó có thể là một trong những lý do - nói chung, tùy thuộc vào ngôn ngữ, hàm tạo của bạn không nên có nhiều hơn 6-8 đối số và không quá 3-4 đối số nên là chính các đối tượng, trừ khi mẫu cụ thể (như Buildermẫu) ra lệnh nó Nếu bạn chuyển các tham số cho hàm tạo của mình vì đối tượng của bạn khởi tạo các đối tượng khác, đó là một trường hợp rõ ràng cho IoC.
Jonathan Rich

12

Bạn đã liệt kê các nhược điểm của mẫu nhà máy tĩnh khá tốt, nhưng tôi không hoàn toàn đồng ý với các nhược điểm của mẫu tiêm phụ thuộc:

Việc tiêm phụ thuộc đó yêu cầu bạn viết mã cho mỗi phụ thuộc không phải là một lỗi, nhưng là một tính năng: Nó buộc bạn phải suy nghĩ xem bạn có thực sự cần những phụ thuộc này hay không, do đó thúc đẩy khớp nối lỏng lẻo. Trong ví dụ của bạn:

Đây là một tình huống điển hình tôi gặp vấn đề với: Tôi có các lớp ngoại lệ, sử dụng các mô tả lỗi được tải từ cơ sở dữ liệu, sử dụng truy vấn có tham số cài đặt ngôn ngữ người dùng, trong đối tượng phiên người dùng. Vì vậy, để tạo một Ngoại lệ mới, tôi cần một mô tả, yêu cầu phiên cơ sở dữ liệu và phiên người dùng. Vì vậy, tôi đã cam chịu kéo tất cả các đối tượng này qua tất cả các phương thức của mình chỉ trong trường hợp tôi có thể cần phải ném một ngoại lệ.

Không, bạn không cam chịu. Tại sao trách nhiệm của logic nghiệp vụ là bản địa hóa các thông báo lỗi của bạn cho một phiên người dùng cụ thể? Điều gì sẽ xảy ra nếu, đôi khi trong tương lai, bạn muốn sử dụng dịch vụ kinh doanh đó từ một chương trình hàng loạt (không có phiên người dùng ...)? Hoặc nếu thông báo lỗi không được hiển thị cho người dùng hiện đang đăng nhập, nhưng người giám sát của anh ta (người có thể thích một ngôn ngữ khác) thì sao? Hoặc nếu bạn muốn sử dụng lại logic nghiệp vụ trên máy khách (không có quyền truy cập vào cơ sở dữ liệu ...) thì sao?

Rõ ràng, nội địa hóa các tin nhắn phụ thuộc vào người xem các tin nhắn này, nghĩa là trách nhiệm của lớp trình bày. Do đó, tôi sẽ đưa ra các ngoại lệ thông thường từ dịch vụ kinh doanh, điều đó xảy ra để mang một mã định danh thông báo mà sau đó có thể tra cứu trình xử lý ngoại lệ của lớp trình bày trong bất kỳ nguồn tin nhắn nào nó sử dụng.

Bằng cách đó, bạn có thể loại bỏ 3 phụ thuộc không cần thiết (UserSession, ExceptionFactory và có thể mô tả), do đó làm cho mã của bạn vừa đơn giản vừa linh hoạt hơn.

Nói chung, tôi chỉ sử dụng các nhà máy tĩnh cho những thứ bạn cần truy cập phổ biến và được đảm bảo có sẵn trong tất cả các môi trường mà chúng tôi có thể muốn chạy mã (chẳng hạn như Ghi nhật ký). Đối với mọi thứ khác, tôi sẽ sử dụng tiêm phụ thuộc cũ đơn giản.


Điều này. Tôi không thể thấy cần phải đánh DB để ném ngoại lệ.
Caleth

1

Sử dụng tiêm phụ thuộc. Sử dụng các nhà máy tĩnh là một việc làm của Service Locatorantipotype. Xem công việc bán kết từ Martin Fowler tại đây - http://martinfowler.com/articles/injection.html

Nếu các đối số hàm tạo của bạn trở nên quá lớn và bạn không sử dụng bộ chứa DI bằng cách viết các nhà máy của riêng bạn để khởi tạo, cho phép nó có thể được cấu hình, bằng XML hoặc ràng buộc triển khai với giao diện.


5
Trình định vị dịch vụ không phải là một phản mẫu - Bản thân Fowler tham chiếu nó trong URL bạn đã đăng. Mặc dù mẫu Định vị dịch vụ có thể bị lạm dụng (giống như cách Singletons bị lạm dụng - để trừu tượng hóa trạng thái toàn cầu), đó là một mẫu rất hữu ích.
Jonathan Rich

1
Thú vị muốn biết. Tôi đã luôn nghe nó được gọi là một mô hình chống.
Sam

4
Nó chỉ là một antipotype nếu bộ định vị dịch vụ được sử dụng để lưu trữ trạng thái toàn cầu. Bộ định vị dịch vụ phải là một đối tượng không trạng thái sau khi khởi tạo và tốt nhất là không thay đổi.
Jonathan Rich

XML không phải là loại an toàn. Tôi sẽ coi đó là một kế hoạch z sau khi mọi cách tiếp cận khác đều thất bại
Richard Tingle

1

Tôi cũng sẽ đi với Dependency Injection. Hãy nhớ rằng DI không chỉ được thực hiện thông qua các nhà xây dựng, mà còn thông qua các setters tài sản. Ví dụ, logger có thể được thêm vào như một tài sản.

Ngoài ra, bạn có thể muốn sử dụng bộ chứa IoC có thể nâng một số gánh nặng cho bạn, ví dụ bằng cách giữ các tham số của hàm tạo cho những thứ cần thiết trong thời gian chạy bằng logic miền của bạn (giữ cho hàm tạo theo cách thể hiện ý định lớp và các phụ thuộc miền thực) và có thể tiêm các lớp trợ giúp khác thông qua các thuộc tính.

Một bước tiến nữa mà bạn có thể muốn đi là Chương trình định hướng theo khía cạnh, được triển khai trong nhiều khung chính. Điều này có thể cho phép bạn chặn (hoặc "khuyên" sử dụng thuật ngữ AspectJ) hàm tạo của lớp và tiêm các thuộc tính có liên quan, có thể được cung cấp một thuộc tính đặc biệt.


4
Tôi tránh DI thông qua setters vì nó giới thiệu một cửa sổ thời gian trong đó đối tượng không ở trạng thái khởi tạo hoàn toàn (giữa hàm tạo và lệnh gọi setter). Hay nói cách khác, nó giới thiệu một lệnh gọi phương thức (phải gọi X trước Y) mà tôi tránh nếu có thể.
RokL

1
DI thông qua setters tài sản là lý tưởng cho các phụ thuộc tùy chọn. Ghi nhật ký là một ví dụ tốt. Nếu bạn cần ghi nhật ký, thì hãy đặt thuộc tính Logger, nếu không, thì đừng đặt nó.
Preston

1

Các nhà máy làm cho việc kiểm tra trở thành một nỗi đau và không cho phép dễ dàng hoán đổi việc thực hiện. Họ cũng không làm cho các phụ thuộc trở nên rõ ràng (ví dụ: bạn đang kiểm tra một phương thức, không biết thực tế rằng nó gọi một phương thức gọi một phương thức gọi một phương thức sử dụng cơ sở dữ liệu).

Tôi không hoàn toàn đồng ý. Ít nhất là không nói chung.

Nhà máy đơn giản:

public IFoo GetIFoo()
{
    return new Foo();
}

Tiêm đơn giản:

myDependencyInjector.Bind<IFoo>().To<Foo>();

Cả hai đoạn mã đều phục vụ cùng một mục đích, chúng thiết lập một liên kết giữa IFooFoo. Mọi thứ khác chỉ là cú pháp.

Thay đổi Foothành ADifferentFoochính xác nhiều nỗ lực trong một trong hai mẫu mã.
Tôi đã nghe mọi người tranh luận rằng tiêm phụ thuộc cho phép sử dụng các ràng buộc khác nhau, nhưng cùng một lý lẽ có thể được đưa ra về việc tạo ra các nhà máy khác nhau. Chọn đúng ràng buộc là chính xác phức tạp như chọn đúng nhà máy.

Phương pháp nhà máy cho phép bạn ví dụ sử dụng Fooở một số nơi và ADifferentFooở những nơi khác. Một số người có thể gọi điều này là tốt (hữu ích nếu bạn cần), một số có thể gọi điều này là xấu (bạn có thể làm một công việc nửa vời trong việc thay thế mọi thứ).
Tuy nhiên, không khó để tránh sự mơ hồ này nếu bạn sử dụng một phương thức duy nhất trả về IFoođể bạn luôn có một nguồn duy nhất. Nếu bạn không muốn tự bắn vào chân mình, thì đừng cầm súng đã được nạp đạn, hoặc đảm bảo không nhắm vào chân bạn.


Dependword tiêm ồ ạt làm phồng danh sách đối số của hàm tạo và nó làm mờ một số khía cạnh trên toàn bộ mã của bạn.

Đây là lý do tại sao một số người thích truy xuất một cách rõ ràng các phụ thuộc trong hàm tạo, như thế này:

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

Tôi đã nghe các đối số pro (không có hàm tạo), tôi đã nghe các đối số con (sử dụng hàm tạo cho phép tự động hóa DI nhiều hơn).

Cá nhân, trong khi tôi đã nhường cho đàn anh của chúng tôi, những người muốn sử dụng các đối số của hàm tạo, tôi đã nhận thấy một vấn đề với danh sách thả xuống trong VS (phía trên bên phải, để duyệt các phương thức của lớp hiện tại) khi các tên phương thức bị mất khi một của các chữ ký phương thức dài hơn màn hình của tôi (=> hàm tạo cồng kềnh).

Ở cấp độ kỹ thuật, tôi cũng không quan tâm. Hoặc là tùy chọn mất khoảng nhiều nỗ lực để gõ. Và vì bạn đang sử dụng DI, nên bạn sẽ không thường gọi một nhà xây dựng bằng tay. Nhưng lỗi UI của Visual Studio khiến tôi không muốn làm khó đối số của hàm tạo.


Như một lưu ý phụ , tiêm phụ thuộc và các nhà máy không loại trừ lẫn nhau . Tôi đã gặp trường hợp thay vì chèn một phụ thuộc, tôi đã chèn một nhà máy tạo ra các phụ thuộc (NInject may mắn cho phép bạn sử dụng Func<IFoo>để bạn không cần phải tạo một lớp nhà máy thực tế).

Các trường hợp sử dụng cho điều này là rất hiếm nhưng chúng tồn tại.


OP hỏi về các nhà máy tĩnh . stackoverflow.com/questions/929021/
trộm

@Basilevs "Câu hỏi là, làm thế nào để tôi cung cấp các thành phần này cho các thành phần khác."
Anthony Rutledge

1
@Basilevs Tất cả những gì tôi nói, ngoài ghi chú bên lề, cũng áp dụng cho các nhà máy tĩnh. Tôi không chắc chắn những gì bạn đang cố gắng chỉ ra cụ thể. Liên kết trong tài liệu tham khảo là gì?
Flater

Một trường hợp sử dụng như vậy của việc tiêm một nhà máy có thể là một trường hợp như vậy khi một người đã nghĩ ra một lớp yêu cầu HTTP trừu tượng và chiến lược năm lớp con đa hình khác, một cho GET, POST, PUT, PATCH và DELETE? Bạn không thể biết phương thức HTTP nào sẽ được sử dụng mỗi lần khi cố gắng tích hợp phân cấp lớp đã nói với bộ định tuyến loại MVC (có thể phụ thuộc một phần vào lớp yêu cầu HTTP. Giao diện thông báo HTTP PSR-7 rất tệ.
Anthony Rutledge

@AnthonyRutledge: Các nhà máy DI có thể có nghĩa là hai điều (1) Một lớp trưng bày nhiều phương thức. Tôi nghĩ rằng đây là những gì bạn đang nói về. Tuy nhiên, điều này không thực sự đặc biệt đối với các nhà máy. Cho dù đó là một lớp logic kinh doanh với một số phương thức công khai, hay một nhà máy với một số phương thức công khai, là một vấn đề về ngữ nghĩa và không có sự khác biệt về kỹ thuật. (2) Trường hợp sử dụng dành riêng cho DI cho các nhà máy là một phụ thuộc không phải của nhà máy được khởi tạo một lần (trong khi tiêm), trong khi phiên bản xuất xưởng có thể được sử dụng để khởi tạo sự phụ thuộc thực tế ở giai đoạn sau (và có thể nhiều lần).
Flater

0

Trong ví dụ giả định này, một lớp nhà máy được sử dụng trong thời gian chạy để xác định loại đối tượng yêu cầu HTTP gửi đến nào để khởi tạo, dựa trên phương thức yêu cầu HTTP. Nhà máy được tiêm một thùng chứa thuốc tiêm phụ thuộc. Điều này cho phép nhà máy đưa ra quyết định thời gian chạy và để cho thùng chứa phụ thuộc tiêm xử lý các phụ thuộc. Mỗi đối tượng yêu cầu HTTP gửi đến có ít nhất bốn phụ thuộc (superglobals và các đối tượng khác).

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

Mã máy khách cho một thiết lập kiểu MVC, trong phạm vi tập trung index.php, có thể trông giống như sau (bỏ qua xác nhận).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

Ngoài ra, bạn có thể loại bỏ tính chất tĩnh (và từ khóa) của nhà máy và cho phép người tiêm phụ thuộc quản lý toàn bộ (do đó, nhà xây dựng đã nhận xét). Tuy nhiên, bạn sẽ phải thay đổi một số (không phải là hằng số) của các tham chiếu thành viên lớp ( self) thành các thành viên thể hiện ( $this).


Downvote mà không có ý kiến ​​là không hữu ích.
Anthony Rutledge
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.