【问题标题】:Design Question - how do you break the dependency between classes using an interface?设计问题 - 如何使用接口打破类之间的依赖关系?
【发布时间】:2010-06-02 01:27:49
【问题描述】:

我提前道歉,但这将是一个很长的问题。

我被困住了。我正在尝试同时学习单元测试、C# 和设计模式。 (也许这是我的问题。)因此,我正在阅读单元测试艺术(Osherove)、清洁代码(Martin)和 Head First Design Patterns(O'Reilly)。

我现在才刚刚开始了解代表和事件(如果您要拖钓我最近的 SO 问题,您会看到这些)。我仍然不太了解 lambdas。

为了了解所有这些,我给自己制定了一个学习项目,我称之为 goAlarms。我有一个警报类,其中包含您期望的成员(NextAlarmTime、Name、AlarmGroup、Event Trigger 等)

我希望闹钟的“定时器”是可扩展的,所以我创建了一个 IAlarmScheduler 接口,如下所示......

public interface AlarmScheduler
{
    Dictionary<string,Alarm> AlarmList { get; }
    void Startup();
    void Shutdown();
    void AddTrigger(string triggerName, string groupName, Alarm alarm);
    void RemoveTrigger(string triggerName);
    void PauseTrigger(string triggerName);
    void ResumeTrigger(string triggerName);
    void PauseTriggerGroup(string groupName);
    void ResumeTriggerGroup(string groupName);
    void SetSnoozeTrigger(string triggerName, int duration);
    void SetNextOccurrence (string triggerName, DateTime nextOccurrence);
}

这个 IAlarmScheduler 接口定义了一个组件,它将引发警报(触发器),该警报将冒泡到我的警报类并引发警报本身的触发事件。它本质上是“定时器”组件。

我发现 Quartz.net 组件非常适合这个,所以我创建了一个实现 IAlarmScheduler 的 QuartzAlarmScheduler 类。

一切都好。我的问题是警报类是抽象的,我想创建很多不同种类的警报。例如,我已经有一个 Heartbeat 警报(每隔 (int) 分钟间隔触发)、AppointmentAlarm(在设定的日期和时间触发)、Daily Alarm(每天在 X 触发),也许还有其他。

Quartz.NET 非常适合处理这个问题。

我的问题是设计问题。我希望能够在我的 Alarm 类(或任何派生类)不了解 Quartz 的情况下实例化任何类型的警报。问题是 Quartz 有很棒的工厂,它们为我的警报类需要的触发器返回正确的设置。因此,例如,我可以通过使用 TriggerUtils.MakeMinutelyTrigger 为上述心跳警报创建触发器来获得 Quartz 触发器。或 TriggerUtils.MakeDailyTrigger 用于每日警报。

我想我可以这样总结。间接或直接地,我希望我的警报类能够使用 TriggerUtils.Make* 类,而无需对它们一无所知。我知道这是一个矛盾,但这就是我问这个问题的原因。

我考虑将一个委托字段放入警报中,该字段将被分配这些 Make 方法之一,但这样做我在警报和 Quartz 之间创建了一个硬依赖关系,我希望避免出于单元测试目的和设计目的。我曾想过根据here 为 QuartzAlarmScheduler 中的类型使用一个开关,但我知道这是糟糕的设计,我正在努力学习好的设计。

你们太棒了,提前感谢您的回答。

赛斯

【问题讨论】:

    标签: c# design-patterns interface


    【解决方案1】:

    也许您可以有一个 AlarmFactory 类,它会返回适当的 IAlarmScheduler 引用类型。这可能是隔离 Quartz 知识及其与不同子类的关系的一种方法。

    我不确定是否有可能使 IAlarmScheduler 的子类完全不了解 Quartz 细节,但完全有可能使 IAlarmScheduler 的客户端如此。这就是接口和工厂组合为您所做的。

    【讨论】:

      【解决方案2】:

      这是一个相当普遍的问题。您不控制框架基类,并且您想对接口进行编码。使用 HttpContext 类的 ASP.NET 中也会出现同样的问题。如果你用 google HttpContextWrapper ASP.NET MVC,你会发现很多有用的文章讨论了微软如何打破与 System.Web.Abstractions 命名空间的依赖关系。

      这是摘要。解决起来很麻烦,但这是一个很好理解的模式。

      您定义了一个接受 IAlarmScheduler 作为构造函数参数的 AlarmSchedulerWrapper。 AlarmSchedulerWrapper 将所有调用转发到组合的 IAlarmScheduler。您现在可以传入实现 IAlarmScheduler 的 QuartzAlarmScheduler。这是唯一需要依赖的地方。

      对于测试,您可以模拟 AlarmScheduler 或创建双精度 (AlarmSchedulerDouble)。可以编写 AlarmSchedulerDouble 来模拟警报事件并记录对它的调用(使其成为“间谍”)。

      如果您要实现很多具体的 AlarmScheduler 实例,您通常还会创建一个 AlarmSchedulerBase。 AlarmSchedulerWrapper 和 AlarmSchedulerDouble 会将其用作它们的基类。

      编辑:更详细地阅读您的问题,我发现这是您需要抽象出来的触发器。类似的答案。定义 IAlarmTriggers,一个通过转发到 Quartz 来实现它们的包装器,以及一个用于测试的 double。请注意,触发器实用程序可能是静态的。不过,只需从实例化的类中组合它们即可。

      【讨论】:

        【解决方案3】:

        我想我明白你想要什么以及你的问题是什么,但如果我不明白,请告诉我:)

        首先,暂时只是为了学习正确的 OOD,忘记委托、事件和 lambda。我这么说是有原因的,你以后会明白的。现在,专注于接口及其实现。您所谓的“事件”也可以实现为接口(例如 IHaveAMethodThatYouShouldCall,方法为 TheMethodToCall)。

        现在,您的 Alarm 将实现接口 IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow,唯一的方法是 ItIsTheTime。

        您的 AlarmScheduler 将使用唯一方法 RegisterAlarm(IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow alarm) 实现接口 IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime。

        此外,您的 Alarm 将在其构造函数中接收一个 IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime 类型的对象,并调用其 RegisterAlarm 并将其自身传递给它。此时传入的实际 AlarmScheduler 会将指向 Alarm 的指针存储在其私有变量中,并在需要时调用其 ItIsTheTime 方法。

        每当您创建警报时,您首先会创建警报调度程序并将其实例传递给警报的构造函数。

        我希望这是有道理的。

        【讨论】:

          【解决方案4】:

          我同意罗布的观点。 AlarmScheduler 不是需要接口的东西。 ITrigger 需要一个接口。

          修改 AlarmScheduler 使其方法与 ITrigger 接口一起使用。通过这种方式,AlarmScheduler 可以与 ITrigger 的实现一起工作,而无需知道它们是什么或它们是如何构造的。

          从这里,您可以选择如何将 ITriggers 放入调度程序。

          1. 创建引导程序以在启动时手动实例化它们。
          2. 使用依赖注入框架来做同样的事情。
          3. 使用以下方法创建一个 AlarmFactory 类:

          ITrigger CreateMyCustomTriggerType1(param1, param2, param3)

          ITrigger CreateMyCustomTriggerType2(param1)

          当我们谈到这个主题时——使用枚举来指示类型并没有错。使用枚举将指示某种 Switch 语句来决定创建哪种类型。从设计的角度来看,只要不重复 switch 语句就没有问题。如果您将 Switch 语句隔离在 FACTORY 方法后面,那么就良好的 OOP 设计而言,您已经尽职尽责了。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-05-24
            • 2016-02-21
            • 1970-01-01
            • 1970-01-01
            • 2015-09-06
            相关资源
            最近更新 更多