带非依赖参数的构造函数注入
本文关键字:构造函数 注入 参数 依赖 | 更新日期: 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
实例时,它可以使用这些参数来创建具体实现的实例。显然,ITradingApiProvider
和ITradingApi
的实现是紧密耦合的,但我认为只要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
类使用表达式树直接处理这些属性。如果有人对这种先进的工厂设计感兴趣,请留下评论。
我认为您的提供商方法没有问题。这里有两个问题:
- 一个可操作的:你的
ITradingAPI
,它为你可以执行的操作定义了一个合同。 - 元数据:描述实际实现属性的东西(元数据可能不太合适,但想不出更好的名字)
现在显然你需要一些东西,可以使两者之间的连接,那就是你的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()
。