2012-04-25 33 views
11

Tóm tắtDependency Injection và phát triển năng suất

Trong vài tháng qua, tôi đã được lập trình một trọng lượng nhẹ, C# dựa trên công cụ trò chơi với trừu tượng API và thực thể/thành phần/hệ thống kịch bản. Toàn bộ ý tưởng của nó là để giảm bớt quá trình phát triển trò chơi trong XNA, SlimDX và như vậy, bằng cách cung cấp kiến ​​trúc tương tự như của công cụ Unity.

Thiết kế thách thức

Như hầu hết các nhà phát triển trò chơi đã biết, có rất nhiều dịch vụ khác nhau bạn cần truy cập trên khắp mã của bạn. Nhiều nhà phát triển đã sử dụng các trường hợp tĩnh toàn cầu, ví dụ: một trình quản lý Render (hoặc một nhà soạn nhạc), một Scene, Graphicsdevice (DX), Logger, Trạng thái đầu vào, Viewport, Window và vân vân. Có một số cách tiếp cận thay thế cho các cá thể/cá thể tĩnh toàn cục. Một là cung cấp cho mỗi lớp một thể hiện của các lớp mà nó cần truy cập, hoặc thông qua một constructor hoặc constructor/property dependency injection (DI), khác là sử dụng một bộ định vị dịch vụ toàn cầu, như ObjectMap của StructureMap, nơi định vị dịch vụ thường được cấu hình như một container IoC.

Dependency Injection

tôi đã chọn để đi theo con đường DI vì nhiều lý do. Rõ ràng nhất là khả năng kiểm thử, bằng cách lập trình chống lại các giao diện và có tất cả các phụ thuộc của mọi lớp được cung cấp cho chúng thông qua một hàm tạo, các lớp đó được kiểm tra dễ dàng vì container thử nghiệm có thể khởi tạo các dịch vụ được yêu cầu, hoặc các mocks của chúng. mỗi lớp sẽ được kiểm tra. Một lý do khác để làm DI/IoC là, tin hay không, để tăng khả năng đọc mã. Không có quá trình khởi tạo lớn hơn để khởi tạo tất cả các dịch vụ khác nhau và các lớp instantiating thủ công với các tham chiếu đến các dịch vụ được yêu cầu. Cấu hình Kernel (NInject)/Registry (StructureMap) thuận tiện cho một điểm cấu hình duy nhất cho engine/game, nơi các triển khai dịch vụ được chọn và cấu hình.

vấn đề của tôi

  • Tôi thường cảm thấy như tôi đang tạo ra các giao diện cho các giao diện vì lợi ích
  • năng suất của tôi đã đi xuống đáng kể vì tất cả tôi làm là lo lắng về việc làm thế nào để làm điều DI chiều, thay vì cách nhanh chóng và đơn giản toàn cầu tĩnh cách.
  • Trong một số trường hợp, ví dụ: khi khởi tạo các thực thể mới trong thời gian chạy, một thực thể cần truy cập vào thùng chứa/hạt nhân IoC để tạo cá thể. Điều này tạo ra một sự phụ thuộc vào container IoC chính nó (ObjectFactory trong SM, một cá thể của hạt nhân trong Ninject), mà thực sự đi ngược lại lý do để sử dụng nó ngay từ đầu. Vấn đề đó được giải quyết như thế nào? Nhà máy trừu tượng đến với tâm trí, nhưng điều đó chỉ phức tạp hơn nữa mã.
  • Tùy thuộc vào yêu cầu dịch vụ, các nhà xây dựng của một số lớp có thể rất lớn, điều này sẽ làm cho lớp hoàn toàn vô dụng trong các ngữ cảnh khác và nếu không sử dụng IoC.

Về cơ bản, DI/IoC làm chậm đáng kể năng suất của tôi và trong một số trường hợp làm phức tạp thêm mã và kiến ​​trúc. Do đó tôi không chắc chắn liệu đó có phải là con đường tôi nên theo, hoặc chỉ từ bỏ và làm những điều theo cách cũ kỹ.Tôi không tìm kiếm một câu trả lời duy nhất nói những gì tôi nên hoặc không nên làm nhưng một cuộc thảo luận về nếu sử dụng DI là giá trị nó trong thời gian dài như trái ngược với cách sử dụng toàn cầu tĩnh/singleton cách, có thể ưu và khuyết điểm tôi đã bỏ qua và giải pháp có thể cho các vấn đề của tôi được liệt kê ở trên, khi xử lý DI.

