【问题标题】:How should common references be passed around in an assembly?应该如何在程序集中传递公共引用?
【发布时间】:2013-04-15 21:56:57
【问题描述】:

我正在尝试摆脱代码库中的静态类、静态辅助方法和单例类。目前,它们几乎遍布整个代码,尤其是实用程序类和日志库。这主要是由于需要模拟能力以及面向对象的设计和开发问题,例如可扩展性。将来我可能还需要引入某种形式的依赖注入,并愿意为此敞开大门。

基本上,我遇到的问题是关于传递常用引用的方法。这些是代码库中几乎每个类都使用的对象,例如日志接口、实用程序(帮助程序)类接口,并且可能是一个类的实例,该类的实例包含大多数类相关的程序集的内部公共状态。

据我所知,有两种选择。一种是定义一个存储公共引用的类(或接口),如果您愿意,还可以定义一个上下文,并将上下文传递给创建的每个对象。另一种选择是将几乎每个类的每个公共引用作为单独的参数传递,这将增加类构造函数的参数数量。

这些方法中哪一种更好,每种方法的优缺点是什么,是否有更好的方法来完成这项任务?

【问题讨论】:

标签: c# oop design-patterns singleton static-classes


【解决方案1】:

你会得到不同的意见,但是通常为每个依赖项传递一个单独的参数给构造函数是首选的,原因如下:

  • 它清楚地定义了类的实际依赖关系 - 使用“上下文”,如果不深入研究代码,您不知道使用了上下文的哪些部分。
  • 通常为构造函数提供大量参数是一种设计味道,因此使用构造函数注入可以帮助您发现设计缺陷。
  • 在测试时,您可以模拟单个依赖项,而不必模拟整个上下文

【讨论】:

  • 我同意您的观点,但是另一方面,将对日志记录类或实用程序类的引用传递给我创建的几乎每个小对象确实会弄乱我认为的代码。我不确定是否有一个干净的解决方案。有些人可能会声称最好将此类类保留为静态类或单例类,但它有其自身的一系列问题。
  • 解决这些问题的几种方法是 1) 拥有一个保存记录器的基类,2) 通过使用 Setter 注入使记录器成为“可选”依赖项,以及 3) 在通过事件进行更高级别和“冒泡”日志记录。静态对象/单例的一大缺点是它使单元测试变得困难,因为您不能存根/模拟单例。
  • 我绝对同意静态对象​​和单例的大缺点。至于 1),基类太宝贵了,尤其是在没有多重继承的语言中,因此我认为我不想使用继承来传递对记录器的引用。但这对于自然类层次结构绝对有用。我之前考虑过 2),我认为使用该代码的人会忘记在创建对象后设置记录器引用,然后在需要时记录会默默地失败。 3) 很有趣,而且肯定有它自己有用的地方。
  • 回答你的问题的核心 - 如果每个小类需要一个记录器,而你不想要一个静态类,那么我看不到解决方法。有一些 DI 框架(如 Ninject)可以消除一些“笨拙”的代码,但我仍然认为您会希望以某种方式注入这些依赖项以提高可测试性。
【解决方案2】:

我通常使用上下文对象方法,并将上下文对象传递给对象的构造函数或方法——取决于哪个最有意义。

上下文对象模式可以有几种形式。

您可以定义一个具有您需要的成员的接口,或者您可以生成一种容器类。例如,在编写松散耦合的组件时,我倾向于让我实现的每个组件都有一个匹配的接口,以便在需要时可以重新实现它。然后我在“管理器”对象上注册对象,如下所示:

public interface IServiceManager
{
    public T GetService<T>();
    public T RequireService<T>();
    public void RegisterService<T>(T service);
    public void UnregisterService<T>(T service);
}

在幕后有一个从类型到对象的映射,这使我能够非常快速地将大量不同的组件组合成一个工作整体。每个组件都通过接口请求其他组件,而管理器对象将它们粘合在一起。 (如果您正确编写组件,您甚至可以在进程运行时将一项服务换成另一项服务!)

人们会按照以下方式注册服务:

class FooService : IFooService { }

// During process start-up:
serviceManager.RegisterService<IFooService>(new FooService());

由于字典查找,这种方法比平面接口方法的开销更大,但它使我能够构建非常复杂的系统,这些系统可以很容易地通过不同的服务实现重新部署。 (而且,像往常一样,我遇到的任何瓶颈都不是从字典中查找服务对象,而是在其他地方,例如数据库。)

【讨论】:

  • 非常感谢您的回答,这正是我期待的反馈类型。据我所知,TFS 2012 API 中使用了这种工厂或服务提供商方法。在这种方法中为同一接口注册多个具体实现是否有意义?如果是这样,这意味着什么?经理将如何解决依赖关系?
  • @exc 可能取决于上下文。我写了such an implementation,我在几个项目中成功使用了它。如果您只要求一个实现,它会通过抛出异常来解决歧义,但如果您要求多个实现,它将返回所有实现。当然,其他算法也是可能的——选择最能解决您的问题的算法。
  • 这不是真正的 context 模式 - 它是 Service Locater 模式
  • @DStanley 我认为服务定位器模式是上下文模式的一种特殊化。 AFAICT 的主要区别在于,上下文对象对您需要共享的每一位数据都具有一流的属性,而服务定位器对象允许任意注册新对象。我不认为这种差异非常显着,至少在这种特定语言中没有。
  • @cdhowie 我看一下代码,谢谢。我认为这减少了传递服务管理器引用的问题。您是否将服务管理器引用传递给一个类需要另一个类的服务的类构造函数?
【解决方案3】:

我建议将其作为参数传递给构造函数。这对于依赖注入和单元可测试性(模拟)都有很大的优势。

【讨论】:

  • 我认为参数是通过 一个 对构造函数的引用传递 several 属性或 several 对构造函数的引用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-20
  • 2013-01-05
  • 1970-01-01
  • 1970-01-01
  • 2013-12-30
  • 1970-01-01
相关资源
最近更新 更多