我和另外两位同事正试图了解如何最好地设计一个程序。例如,我有一个接口ISoda
和多个实现该接口的类,如Coke
,Pepsi
,DrPepper
等....
我的同事说,最好将这些项目放入数据库,如键/值对。例如:
Key | Name
--------------------------------------
Coke | my.namespace.Coke, MyAssembly
Pepsi | my.namespace.Pepsi, MyAssembly
DrPepper | my.namespace.DrPepper, MyAssembly
...然后有XML配置文件,将输入映射到正确的密钥,查询数据库中的密钥,然后创建对象。
我没有任何具体原因,但我觉得这是一个糟糕的设计,但我不知道该说些什么或如何正确反对它。
我的第二位同事建议我们对这些课程进行微观管理。所以基本上输入将通过switch语句,类似于此:
ISoda soda;
switch (input)
{
case "Coke":
soda = new Coke();
break;
case "Pepsi":
soda = new Pepsi();
break;
case "DrPepper":
soda = new DrPepper();
break;
}
这对我来说似乎有点好,但我仍然认为有更好的方法。过去几天我一直在阅读IoC容器,这似乎是一个很好的解决方案。但是,我对依赖注入和IoC容器仍然很新,所以我不知道如何正确地争论它。或许我错了,有更好的方法吗?如果是这样,有人可以提出更好的方法吗?
我可以提出什么样的论据来说服我的同事尝试另一种方法?有哪些优点/缺点?我们为什么要这样做呢?
不幸的是,我的同事非常不愿意改变,所以我想弄清楚如何说服他们。
修改
似乎我的问题有点难以理解。我们要弄清楚的是如何最好地设计一个利用多个接口的应用程序,并包含许多实现这些接口的具体类型。
是的,我知道将这些内容存储在数据库中是一个坏主意,但我们根本不知道更好的方法。这就是为什么我要求如何我们做得更好。
public void DrinkSoda(string input)
{
ISoda soda = GetSoda(input);
soda.Drink();
}
我们如何正确实施GetSoda
?我们应该重新考虑整个设计吗?如果传递这样的魔法字符是个坏主意,那么我们应该怎么做呢?
用户输入,例如“Diet Coke”,“Coke”,“Coke Lime”或任何会实例化Coke
类型的内容,因此多个关键字会映射到单一类型。
很多人都说上面发布的代码很糟糕。我知道这很糟糕。我正在寻找理由向同事们展示,并说明为什么这很糟糕以及为什么我们需要改变。
如果这个解释仍然难以理解,我道歉。我很难描述这种情况,因为我真的不明白如何用文字表达。
答案 0 :(得分:7)
作为一般规则,传递包装概念的对象比传递字符串更加面向对象。也就是说,当您需要将稀疏数据(如字符串)转换为正确的域对象时,有很多情况(特别是在UI方面)。
例如,您需要将字符串形式的用户输入转换为ISoda实例。
标准的,松散耦合的方式是使用Abstract Factory。像这样定义:
public interface ISodaFactory
{
ISoda Create(string input);
}
您的消费者现在可以依赖ISodaFactory并使用它,如下所示:
public class SodaConsumer
{
private readonly ISodaFactory sodaFactory;
public SodaConsumer(ISodaFactory sodaFactory)
{
if (sodaFactory == null)
{
throw new ArgumentNullException(sodaFactory);
}
this.sodaFactory = sodaFactory;
}
public void DrinkSoda(string input)
{
ISoda soda = GetSoda(input);
soda.Drink();
}
private ISoda GetSoda(input)
{
return this.sodaFactory.Create(input);
}
}
注意Guard子句和readonly
关键字的组合如何保证SodaConsumer类的不变量,它允许您使用依赖项而不必担心它可能为null。
答案 1 :(得分:4)
我遇到的依赖注入的最佳介绍实际上是quick series of articles that form a walkthrough of the concepts wiki网站上的Ninject。
你还没有真正给出你想要实现的目标的背景,但在我看来,你正试图设计一个“插件”架构。如果是这种情况 - 你的感觉是正确的 - 这些设计都是一句话,可怕。第一个是相对较慢的(数据库和反射?),对数据库层有大量不必要的依赖关系,并且不会随程序扩展,因为每个新类都必须输入到数据库中。使用反射有什么问题?
第二种方法是关于不可扩展的。每次引入新类时,都必须更新旧代码,并且必须“了解”您将要插入的每个对象 - 使第三方无法开发插件。您添加的耦合,即使是像Locator模式这样的耦合,也意味着您的代码不够灵活。
这可能是使用依赖注入的好机会。阅读我链接的文章。关键思想是依赖注入框架将使用配置文件来动态加载您指定的类。这使得交换程序组件变得非常简单和直接。
答案 2 :(得分:4)
如果我需要根据名称或其他序列化数据实例化正确的类,这是我通常使用的模式:
public interface ISodaCreator
{
string Name { get; }
ISoda CreateSoda();
}
public class SodaFactory
{
private readonly IEnumerable<ISodaCreator> sodaCreators;
public SodaFactory(IEnumerable<ISodaCreator> sodaCreators)
{
this.sodaCreators = sodaCreators;
}
public ISoda CreateSodaFromName(string name)
{
var creator = this.sodaCreators.FirstOrDefault(x => x.Name == name);
// TODO: throw "unknown soda" exception if creator null here
return creator.CreateSoda();
}
}
请注意,您仍然需要进行大量ISodaCreator
实现,并以某种方式收集要传递给SodaFactory
构造函数的所有实现。为了减少相关工作,您可以根据反射创建一个通用的苏打创建者:
public ReflectionSodaCreator : ISodaCreator
{
private readonly ConstructorInfo constructor;
public string Name { get; private set; }
public ReflectionSodaCreator(string name, ConstructorInfo constructor)
{
this.Name = name;
this.constructor = constructor;
}
public ISoda CreateSoda()
{
return (ISoda)this.constructor.Invoke(new object[0])
}
}
然后只扫描执行程序集(或其他一些程序集)来查找所有苏打类型并构建SodaFactory
,如下所示:
IEnumerable<ISodaCreator> sodaCreators =
from type in Assembly.GetExecutingAssembly().GetTypes()
where type.GetInterface(typeof(ISoda).FullName) != null
from constructor in type.GetConstructors()
where constructor.GetParameters().Length == 0
select new ReflectionSodaCreator(type.Name, constructor);
sodaCreators = new List<ISodaCreator>(sodaCreators); // evaluate only once
var sodaFactory = new SodaFactory(sodaCreators);
请注意,我的答案补充了Mark Seemann的答案:他告诉您让客户使用抽象的ISodaFactory
,这样他们就不必关心 苏打工厂的运作方式。我的答案是这样一个苏打工厂的示例实施。
答案 3 :(得分:3)
我会说实话,我知道其他人不同意,但我发现将类实例化为XML或数据库是一个可怕的想法。即使它牺牲了一些灵活性,我也很擅长使代码变得清晰。我看到的缺点是:
我实际上有点厌倦了听到这种特殊的节目时尚。它感觉和看起来很丑,它解决了一个问题,很少有项目需要解决。说真的,你多久会从该代码中添加和删除苏打水。除非它几乎每次你编译,否则这么多的额外complexexity就没那么有优势了。
为了澄清,我认为控制反转可能是一个很好的设计理念。我只是不喜欢外部嵌入代码只是为了实例化,用XML或其他方式。
交换机案例解决方案很好,因为这是实现工厂模式的一种方法。大多数程序员都不会很难理解它。它也比在代码中实例化子类更清晰。
答案 4 :(得分:3)
每个DI库都有自己的语法。 Ninject如下所示:
public class DrinkModule : NinjectModule
{
public override void Load()
{
Bind<IDrinkable>()
.To<Coke>()
.Only(When.ContextVariable("Name").EqualTo("Coke"));
Bind<IDrinkable>()
.To<Pepsi>()
.Only(When.ContextVariable("Name").EqualTo("Pepsi"));
// And so on
}
}
当然,维护映射变得非常繁琐,所以如果我有这种系统,那么我通常最终将基于反射的注册放在一起,类似于this one:
public class DrinkReflectionModule : NinjectModule
{
private IEnumerable<Assembly> assemblies;
public DrinkReflectionModule() : this(null) { }
public DrinkReflectionModule(IEnumerable<Assembly> assemblies)
{
this.assemblies = assemblies ??
AppDomain.CurrentDomain.GetAssemblies();
}
public override void Load()
{
foreach (Assembly assembly in assemblies)
RegisterDrinkables(assembly);
}
private void RegisterDrinkables(Assembly assembly)
{
foreach (Type t in assembly.GetExportedTypes())
{
if (!t.IsPublic || t.IsAbstract || t.IsInterface || t.IsValueType)
continue;
if (typeof(IDrinkable).IsAssignableFrom(t))
RegisterDrinkable(t);
}
}
private void RegisterDrinkable(Type t)
{
Bind<IDrinkable>().To(t)
.Only(When.ContextVariable("Name").EqualTo(t.Name));
}
}
稍微多一点代码,但你永远不必更新它,它会变成自我注册。
哦,你这样使用它:
var pop = kernel.Get<IDrinkable>(With.Parameters.ContextVariable("Name", "Coke"));
显然这是特定于单个DI库(Ninject),但它们都支持变量或参数的概念,并且所有这些都有相同的模式(Autofac,Windsor,Unity等)
所以对你的问题的简短回答基本上是是的,DI可以做到这一点,这是一个非常好的地方有这种逻辑,虽然你的原始代码示例都不是问题实际上与DI有关(第二个是工厂方法,第一个是......好吧,企业)。
答案 5 :(得分:3)
我认为问题甚至不是工厂或其他什么;这是你的班级结构。
DrPepper
的行为与Pepsi
的行为有何不同? Coke
?对我而言,听起来你正在使用不正确的接口。
根据具体情况,我会创建一个Soda
类,其属性为BrandName
或类似。 BrandName
将通过DrPepper
或Coke
设置为enum
或string
(前者似乎是您案例的更好解决方案)。
解决工厂问题很容易。事实上,你根本不需要工厂。只需使用Soda
类的构造函数。