Trả lời

19

Bạn có nên quay trở lại kiểu cũ không? Câu trả lời của tôi ngắn gọn là không. DI có nhiều lợi ích cho tất cả các lý do bạn đề cập.

Tôi thường cảm thấy như tôi đang tạo ra các giao diện cho các giao diện vì lợi ích

Nếu bạn đang làm điều này bạn có thể vi phạm các Reused Abstractions Principle (RAP)

Tùy thuộc vào yêu cầu dịch vụ, nhà thầu một số lớp học có thể nhận được rất lớn, điều này sẽ làm cho lớp hoàn toàn vô dụng trong các ngữ cảnh khác ở đâu và nếu không sử dụng IoC.

Nếu lớp nhà thầu của bạn quá lớn và phức tạp, đây là cách tốt nhất để cho bạn thấy rằng bạn đang vi phạm một nguyên tắc rất quan trọng khác: Single Reponsibility Principle. Trong trường hợp này là lúc để trích xuất và cấu trúc lại mã của bạn thành các lớp khác nhau, số lượng phụ thuộc được đề xuất là khoảng 4.

Để làm DI bạn không cần phải có giao diện, DI chỉ là cách bạn có được sự phụ thuộc vào đối tượng của bạn. Tạo giao diện có thể là một cách cần thiết để có thể thay thế một sự phụ thuộc cho các mục đích thử nghiệm. Trừ khi đối tượng của sự phụ thuộc là:

  1. dễ dàng để cô lập
  2. Không nói chuyện với hệ thống con bên ngoài (hệ thống tập tin vv)

Bạn có thể tạo sự phụ thuộc của bạn như là một lớp trừu tượng, hoặc bất kỳ lớp nào mà các phương pháp bạn muốn thay thế là ảo. Tuy nhiên các giao diện tạo ra cách tách rời tốt nhất của một phụ thuộc.

Trong một số trường hợp, ví dụ: khi khởi tạo các thực thể mới trong thời gian chạy, một cần quyền truy cập vào thùng chứa/hạt nhân IoC để tạo cá thể. Điều này tạo ra một sự phụ thuộc vào container IoC chính nó (ObjectFactory trong SM, một thể hiện của hạt nhân trong Ninject), mà thực sự đi chống lại lý do cho việc sử dụng một ở nơi đầu tiên. Làm cách nào để có thể được giải quyết ? Các nhà máy trừu tượng đến với tâm trí, nhưng chỉ cần thêm làm phức tạp mã.

Theo như sự phụ thuộc vào thùng chứa IOC, bạn không bao giờ nên phụ thuộc vào nó trong các lớp khách hàng của bạn. Và họ không phải làm như vậy.

Để sử dụng lần đầu tiên tiêm phụ thuộc đúng cách là hiểu khái niệm về Composition Root. Đây là nơi duy nhất mà vùng chứa của bạn nên được tham chiếu. Tại thời điểm này toàn bộ đồ thị đối tượng của bạn được xây dựng. Một khi bạn hiểu điều này bạn sẽ nhận ra bạn không bao giờ cần container trong khách hàng của bạn. Vì mỗi khách hàng chỉ được tiêm phụ thuộc của nó.

Ngoài ra còn có nhiều mẫu creational khác mà bạn có thể làm theo để thực hiện xây dựng dễ dàng hơn: Giả sử bạn muốn xây dựng một đối tượng với nhiều phụ thuộc như thế này:

new SomeBusinessObject(
    new SomethingChangedNotificationService(new EmailErrorHandler()), 
    new EmailErrorHandler(), 
    new MyDao(new EmailErrorHandler())); 

Bạn có thể tạo một nhà máy bê tông mà biết làm thế nào để xây dựng này:

public static class SomeBusinessObjectFactory 
{ 
    public static SomeBusinessObject Create() 
    { 
     return new SomeBusinessObject(
      new SomethingChangedNotificationService(new EmailErrorHandler()), 
      new EmailErrorHandler(), 
      new MyDao(new EmailErrorHandler())); 
    } 
} 

