设计问题 - 如何使用接口打破类之间的依赖关系?

时间:2010-06-02 01:27:49

标签: c# design-patterns interface

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

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

我现在才刚刚开始了解代表和事件(如果你是最近我的问题,你会看到)。我还是不太喜欢lambdas。

为了将所有这些情境化,我给自己开了一个学习项目,我称之为goAlarms。我有一个你想要的成员的Alarm类(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接口定义了一个组件,它将触发警报(Trigger),它将冒泡到我的Alarm类并引发警报本身的触发事件。它本质上是“Timer”组件。

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

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

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

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

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

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

你们很棒,并提前感谢你们的答案。

赛斯

4 个答案:

答案 0 :(得分:1)

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

我不确定是否有可能使它成为IAlarmScheduler的子类完全不知道Quartz的细节,但完全有可能让它成为IAlarmScheduler的 clients 。这就是界面和工厂组合为您所做的。

答案 1 :(得分:0)

这是一个相当普遍的问题。您不控制框架基类,并且您希望编码到接口。使用HttpContext类在ASP.NET中发生同样的问题。如果您使用Google HttpContextWrapper ASP.NET MVC,您会发现很多有用的文章,讨论Microsoft如何破坏与System.Web.Abstractions命名空间的依赖关系。

以下是摘要。解决这个问题很麻烦,但这是一个很好理解的模式。

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

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

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

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

答案 2 :(得分:0)

我想我明白你想要什么,你的问题是什么,但如果我不知道,请告诉我。)

首先,暂时只是为了学习正确的OOD,忘记委托,事件和lambda。我说这个是有原因的,你以后会理解的。目前,重点关注接口及其实现。你所谓的“事件”也可以作为一个接口实现(例如IHaveAMethodThatYouShouldCall,方法是TheMethodToCall)。

现在,你的闹钟将实现接口IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow,只有方法是ItIsTheTime。

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

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

每当您创建Alarm时,您首先要创建AlarmScheduler并将其实例传递给Alarm的构造函数。

我希望这是有道理的。

答案 3 :(得分:0)

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

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

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

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

    ITrigger CreateMyCustomTriggerType2(param1)

    虽然我们正在研究这个主题 - 但使用Enums来决定类型是没有错的。使用枚举将指定某种类型的Switch语句来决定要创建的类型。从设计的角度来看,只要不重复switch语句就没有问题。如果您将F语句背后的Switch语句隔离开来,那么就完美的OOP设计而言,您已经完成了尽职调查。