Sử dụng Func thay vì giao diện cho IoC


14

Bối cảnh: Tôi đang sử dụng C #

Tôi đã thiết kế một lớp, và để cô lập nó, và làm cho việc kiểm tra đơn vị dễ dàng hơn, tôi chuyển qua tất cả các phụ thuộc của nó; nó không có đối tượng khởi tạo trong nội bộ. Tuy nhiên, thay vì tham chiếu các giao diện để lấy dữ liệu cần thiết, tôi có tham chiếu mục đích chung Funcs trả về dữ liệu / hành vi mà nó yêu cầu. Khi tôi tiêm các phụ thuộc của nó, tôi chỉ có thể làm như vậy với các biểu thức lambda.

Đối với tôi, đây có vẻ là một cách tiếp cận tốt hơn bởi vì tôi không phải thực hiện bất kỳ sự chế nhạo tẻ nhạt nào trong quá trình thử nghiệm đơn vị. Ngoài ra, nếu việc thực hiện xung quanh có những thay đổi cơ bản, tôi sẽ chỉ cần thay đổi lớp nhà máy; không có thay đổi trong lớp chứa logic sẽ là cần thiết.

Tuy nhiên, tôi chưa bao giờ thấy IoC được thực hiện theo cách này trước đây, điều này khiến tôi nghĩ rằng có một số cạm bẫy tiềm năng mà tôi có thể đang thiếu. Điều duy nhất tôi có thể nghĩ đến là sự không tương thích nhỏ với phiên bản C # trước đó không xác định Func và đây không phải là vấn đề trong trường hợp của tôi.

Có bất kỳ vấn đề nào với việc sử dụng các đại biểu chung / các hàm bậc cao hơn như Func cho IoC, trái ngược với các giao diện cụ thể hơn không?


2
Những gì bạn đang mô tả là các hàm bậc cao hơn, một khía cạnh quan trọng của lập trình hàm.
Robert Harvey

2
Tôi sẽ sử dụng một đại biểu thay vì Funcvì bạn có thể đặt tên cho các tham số, nơi bạn có thể nêu ý định của họ.
Stefan Hanke

1
liên quan: stackoverflow: ioc-Factory-pros-and-contras-for-interface-vs.-đại biểu . Câu hỏi @TheCatWhisperer chung chung hơn trong khi câu hỏi stackoverflow thu hẹp vào trường hợp đặc biệt "nhà máy"
k3b

Câu trả lời:


11

Nếu một giao diện chỉ chứa một chức năng, không nhiều hơn và không có lý do thuyết phục nào để giới thiệu hai tên (tên giao diện tên chức năng bên trong giao diện), sử dụng Functhay thế có thể tránh mã nồi hơi không cần thiết và trong hầu hết các trường hợp thích hợp hơn - giống như khi bạn bắt đầu thiết kế DTO và nhận ra nó chỉ cần một thuộc tính thành viên.

