簡體   English   中英

依賴注入:如何將從方法參數接收的值集成到我想解析為構造函數參數的依賴項中?

[英]Dependency Injection: how do I integrate values received from method parameters into dependencies that I want to resolve as constructor parameters?

請耐心等待——這是一個復雜的問題,我已經盡可能地簡化了它。 (我正在使用 ASP.NET web API 和 AutoFac,為簡潔起見省略了一堆配置。)

我的目標是最大化依賴注入由 DI 框架處理的程度,在某些對象的所有依賴關系直到運行時才知道的情況下。

我們的球員是:

  • 接受 web 請求的 CONTROLLER class,應用程序的入口點 - 輸入包括repoName

  • 一個 REPOSITORY RESOLVER class,一個將repoName解析為特定 REPOSITORY 的工廠。 這是它的實現:

    public class RepositoryResolver : IRepositoryResolver
    {
        public IRepository Resolve(string repoName)
        {
            return new Repository(new Input { RepoName = repoName });
        }
    }
  • 存儲庫 class(代表數據庫或其他)。 這是它的實現:
    public class Repository : IRepository
    {
        private readonly Input input; // proxy for connection string or other identifying information
    
        public Repository (Input input)
        {
            this.input = input;
        }

        public string[] Get()
        {
            return new[] { "I", "am", "a", input.RepoName };
        }
    }
  • 計算結果的業務邏輯 class。 業務邏輯 class 依賴於單個存儲庫; 它對多個存儲庫或存儲庫解析器一無所知,因為它不關心它們。 這是它的實現:
    public class BusinessLogic : IBusinessLogic
    {
        private readonly IRepository repository;

        public BusinessLogic(IRepository repository)
        {
            this.repository = repository;
        }

        public string[] Compute()
        {
            return repository.Get();
        }
    }

在程序上,我想要完成的事情(在這個精簡的玩具示例中)非常簡單。 這是 controller 的示例實現:

建議的答案 #1 - 純 DI(無容器)

public class PureDIController : ApiController
{
    public ProceduralValuesController() { }

    public IEnumerable<string> Get(string repoName)
    {
        IRepositoryResolver repoSource = new RepositoryResolver();
        IRepository repo = repoSource.Resolve(repoName);
        IBusinessLogic businessLogic = new BusinessLogic(repo);
        return businessLogic.Compute();
    }
}

...這可行,但顯然我在這里沒有使用 DI 容器。 當像這樣使用純 DI 時,對一個玩家的改變往往會產生超出其直接合作者的連鎖反應,並可能通過許多層(此處未表示); 我覺得將這種組合邏輯移動到 DI 容器中會減少很多這種類型的重構。 這就是這個問題的價值主張。

但是,當我嘗試使用依賴注入重寫這個 class 時,我遇到了一個問題:BUSINESS LOGIC 依賴於 REPOSITORY,因此無法通過預先創建的 DI 容器來解決。 因此,我無法解決這里的揮手評論:

public class DIValuesController : ApiController
{
    private readonly IRepositoryResolver repoSource;
    private readonly IBusinessLogic businessLogic;

    public DIValuesController(
        IRepositoryResolver repoSource,
        IBusinessLogic businessLogic)
    {
        this.repoSource = repoSource;
        this.businessLogic = businessLogic;
    }

    public IEnumerable<string> Get(string repoName)
    {
        var repo = repoSource.Resolve(repoName);
        /* ...handwaving to integrate repo into businessLogic... */
        return businessLogic.Compute();
    }
}

...因為在實例化IBusinessLogic時無法解析 IBusinessLogic。

我已經開發了幾種可能的解決方案,我會將它們添加為潛在的答案。 但是,我不喜歡其中任何一個,因此發表了這篇文章。 ¯_(ツ)_/¯請給我一個我還沒有想到的驚喜!

答案 #2 - 根據需要傳遞參數

該解決方案放棄了我們可以將方法參數有效地轉換為構造函數參數的原始前提。 相反,它假定需要將 repoName(或某些等效的區分符)傳遞到需要該信息的任何 function 中。 這是 controller 的一個可能的示例實現(請注意,BusinessLogic 現在需要一個附加參數):

public class ParameterPassingController : ApiController
{
    private readonly IBusinessLogic businessLogic;

    public ParameterPassingController(
        IBusinessLogic businessLogic)
    {
        this.businessLogic = businessLogic;
    }

    public IEnumerable<string> Get(string repoName)
    {
        return businessLogic.Compute(repoName);
    }
}

這是BusinessLogic的新實現:

public class BusinessLogic : IBusinessLogic
{
    private readonly IRepositoryResolver repositoryResolver;

    public BusinessLogic(IRepository repositoryResolver)
    {
        this.repositoryResolver = repositoryResolver;
    }

    public string[] Compute(string repoName)
    {
        var repo = repositoryResolver.Resolve(repoName);
        return repo.Get();
    }
}