Và sau đó sử dụng nó như thế này:

SomeBusinessObject bo = SomeBusinessObjectFactory.Create(); 

Bạn cũng có thể sử dụng mans di nghèo và tạo ra một nhà xây dựng mà không có đối số tại tất cả:

public SomeBusinessObject() 
{ 
    var errorHandler = new EmailErrorHandler(); 
    var dao = new MyDao(errorHandler); 
    var notificationService = new SomethingChangedNotificationService(errorHandler); 
    Initialize(notificationService, errorHandler, dao); 
} 

protected void Initialize(
    INotificationService notifcationService, 
    IErrorHandler errorHandler, 
    MyDao dao) 
{ 
    this._NotificationService = notifcationService; 
    this._ErrorHandler = errorHandler; 
    this._Dao = dao; 
} 

Sau đó, nó chỉ có vẻ như nó đã từng làm việc:

SomeBusinessObject bo = new SomeBusinessObject(); 

Sử dụng DI Poor Man được coi xấu khi triển khai mặc định của bạn nằm trong thư viện bên thứ ba bên ngoài, nhưng ít xấu hơn khi bạn có triển khai mặc định tốt.

Sau đó hiển nhiên có tất cả các vùng chứa DI, Trình xây dựng đối tượng và các mẫu khác.

Vì vậy, tất cả những gì bạn cần là nghĩ về một mẫu hình sáng tạo tốt cho đối tượng của bạn. Đối tượng của bạn không nên quan tâm cách tạo ra các phụ thuộc, thực tế nó làm cho chúng phức tạp hơn và khiến chúng kết hợp 2 loại logic. Vì vậy, tôi không tin tưởng bằng cách sử dụng DI nên mất năng suất.

Có một số trường hợp đặc biệt mà đối tượng của bạn không thể chỉ lấy một cá thể được tiêm vào nó. Trường hợp cuộc đời nói chung là ngắn hơn và trường hợp on-the-fly được yêu cầu. Trong trường hợp này bạn nên tiêm Factory vào đối tượng là một phụ thuộc:

public interface IDataAccessFactory 
{ 
    TDao Create<TDao>(); 
} 

Như bạn có thể thấy phiên bản này là chung chung bởi vì nó có thể tận dụng một container IoC để tạo ra nhiều loại hình (Hãy lưu ý mặc dù các container IoC vẫn không hiển thị với khách hàng của tôi).

public class ConcreteDataAccessFactory : IDataAccessFactory 
{ 
    private readonly IocContainer _Container; 

    public ConcreteDataAccessFactory(IocContainer container) 
    { 
     this._Container = container; 
    } 

    public TDao Create<TDao>() 
    { 
     return (TDao)Activator.CreateInstance(typeof(TDao), 
      this._Container.Resolve<Dependency1>(), 
      this._Container.Resolve<Dependency2>()) 
    } 
} 

Thông báo tôi đã sử dụng activator mặc dù tôi đã có một container IoC, đây là điều quan trọng cần lưu ý rằng các nhà máy cần phải xây dựng một thể hiện mới của đối tượng và không chỉ đảm nhận container sẽ cung cấp một ví dụ mới là đối tượng có thể được đăng ký với các vòng đời khác nhau (Singleton, ThreadLocal, vv). Tuy nhiên tùy thuộc vào container bạn đang sử dụng một số có thể tạo ra các nhà máy này cho bạn. Tuy nhiên, nếu bạn chắc chắn đối tượng được đăng ký với thời gian trôi qua, bạn có thể giải quyết nó một cách đơn giản.

EDIT: Thêm lớp với Abstract Factory phụ thuộc:

public class SomeOtherBusinessObject 
{ 
    private IDataAccessFactory _DataAccessFactory; 

    public SomeOtherBusinessObject(
     IDataAccessFactory dataAccessFactory, 
     INotificationService notifcationService, 
     IErrorHandler errorHandler) 
    { 
     this._DataAccessFactory = dataAccessFactory; 
    } 

    public void DoSomething() 
    { 
     for (int i = 0; i < 10; i++) 
     { 
      using (var dao = this._DataAccessFactory.Create<MyDao>()) 
      { 
       // work with dao 
       // Console.WriteLine(
       //  "Working with dao: " + dao.GetHashCode().ToString()); 
      } 
     } 
    } 
} 

Về cơ bản làm DI/IoC làm chậm đáng kể xuống suất của tôi và trong một số trường hợp phức tạp thêm mã và kiến ​​trúc

Đánh dấu Seeman đã viết một blog tuyệt vời về chủ đề này, và trả lời câu hỏi: Phản ứng đầu tiên của tôi đối với loại câu hỏi đó là: bạn nói mã được ghép nối lỏng lẻo khó hiểu hơn. Khó hơn cái gì?

Loose Coupling and the Big Picture

EDIT: Cuối cùng tôi muốn chỉ ra rằng không phải mọi đối tượng và sự phụ thuộc nhu cầu hoặc cần phải phụ thuộc tiêm, đầu tiên xem xét nếu những gì bạn đang sử dụng là thực sự được coi là một sự phụ thuộc:

gì phụ thuộc?

  • Cấu hình ứng dụng
  • tài nguyên hệ thống (Clock)
  • Bên Thứ Ba Libraries
  • Cơ sở dữ liệu
  • WCF/Mạng Dịch vụ
  • Hệ thống bên ngoài (File/Email)

Bất kỳ của các đối tượng trên hoặc cộng tác viên có thể nằm ngoài tầm kiểm soát của bạn và gây ra ef phụ fects và sự khác biệt trong hành vi và làm cho nó khó khăn để kiểm tra. Đây là những thời điểm để xem xét một Abstraction (Class/Interface) và sử dụng DI.

Điều gì không phụ thuộc, không thực sự cần DI?

  • List<T>
  • MemoryStream
  • Strings/Primitives
  • Leaf Objects/dto của

Đối tượng như trên chỉ có thể được khởi tạo khi cần sử dụng từ khóa new. Tôi sẽ không đề nghị sử dụng DI cho các đối tượng đơn giản như vậy trừ khi có những lý do cụ thể. Xem xét câu hỏi nếu đối tượng nằm dưới sự kiểm soát hoàn toàn của bạn và không gây ra bất kỳ biểu đồ đối tượng bổ sung hoặc tác dụng phụ nào trong hành vi (ít nhất là bất kỳ thứ gì bạn muốn thay đổi/kiểm soát hành vi hoặc kiểm tra). Trong trường hợp này chỉ đơn giản là mới chúng lên.

Tôi đã đăng rất nhiều liên kết tới bài đăng của Mark Seeman, nhưng tôi thực sự khuyên bạn nên đọc các bài đăng trên blog và sách của mình.

+0

Trả lời tuyệt vời, ngay trên chủ đề, cảm ơn bạn! Tôi đã đọc cả hai bài báo mà bạn đã liên kết, cả hai đều là một nguồn thông tin tuyệt vời và thực sự đã trả lời rất nhiều câu hỏi khác mà tôi có trong đầu. RAP vi phạm và SRP nổi bật như những điều tốt đẹp để ghi nhớ trong trường hợp của tôi. Một điều cuối cùng nổi bật từ câu trả lời của bạn, bạn đề cập đến việc sử dụng các nhà máy trừu tượng để ẩn một container IoC từ máy khách của bạn, liệu nó có được cho một nhà máy trừu tượng phụ thuộc vào một container IoC không? – edvaldig

+1

Cảm ơn, tôi rất vui vì điều này đã giúp bạn, và tôi sẽ nói có điều này là rất có thể chấp nhận cho nhà máy trừu tượng biết về thùng chứa, vì nhà máy hoạt động trong cùng một lớp với bố cục gốc là 'keo' và nó không chứa logic nghiệp vụ, nó chỉ điều chỉnh các trường hợp cụ thể. Vì vậy, trong một môi trường thử nghiệm, bạn thậm chí có thể không sử dụng một thùng chứa IOC và bạn sẽ cần một nhà máy khác vì loại bê tông sẽ khác nhau – Andre

+0

Xin chào, tôi vừa thêm một bản chỉnh sửa khác vào cuối để bạn hiểu về phụ thuộc là gì lo lắng về bất kỳ sự quản lý phụ thuộc nào cho những đối tượng đơn giản và không yêu cầu nó. – Andre

Các vấn đề liên quan