Tôi đoán nhiều người đã quen với việc sử dụng hơn interfaces, bởi vì tại thời điểm khi tiêm phụ thuộc và IoC đang trở nên phổ biến, không có lớp nào thực sự tương đương với Funclớp trong Java hoặc C ++ (Tôi thậm chí không chắc Funclà có sẵn tại thời điểm đó trong C # không ). Vì vậy, rất nhiều hướng dẫn, ví dụ hoặc sách giáo khoa vẫn thích interfacehình thức, ngay cả khi sử dụng Funcsẽ thanh lịch hơn.

Bạn có thể xem xét câu trả lời trước đây của tôi về nguyên tắc phân tách giao diện và phương pháp Thiết kế dòng chảy của Ralf Hampal. Nghịch lý này thực hiện DI với Funccác tham số dành riêng , với lý do chính xác giống như lý do bạn đã đề cập một mình (và một số chi tiết khác). Vì vậy, như bạn thấy, ý tưởng của bạn không phải là một ý tưởng thực sự mới, hoàn toàn ngược lại.

Và vâng, tôi đã sử dụng phương pháp này một mình, cho mã sản xuất, cho các chương trình cần xử lý dữ liệu dưới dạng một đường ống với một số bước trung gian, bao gồm các thử nghiệm đơn vị cho từng bước. Vì vậy, từ đó tôi có thể cung cấp cho bạn kinh nghiệm đầu tay rằng nó có thể hoạt động rất tốt.


Chương trình này thậm chí không được thiết kế với nguyên tắc phân tách giao diện, về cơ bản chúng chỉ được sử dụng như các lớp trừu tượng. Đây là lý do nữa tại sao tôi đi theo cách tiếp cận này, các giao diện không được suy nghĩ kỹ và hầu như vô dụng vì chỉ có một lớp thực hiện chúng. Nguyên tắc phân biệt giao diện không cần phải được đưa đến mức cực đoan, đặc biệt là bây giờ chúng ta có Func, nhưng trong suy nghĩ của tôi, nó vẫn rất quan trọng.
TheCatWhisperer

1
Chương trình này thậm chí không được thiết kế với nguyên tắc phân biệt giao diện - tốt, theo mô tả của bạn, có lẽ bạn chỉ không biết rằng thuật ngữ này có thể được áp dụng cho loại thiết kế của bạn.
Doc Brown

Bác sĩ, tôi đã đề cập đến mã được tạo bởi những người tiền nhiệm của tôi, mà tôi đang tái cấu trúc, và trong đó lớp mới của tôi có phần phụ thuộc vào. Một phần lý do tôi đã thực hiện với phương pháp này là vì tôi dự định thực hiện các thay đổi lớn cho các phần khác của chương trình trong tương lai, bao gồm cả sự phụ thuộc của lớp này
TheCatWhisperer

1
"... Vào thời điểm khi tiêm phụ thuộc và IoC trở nên phổ biến, không có thực sự tương đương với lớp Func" Doc chính xác là những gì tôi gần như đã trả lời nhưng không thực sự cảm thấy đủ tự tin! Cảm ơn đã xác minh sự nghi ngờ của tôi về điều đó.
Graham

9

Một trong những lợi thế chính mà tôi tìm thấy với IoC là nó sẽ cho phép tôi đặt tên cho tất cả các phụ thuộc của mình thông qua việc đặt tên giao diện của chúng và container sẽ biết nên cung cấp cái nào cho nhà xây dựng bằng cách khớp tên loại. Điều này là thuận tiện và nó cho phép nhiều tên mô tả phụ thuộc hơn Func<string, string>.

Tôi cũng thường thấy rằng ngay cả với một phụ thuộc đơn giản, đôi khi nó cần có nhiều hơn một chức năng - một giao diện cho phép bạn nhóm các chức năng này lại với nhau theo cách tự ghi lại tài liệu, so với việc có nhiều tham số mà tất cả đều đọc Func<string, string>, Func<string, int>.

Chắc chắn có những lúc thật hữu ích khi chỉ cần có một đại biểu được thông qua như một sự phụ thuộc. Đó là một cuộc gọi phán xét khi bạn sử dụng một đại biểu và có một giao diện với rất ít thành viên. Trừ khi nó thực sự rõ ràng mục đích của cuộc tranh luận là gì, tôi thường sẽ sai ở khía cạnh sản xuất mã tự viết; I E. viết một giao diện.


Tôi đã phải gỡ lỗi mã của ai đó, khi người triển khai ban đầu không còn ở đó nữa và tôi có thể nói với bạn từ trải nghiệm đầu tiên, rằng việc tìm ra lambda nào được chạy ở đâu trong ngăn xếp là rất nhiều công việc và một quá trình thực sự bực bội. Nếu người triển khai ban đầu đã sử dụng các giao diện, sẽ rất dễ dàng để tìm ra lỗi mà tôi đang tìm kiếm.
cwap

Cwap, tôi cảm thấy nỗi đau của bạn. Tuy nhiên, trong triển khai cụ thể của tôi, lambdas là tất cả một lớp lót chỉ đến một chức năng duy nhất. Chúng cũng được tạo ra ở một nơi duy nhất.
TheCatWhisperer

3

Có bất kỳ vấn đề nào với việc sử dụng các đại biểu chung / các hàm bậc cao hơn như Func cho IoC, trái ngược với các giao diện cụ thể hơn không?

Không hẳn vậy. Một Func là loại giao diện riêng của nó (theo nghĩa tiếng Anh, không phải nghĩa C #). "Tham số này là thứ cung cấp X khi được hỏi." Func thậm chí còn có lợi ích là chỉ cung cấp thông tin một cách lười biếng khi cần thiết. Tôi làm điều này một chút và đề nghị nó trong chừng mực.

Đối với nhược điểm:

  • Các container IoC thường thực hiện một số phép thuật để kết nối các phụ thuộc theo cách xếp tầng và có thể sẽ không chơi tốt khi một số thứ Tvà một số thứ Func<T>.
  • Funcs có một số lỗi, do đó có thể khó hơn một chút để lý do và gỡ lỗi.
  • Funcs trì hoãn khởi tạo, có nghĩa là lỗi thời gian chạy có thể xuất hiện vào những thời điểm kỳ lạ hoặc hoàn toàn không trong quá trình thử nghiệm. Nó cũng có thể làm tăng cơ hội đặt hàng các vấn đề hoạt động và tùy thuộc vào việc sử dụng của bạn, các bế tắc trong thứ tự khởi tạo.
  • Những gì bạn chuyển vào Func có thể là một sự đóng cửa, với chi phí thấp và các biến chứng đòi hỏi.
  • Gọi Func chậm hơn một chút so với truy cập trực tiếp vào đối tượng. (Không đủ để bạn nhận thấy trong bất kỳ chương trình không tầm thường nào, nhưng nó ở đó)

1
Tôi sử dụng IoC Container để tạo nhà máy theo cách truyền thống, sau đó nhà máy đóng gói các phương thức giao diện vào lambdas, khắc phục vấn đề bạn đề cập ở điểm đầu tiên. Điểm tốt.
TheCatWhisperer

1

Hãy lấy một ví dụ đơn giản-- có lẽ bạn đang tiêm một phương tiện đăng nhập.

Tiêm một lớp

class Worker: IWorker
{
    ILogger _logger;

    Worker(ILogger logger)
    {
        _logger = logger;
    }
    void SomeMethod()
    {
        _logger.Debug("This is a debug log statement.");
    }
}        

Tôi nghĩ rằng đó là khá rõ ràng những gì đang xảy ra. Hơn nữa, nếu bạn đang sử dụng bộ chứa IoC, bạn thậm chí không cần tiêm bất cứ thứ gì một cách rõ ràng, bạn chỉ cần thêm vào gốc thành phần của mình:

container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();

Khi gỡ lỗi Worker, nhà phát triển chỉ cần tham khảo gốc thành phần để xác định lớp cụ thể nào đang được sử dụng.

Nếu một nhà phát triển cần logic phức tạp hơn, anh ta có toàn bộ giao diện để làm việc với:

    void SomeMethod()
    { 
       if (_logger.IsDebugEnabled) {
           _logger.Debug("This is a debug log statement.");
       }
    }

Tiêm một phương pháp

class Worker
{
    Action<string> _methodThatLogs;

    Worker(Action<string> methodThatLogs)
    {
        _methodThatLogs = methodThatLogs;
    }
    void SomeMethod()
    {
        _methodThatLogs("This is a logging statement");
    }
}        

Đầu tiên, lưu ý rằng tham số constructor có tên dài hơn bây giờ , methodThatLogs. Điều này là cần thiết bởi vì bạn không thể nói những gì Action<string>cần phải làm. Với giao diện, nó đã hoàn toàn rõ ràng, nhưng ở đây chúng ta phải dùng đến việc dựa vào việc đặt tên tham số. Điều này dường như ít đáng tin cậy và khó thực thi hơn trong quá trình xây dựng.

Bây giờ, làm thế nào để chúng ta tiêm phương pháp này? Chà, container IoC sẽ không làm điều đó cho bạn. Vì vậy, bạn còn lại tiêm nó một cách rõ ràng khi bạn khởi tạo Worker. Điều này đặt ra một số vấn đề:

  1. Đó là nhiều công việc để khởi tạo một Worker
  2. Các nhà phát triển cố gắng gỡ lỗi Workersẽ thấy khó khăn hơn để tìm ra trường hợp cụ thể nào được gọi. Họ không thể chỉ tham khảo các gốc thành phần; họ sẽ phải theo dõi thông qua mã.

Nếu chúng ta cần logic phức tạp hơn thì sao? Kỹ thuật của bạn chỉ phơi bày một phương pháp. Bây giờ tôi cho rằng bạn có thể nướng những thứ phức tạp vào lambda:

var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );

nhưng khi bạn đang viết bài kiểm tra đơn vị của mình, làm thế nào để bạn kiểm tra biểu thức lambda đó? Nó ẩn danh, vì vậy khung kiểm tra đơn vị của bạn không thể khởi tạo trực tiếp. Có thể bạn có thể tìm ra một số cách thông minh để làm điều đó, nhưng nó có thể sẽ là một PITA lớn hơn so với sử dụng giao diện.

Tóm tắt về sự khác biệt:

  1. Chỉ tiêm một phương pháp làm cho việc suy luận mục đích trở nên khó khăn hơn, trong khi một giao diện truyền đạt rõ ràng mục đích.
  2. Chỉ tiêm một phương thức làm lộ ít chức năng hơn đối với lớp nhận tiêm. Ngay cả khi bạn không cần nó ngày hôm nay, bạn có thể cần nó vào ngày mai.
  3. Bạn không thể tự động chỉ tiêm một phương thức bằng cách sử dụng bộ chứa IoC.
  4. Bạn không thể biết từ gốc thành phần mà lớp cụ thể đang làm việc trong một trường hợp cụ thể.
  5. Đó là một vấn đề để đơn vị kiểm tra chính biểu thức lambda.

Nếu bạn ổn với tất cả những điều trên, thì bạn chỉ nên tiêm phương pháp. Nếu không, tôi khuyên bạn nên gắn bó với truyền thống và tiêm giao diện.


Tôi nên đã chỉ định, lớp tôi đang làm là một lớp logic quá trình. Nó dựa vào các lớp bên ngoài để có được dữ liệu cần thiết để đưa ra quyết định sáng suốt, một tác dụng phụ gây ra lớp như vậy một logger không được sử dụng. Một vấn đề với ví dụ của bạn là OO kém, đặc biệt là trong bối cảnh sử dụng bộ chứa IoC. Thay vì sử dụng một câu lệnh if, làm tăng thêm độ phức tạp cho lớp, nó chỉ đơn giản được truyền vào một bộ ghi chép trơ. Ngoài ra, việc đưa ra logic trong lambdas thực sự sẽ khiến việc kiểm tra trở nên khó khăn hơn ... đánh bại mục đích sử dụng của nó, đó là lý do tại sao nó không được thực hiện.
TheCatWhisperer

Lambdas trỏ đến một phương thức giao diện trong ứng dụng và xây dựng các cấu trúc dữ liệu tùy ý trong các thử nghiệm đơn vị.
TheCatWhisperer

Tại sao mọi người luôn tập trung vào những chi tiết vụn vặt về những gì rõ ràng là một ví dụ độc đoán? Chà, nếu bạn khăng khăng nói về việc ghi nhật ký thích hợp, một logger trơ sẽ là một ý tưởng tồi trong một số trường hợp, ví dụ như khi chuỗi được ghi là tốn kém để tính toán.
John Wu

Tôi không chắc ý của bạn là gì khi "lambdas trỏ đến một phương thức Giao diện." Tôi nghĩ rằng bạn phải thực hiện một phương pháp phù hợp với chữ ký của đại biểu. Nếu phương thức xảy ra thuộc về một giao diện, đó là sự cố; không có kiểm tra biên dịch hoặc thời gian chạy để đảm bảo rằng nó thực hiện. Tôi có hiểu lầm không? Có lẽ bạn có thể bao gồm một ví dụ mã trong bài viết của bạn?
John Wu

Tôi không thể nói tôi đồng ý với kết luận của bạn ở đây. Đối với một điều, bạn nên ghép lớp của mình với số lượng phụ thuộc tối thiểu, vì vậy sử dụng lambdas đảm bảo rằng mỗi mục được chèn chính xác là một phụ thuộc và không phải là sự kết hợp của nhiều thứ trong một giao diện. Bạn cũng có thể hỗ trợ một mô hình xây dựng Workermột phụ thuộc khả thi tại một thời điểm, cho phép mỗi lambda được tiêm độc lập cho đến khi tất cả các phụ thuộc được thỏa mãn. Điều này tương tự như ứng dụng một phần trong FP. Hơn nữa, sử dụng lambdas giúp loại bỏ trạng thái ẩn và khớp nối ẩn giữa các phụ thuộc.
Aaron M. Eshbach

0

Hãy xem xét đoạn mã sau tôi đã viết từ lâu:

public interface IPhysicalPathMapper
{
    /// <summary>
    /// Gets the physical path represented by the relative URL.
    /// </summary>
    /// <param name="relativeURL"></param>
    /// <returns></returns>
    String GetPhysicalPath(String relativeURL);
}

public class EmailBuilder : IEmailBuilder
{
    public IPhysicalPathMapper PhysicalPathMapper { get; set; }
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templateRelativeURL, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(this.PhysicalPathMapper.GetPhysicalPath(templateRelativeURL));

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

Nó lấy một vị trí tương đối vào một tệp mẫu, tải nó vào bộ nhớ, kết xuất nội dung thư và lắp ráp đối tượng e-mail.

Bạn có thể nhìn IPhysicalPathMappervà nghĩ, "Chỉ có một chức năng. Đó có thể là một Func." Nhưng thật ra, vấn đề ở đây là IPhysicalPathMapperthậm chí không tồn tại. Một giải pháp tốt hơn nhiều là chỉ cần tham số hóa đường dẫn :

public class EmailBuilder : IEmailBuilder
{
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templatePath, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(templatePath);

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

Điều này đặt ra một loạt các câu hỏi khác để cải thiện mã này. Ví dụ, có lẽ nó chỉ nên chấp nhận một EmailTemplate, và sau đó có lẽ nó nên chấp nhận một mẫu được kết xuất sẵn, và sau đó có lẽ nó chỉ nên được xếp hàng.

Đây là lý do tại sao tôi không thích đảo ngược kiểm soát như một mô hình phổ biến. Nó thường được coi là giải pháp giống như thần này để soạn thảo tất cả mã của bạn. Nhưng trong thực tế, nếu bạn sử dụng nó một cách phổ biến (trái ngược với một cách tiết kiệm), nó làm cho mã của bạn tệ hơn nhiều bằng cách khuyến khích bạn giới thiệu nhiều giao diện không cần thiết được sử dụng hoàn toàn ngược. (Ngược lại theo nghĩa là người gọi phải thực sự chịu trách nhiệm đánh giá các phụ thuộc đó và chuyển kết quả vào chứ không phải chính lớp gọi cuộc gọi.)

Các giao diện nên được sử dụng một cách tiết kiệm, và đảo ngược kiểm soát và tiêm phụ thuộc cũng nên được sử dụng một cách tiết kiệm. Nếu bạn có số lượng lớn trong số chúng, mã của bạn trở nên khó giải mã hơn nhiều.

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.