這個解決方案感覺很尷尬,因為它修改了 BusinessLogic class 以依賴於比以前更不直接的 object。 一個類的依賴應該由class來決定,而不是由調用者的需要來決定。 BusinessLogic class 比以前更好了,任何導致我們讓它變得更復雜的解決方案都可能不是一個好的解決方案。

還有另一種可能性 - 將 IBusinessLogic 傳遞給 Controller 不是作為實例,而是作為工廠(即 Func<string, IBusinessLogic>),並在 Methode Compute Fall 工廠中使用 repoName。 參見,例如: https://autofaccn.readthedocs.io/en/latest/advanced/delegate-factories.html

答案 #3 - 引入額外的解析器

該解決方案添加了一個業務邏輯解析器,使用與存儲庫解析器相同的模式。 (工廠模式?)

public class BusinessLogicResolverController : ApiController
{
    private readonly IRepositoryResolver repoSource;
    private readonly IBusinessLogicResolver businessLogicSource;

    public BusinessLogicResolverController(
        IRepositoryResolver repoSource,
        IBusinessLogicResolver businessLogicSource)
    {
        this.repoSource = repoSource;
        this.businessLogicSource = businessLogicSource;
    }

    public IEnumerable<string> Get(string repoName)
    {
        var repo = repoSource.Resolve(repoName);
        var businessLogic = businessLogicSource.Resolve(repo);
        return businessLogic.Compute();
    }
}

我不喜歡的一點是,如果有很多類依賴於單個 IRepository(在我的重要示例中,有很多),我需要為它們中的每一個創建一個 Resolver。 這也使 DI 容器可以幫助處理的其他事情變得復雜,例如裝飾器應用程序和其他東西。

答案 #4 - 引入時間耦合

此解決方案將 IRepository 的已注冊實現替換為還實現了 IRepositoryManager 的 class,從而允許它在運行時指向適當的存儲庫。 這是 controller 現在的樣子:

public class TemporallyCoupledController : ApiController
{
    private readonly IRepositoryManager repoManager;
    private readonly IBusinessLogic businessLogic;

    public TemporallyCoupledController(
        IRepositoryManager repoManager,
        IBusinessLogic businessLogic)
    {
        this.repoManager = repoManager;
        this.businessLogic = businessLogic;
    }

    public IEnumerable<string> Get(string repoName)
    {
        repoManager.Set(repoName);
        return businessLogic.Compute();
    }
}

...這是 IRepositoryManager 的實現:

public class RepositoryManager : IRepositoryManager, IRepository
{
    private readonly IRepositoryResolver resolver;
    private IRepository repo = null;

    public RepositoryManager(IRepositoryResolver resolver)
    {
        this.resolver = resolver;
    }

    void IRepositoryManager.Set(string repoName)
    {
        this.repo = resolver.Resolve(repoName);
    }

    string[] IRepository.Get()
    {
        if (repo == null)
            throw new InvalidOperationException($"{nameof(IRepositoryManager.Set)} must be called first.");
        else
            return repo.Get();
    }
}

該解決方案當然允許 controller 保持較小,但根據我的經驗,時間耦合幾乎總是弊大於利。 此外,不清楚 controller 中的這兩行是否相互有任何關系:

        repoManager.Set(repoName);
        return businessLogic.Compute();
        

......但顯然他們這樣做了。 所以這是一個非常糟糕的解決方案。

答案 #5 - 在 controller 中注入基於參數的依賴

此解決方案使 controller 成為組合根的一部分,而不是通過依賴注入來解析它。 (檢索構建容器的行可以通過其他方式完成,但這不是重要的部分 - 主要思想是我們需要在獲得輸入參數后直接訪問其BeginLifetimeScope方法。)

public class SelfAwareDIController : ApiController
{
    public SelfAwareDIController() { }

    public IEnumerable<string> Get(string repoName)
    {
        var container = (AutofacWebApiDependencyResolver)GlobalConfiguration.Configuration.DependencyResolver;

        using (var scope = container.Container.BeginLifetimeScope(builder =>
            builder.RegisterInstance(new Input { RepoName = repoName }).AsSelf()))
        {
            var businessLogic = scope.Resolve<IBusinessLogic>();
            return businessLogic.Compute();
        }
    }
}

該解決方案避免了時間耦合(因為businessLogic不能存在於可以解析的生命周期 scope 之外)。 它還允許我們刪除 REPOSITORY RESOLVER; 我已經對 REPOSITORY RESOLVER 感到不舒服,因為它是 Composition Root 的擴展,會干擾 DI 容器對 object 創建的集中處理。 缺點是它將一些與容器相關的代碼移動到 controller 中,而不是將其全部保留在服務配置中。 (同樣,在一個重要的示例中,可能有許多控制器需要實現類似的邏輯。)它還可以防止 controller 本身被 DI 容器實例化(您可以使用AutoFac.WebApi2包進行實例化)。 盡管如此,因為它將這個新上下文的知識限制為 controller(並且消除了擁有工廠類的必要性),這可能是我所確定的解決方案中最不令人反感的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM