带非依赖参数的构造函数注入

本文关键字:构造函数 注入 参数 依赖 | 更新日期: 2023-09-27 18:03:14

我有一个像这样的接口ITradingApi:

public interface ITradingApi
{
    IOrder CreateOrder(...);
    IEnumerable<Symbol> GetAllSymbols();
    // ...
}

这意味着是交易软件供应商的不同api的门面。我的视图模型在它的构造函数中依赖于这个交易API:

public class MainViewModel
{
    public MainViewModel(ITradingApi tradingApi) { /* ... */ }
    // ...
}

我使用Ninject作为IoC容器,所以我将创建一个视图模型的实例,如下所示:

var vm = kernel.Get<MainViewModel>();

现在,我的问题是:

ITradingApi的实现可能需要额外的参数才能工作。
例子:

  • 一个供应商API在内部使用TCP/IP,所以我需要一个主机名和端口。
  • 另一个供应商使用COM对象。在这里我不需要任何信息。
  • 第三方供应商需要帐户的用户名和密码。

本着不允许不完整对象的精神,我将这些作为参数添加到具体实现的构造函数中。

现在,我不确定,这将如何工作。显然,这些附加参数不属于接口,因为它们是特定于每个实现的。
另一方面,这些附加参数需要由最终用户输入,然后传递给ITradingApi的实现,这意味着ITradingApi的用户需要对具体实现有深入的了解。如何解决这一困境?

更新:
一种方法是创建一个公开所需参数列表的ITradingApiProvider。View可以自动为这些参数创建一个输入表单,该表单绑定到ITradingApiProvider中的参数。现在,当从提供程序请求ITradingApi实例时,它可以使用这些参数来创建具体实现的实例。显然,ITradingApiProviderITradingApi的实现是紧密耦合的,但我认为只要ITradingApi的每个实现都有相应的ITradingApiProvider的实现,这就不是问题了

带非依赖参数的构造函数注入

根据目前为止提供的信息,我想指出一两件事:

首先,具体的配置值是在组合时提供的,还是在运行时作为用户输入时真正首先可用,这是有很大区别的。只要它们能在组合时解决,事情就很容易,因为您可以简单地从环境中读取值并将其提供给适当的构造函数。因此,对于这个答案的其余部分,我将假设事情要困难得多,你实际上需要在运行时从用户那里获得那些值。

我宁愿对实际发生的事情进行建模,而不是试图提出一个通用的配置API。在这种情况下,听起来像是我们从用户那里收集配置值,那么为什么不明确地建模呢?产品交易员

定义一个接口:

public interface ITradingApiTrader
{
    ITradingApi Create(Type apiType);
}

这里假定apiType可以强制转换为ITradingApi,但这不能由编译器强制执行。(我之所以称其为"交易员",是因为这是产品交易员模式(PLoPD 3)的一种变体。)

这和以前有什么不同?

那么,您可以实现 Create方法,方法是为每种类型的ITradingApi显示用户界面。每个具体的用户界面收集其自己的具体ITradingApi实现所需的值,并随后返回一个正确配置的实例。

如果您知道编译时的具体类型,则其他变体包括:

public interface ITradingApiTrader
{
    ITradingApi CreateMT4TradingApi();
    ITradingApi CreateFooTradingApi();
    ITradingApi CreateBarTradingApi();
    // etc.
}

也许你也可以这样做(尽管我没有尝试编译这个):

public interface ITradingApiTrader
{
    ITradingApi Create<T>() where T : ITradingApi;
}

还请注意,您不需要基于Type定义第一个ITradingApiTrader的Create方法—任何标识符(如enum或字符串)都可以代替。

访客

如果ITradingApi集在设计时是(有限的并且)已知的,那么Visitor设计模式也可能提供另一种选择。

如果使用Visitor,则可以使Visit方法显示适当的用户界面,然后使用从用户界面收集的值来创建适当的ITradingApi实例。

基本上,这只是之前的"解决方案"的一个变体,其中产品贸易商被实现为访问者。

这是你想要的吗?

   ninjectKernel.Get<MainViewModel>().WithConstructorArgument("tradingApi", 
       kernel.Get<ITaxCalculator>() .WithConstructorArgument("additionalParameter","someValue")));

好吧,我的两分钱,我不确定你知道什么。这只是为了帮助和尝试…

我们给你的api一个访问者作为接口的构造:

public interface ITradingApi
{
    Object CreateOrder();
    IEnumerable<Object> GetAllSymbols();
}
public class TradingApi : ITradingApi
{
    IvisitorAPI _VisitorAPI;
    public TradingApi(IvisitorAPI visitorAPI)
    {
        _VisitorAPI = visitorAPI;
    }

    public Object CreateOrder()
    {
        var Order = new Object();
        //bla bla bla
        //here code relative to different visitor
        _VisitorAPI.SaveOrder(Order);
        return Order;
    }
}

是你的访问者知道如何处理一些动作,因为根据访问者的不同,他会以不同的方式使用你的api来实现相同的动作(这里是SaveOrder)。

public interface IvisitorAPI
{
    bool SaveOrder(Object order);
}

public class visitorApiIP : IvisitorAPI
{
    public string HostName { get; set; }
    public int Port { get; set; }
    public visitorApiIP(string hostname, int port)
    {
        HostName = hostname;
        Port = port;
    }

    public bool SaveOrder(Object order)
    {
        //save the order using hostname and ip
        //...
        //....
        return true;
    }
}

只有访问者知道他需要什么来实现他的操作版本。因此,不是APi需要额外的参数,我们将逻辑推到了访问者类中。只有当我们知道谁是访问者时,才能创建这个访问者类,因此,肯定是在运行时

希望能给你一些启发。我不知道整个理论是否适用于你的具体情况。

我最好的;)

解决方案是使用我问题中更新部分概述的方法。ITradingApiProvider扮演了抽象工厂的角色,因此应该重命名为ITradingApiFactory。它将公开所需参数的列表,这些参数的值可以设置。View又可以使用该列表自动向用户显示一个输入表单,以便为每个参数输入值,因为只有用户知道参数的值。
Create的调用将使用以下参数:

public interface ITradingApiFactory
{
    ITradingApi Create();
    IEnumerable<Parameter> Parameters { get; }
}
public class Parameter
{
    public Parameter(Type type, string name, string description)
    { Type = type; Name = name; Description = description; }
    public Type Type { get; private set; }
    public string Name { get; private set; }
    public string Description { get; private set; }
    public object Value { get; set; }
}
public class MT4TradingApiFactory : ITradingApiFactory
{
    Dictionary<string, Parameter> _parameters;
    public MT4TradingApiFactory()
    { /* init _parameters */ }
    public ITradingApi Create()
    {
        return new MT4TradingApi(_parameters["hostname"].ToString(),
                                 (int)_parameters["port"]);
    }
    IEnumerable<Parameter> Parameters { get { return _parameters.Values; } }
}

更多信息可以在这个答案中找到。

这可以进一步推进,使其更容易使用,通过给每个工厂实现的参数作为属性,并改变Parameter类使用表达式树直接处理这些属性。如果有人对这种先进的工厂设计感兴趣,请留下评论。

我认为您的提供商方法没有问题。这里有两个问题:

  1. 一个可操作的:你的ITradingAPI,它为你可以执行的操作定义了一个合同。
  2. 元数据:描述实际实现属性的东西(元数据可能不太合适,但想不出更好的名字)

现在显然你需要一些东西,可以使两者之间的连接,那就是你的ITradingAPIProvider。这似乎是合理的,而且很有可能在一两年后你再回过头来看你的代码时,你仍然能够理解它;)

试试类似于策略模式的东西怎么样?创建一个名为IConnectStrategy:

的新接口
interface IConnectStrategy
{
    void Connect();
}

将connectstrategy作为参数添加到ITradingApi中的void CreateOrder(IConnectStrategy connectStrategy)方法中,并让每个供应商创建/指定他们自己的连接方法。例如,为一个供应商创建:

public class TCPConnectStrategy : IConnectStrategy
{
    public TCPConnectStrategy(string hostName, int port)
    {
        /* ... */
    }
    public void Connect()
    {
        /* ... tcp connect ... */
    }
}

(连接可能不是最好的名字,甚至不是你实际在做什么,但请将它应用到任何适合你的项目。)

在注释后编辑:创建一个策略,该策略只对每个具有供应商特定参数的方法具有契约。然后向itradingapi接口添加方法void SetVendorStrategy(IVendorStrategy vendorStrategy)(或属性)。该策略的每个实现都有自己的构造函数和自己的参数,并且itradingapi接口的每个实现中的每个方法(需要特定于供应商的参数)都只调用vendorStrategy.DoSomethingWithVendorSpecificData()