2012-04-25 72 views
24

当我使用依赖注入时,我的接口和具体类之间存在一对一的关系。当我需要向接口添加一个方法时,我最终打破了实现该接口的所有类。使用接口或类进行依赖注入

这是一个简单的例子,但我们假设我需要将​​注入到我的一个类中。

public interface ILogger 
{ 
    void Info(string message); 
} 

public class Logger : ILogger 
{ 
    public void Info(string message) { } 
} 

这样的1对1关系就像是一种代码味道。由于我只有一个实现,如果我创建一个类并将Info方法标记为虚拟以在我的测试中覆盖而不是仅为单个类创建接口,是否存在潜在问题?

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     // Log to file 
    } 
} 

如果我需要另一种实现中,我可以重写Info方法:

public class SqlLogger : Logger 
{ 
    public override void Info(string message) 
    { 
     // Log to SQL 
    } 
} 

如果每个这些类的有,将创建一个漏抽象特定的属性或方法,我可以提取出的基类:

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     throw new NotImplementedException(); 
    } 
} 

public class SqlLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

public class FileLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

我没有把基类标记为抽象的原因是因为如果我想添加另一个方法,我不会打破exis实现。例如,如果我的FileLogger需要Debug方法,我可以在不破坏现有SqlLogger的情况下更新基类Logger

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     throw new NotImplementedException(); 
    } 

    public virtual void Debug(string message) 
    { 
     throw new NotImplementedException(); 
    } 
} 

public class SqlLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

public class FileLogger : Logger 
{ 
    public override void Info(string message) { } 
    public override void Debug(string message) { } 
} 

再次,这是一个简单的例子,但是当我应该更喜欢一个接口?

+1

_我之所以没有将基类标记为抽象是因为如果我想添加另一个method_Hm,抽象类可以包含实现。您可以将Debug方法添加到您的抽象Logger类。 – 2012-04-25 08:09:01

+0

如果你正在编写一个可重用的库,打破现有的实现只是一个问题。你是?或者你只是写一行业务应用程序? – Steven 2012-04-25 11:03:53

+0

这不是问题的范围,但继承被高估。 'SqlLogger'只是一个具有'SqlLogPersistenceStrategy'的具体'Logger'。在大多数情况下,构成比继承好得多。同样对于你的问题,ISP怎么样? 'ILogInfo','ILogError'等 – plalx 2015-11-05 16:28:30

回答

27

“快速” 接听

我要坚持接口。他们将设计为为外部实体消费合同。

@JakubKonecki提到了多重继承。我认为这是坚持使用接口的最大原因,因为如果你强迫他们参加基础课程,它将在消费者方面变得非常明显......没有人喜欢将基类强加给他们。

的更新“快速”接听

你说你的控制之外与接口实现的问题。一个好的方法是简单地创建一个从旧的接口继承的新接口并修复你自己的实现。然后,您可以通知其他团队有新的界面可用。随着时间的推移,您可以弃用旧界面。

不要忘了,您可以使用explicit interface implementations的支持来帮助保持逻辑上相同但不同版本的界面之间的良好鸿沟。

如果你想要所有这些都适合DI,那么尽量不要定义新的接口,而是更喜欢添加。或者限制客户端代码更改,尝试从旧的接口继承新的接口。

实现对消费

有实现接口和消费之间的差异。添加一个方法会破坏实现,但不会破坏用户。

移除一个方法显然打破了消费者,但并没有破坏实现 - 但是如果你对消费者有意识向后兼容,你不会这样做。

我的经验

我们经常有一个接口,1对1的关系。这在很大程度上是形式化的,但你偶尔会得到好的实例,其中接口是有用的,因为我们存根/模拟测试实现,或者我们实际上提供客户特定的实现。事实上,如果我们碰巧改变了界面,这经常会破坏一个实现,这不是一种代码异味,在我看来,它只是你如何针对界面而工作。

由于我们利用工厂模式和DI元素等技术来改进老旧的遗留代码库,我们基于接口的方法现在可以很好地支持我们。测试能够快速利用这样的事实,即在找到“明确”使用(即,不仅仅是具体类的1-1映射)之前,接口已存在于代码库中多年。

基地班缺点

基类是共享实施细则,共同的实体,他们能够做同样的事情与公开共享的API其实是一种副产品在我看来。接口旨在公开共享API,所以使用它们。

对于基类,您也可能会泄漏实现细节,例如,如果您需要公开另一部分实现以供使用。这些都不利于维护一个干净的公共API。

打破/支持实现

如果你走你可能会碰到困难的接口路径切换连界面,由于违约责任。另外,正如你所提到的,你可能会破坏你的控制之外的实现。有两种方法可以解决这个问题:

  1. 说明你不会破坏消费者,但是你不会支持实现。
  2. 声明一旦接口发布,它永远不会改变。

我亲眼目睹后,我看到它有两种形式出现:

  1. 任何新的东西完全独立的接口:MyInterfaceV1MyInterfaceV2
  2. 接口继承:MyInterfaceV2 : MyInterfaceV1

我个人不会选择要走这条路线,我会选择不支持破坏性更改的实现。但有时候我们没有这个选择。

一些代码

public interface IGetNames 
{ 
    List<string> GetNames(); 
} 

// One option is to redefine the entire interface and use 
// explicit interface implementations in your concrete classes. 
public interface IGetMoreNames 
{ 
    List<string> GetNames(); 
    List<string> GetMoreNames(); 
} 

// Another option is to inherit. 
public interface IGetMoreNames : IGetNames 
{ 
    List<string> GetMoreNames(); 
} 

// A final option is to only define new stuff. 
public interface IGetMoreNames 
{ 
    List<string> GetMoreNames(); 
} 
+0

我遇到的一个难题就是在整个企业中共享此接口。如果我更新了一个接口,它会中断所有使用此接口的其他团队的实现。在这一点上,我们迫使他们进行这些改变。在一个完美的世界里,每个人都会很高兴并愿意更新他们的实现,但情况并非总是如此。 – nivlam 2012-04-25 08:14:56

+0

如果你不能改变接口 - 你可以创建另一个:'inteface IDebuger {void Debug(string message);}'并在FileLogger中实现它。所以如果其他团队不需要它,他们将不会使用和实施它。 – 2012-04-25 08:21:50

+0

@nivlam另一种方法是一旦创建了一个接口,它就会被锁定以进行更改。创建新的继承旧的接口,然后实现可以选择实现新的接口......这意味着你的内部实现。我已经更新了我的答案。 – 2012-04-25 08:25:41

2

您应该始终偏好界面。

是的,在某些情况下,您将在类和接口上使用相同的方法,但在更复杂的情况下您不会。还要记住,.NET中没有多重继承。

你应该保持你的接口在一个单独的程序集中,并且你的类应该是内部的。

对接口进行编码的另一个好处是可以在单元测试中轻松地对它们进行模拟。

+0

为什么把接口保存在一个单独的程序集中是一件好事? – thedev 2012-04-25 08:03:33

+4

@thedev您可以在不发布实现的情况下发布接口。这些也可以共享,而不会泄露不必要的实施,这只会增加开销或维护问题。 – 2012-04-25 08:06:05

0

我喜欢的接口。给定存根和模拟也是实现(有点),我总是至少有两个任何接口的实现。另外,可以将接口插入并模拟测试。

此外,Adam Houldsworth提到的合同角度非常有建设性。恕我直言,它使得代码更清洁,而不是接口的实现使它变臭。

9

你​​接口更是打破了interface segregation principle当你开始增加DebugErrorCritical方法除了Info。看看horrible Log4Net ILog interface,你就会知道我在说什么。

而不是创建每个日志严重性的方法,创建需要一个日志对象的单个方法:

void Log(LogEntry entry); 

这彻底解决了所有的问题,这是因为:

  1. LogEntry将是一个简单的DTO,您可以添加新的属性,而不会破坏任何客户端。
  2. 您可以为您的​​界面创建一组扩展方法,该界面映射到该单一的Log方法。

这里有这样的扩展方法的一个例子:

public static class LoggerExtensions 
{ 
    public static void Debug(this ILogger logger, string message) 
    { 
     logger.Log(new LogEntry(message) 
     { 
      Severity = LoggingSeverity.Debug, 
     }); 
    } 

    public static void Info(this ILogger logger, string message) 
    { 
     logger.Log(new LogEntry(message) 
     { 
      Severity = LoggingSeverity.Information, 
     }); 
    } 
} 

有关此设计更详细的讨论,请阅读this

+0

现在的问题是,合同没有定义什么严重性级别或不支持。考虑到ISP,ILogInfo,ILogError,ILogDebug等等呢? – plalx 2015-11-05 16:20:40