开发者

Design Question - how do you break the dependency between classes using an interface?

I apologize in advance but this will be a long question.

I'm stuck. I am trying to learn unit testing, C#, and design patterns - all at once. (Maybe that's my problem.) As such I am reading the Art of Unit Testing (Osherove), and Clean Code (Martin), and Head First Design Patterns (O'Reilly).

I am just now beginning to understand delegates and events (which you would see if you were to troll my SO questions of recent). I still don't quite get lambdas.

To contextualize all of this I have given myself a learning project I am calling goAlarms. I have an Alarm class with members you'd expect (NextAlarmTime, Name, AlarmGroup, Event Trigger etc.)

I wanted the "Timer" of the alarm to be extensible so I created an IAlarmScheduler interface as follows...

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);
}

This IAlarmScheduler interface define a component that will R开发者_开发百科AISE an alarm (Trigger) which will bubble up to my Alarm class and raise the Trigger Event of the alarm itself. It is essentially the "Timer" component.

I have found that the Quartz.net component is perfectly suited for this so I have created a QuartzAlarmScheduler class which implements IAlarmScheduler.

All that is fine. My problem is that the Alarm class is abstract and I want to create a lot of different KINDS of alarm. For example, I already have a Heartbeat alarm (triggered every (int) interval of minutes), AppointmentAlarm (triggered on set date and time), Daily Alarm (triggered every day at X) and perhaps others.

And Quartz.NET is perfectly suited to handle this.

My problem is a design problem. I want to be able to instantiate an alarm of any kind without my Alarm class (or any derived classes) knowing anything about Quartz. The problem is that Quartz has awesome factories that return just the right setup for the Triggers that will be needed by my Alarm classes. So, for example, I can get a Quartz trigger by using TriggerUtils.MakeMinutelyTrigger to create a trigger for the heartbeat alarm described above. Or TriggerUtils.MakeDailyTrigger for the daily alarm.

I guess I could sum it up this way. Indirectly or directly I want my alarm classes to be able to consume the TriggerUtils.Make* classes without knowing anything about them. I know that is a contradiction, but that is why I am asking the question.

I thought about putting a delegate field into the alarm which would be assigned one of these Make method but by doing that I am creating a hard dependency between alarm and Quartz which I want to avoid for both unit testing purposes and design purposes. I thought of using a switch for the type in QuartzAlarmScheduler per here but I know it is bad design and I am trying to learn good design.

You guys are awesome and thanks in advance for your answers.

Seth


Perhaps you could have an AlarmFactory class that would return the appropriate IAlarmScheduler reference type. It might be one way to isolate the knowledge of Quartz and its relationship to different subclasses.

I'm not sure it'll be possible to make it so subclasses of IAlarmScheduler are completely ignorant of Quartz details, but it's entirely possible to make it so clients of IAlarmScheduler are. That's what the interface and factory combination does for you.


This is a fairly common problem. You don't control the framework base class and you want to code to an interface. This same problem occurs in ASP.NET with the HttpContext class. If you google HttpContextWrapper ASP.NET MVC you will find a lot of useful articles that discuss how Microsoft broke the dependency with the System.Web.Abstractions namespace.

Here's the summary. It's a hassle to work around, but it's a well understood pattern.

You define an AlarmSchedulerWrapper that accepts an IAlarmScheduler as a constructor parameter. The AlarmSchedulerWrapper forwards all calls to the composed IAlarmScheduler. You can now pass in your QuartzAlarmScheduler that implements IAlarmScheduler. That's the only place you'll need the dependency.

For testing, you can either mock the AlarmScheduler or create a double (AlarmSchedulerDouble). The AlarmSchedulerDouble can be written to simulate alarm events and record calls against it (making it a "Spy").

If you are going to be implementing a lot of concrete AlarmScheduler instances, you typically would also create an AlarmSchedulerBase. AlarmSchedulerWrapper and AlarmSchedulerDouble would use it as their base class.

Edit: Read your question in more detail and I see that it's the triggers you need to abstract away. Similar answer. Define IAlarmTriggers, a wrapper that implements them by forwarding to Quartz, and a double for testing. Note the trigger utilities are probably static. Still, just compose them from an instantiated class.


I think I understand what you want and what your problem is, but let me know if I don't :)

First of all, for the time being, just for the purpose of learning proper OOD, forget about delegates, events, and lambdas. There's a reason I'm saying this, you will understand it later. For now, focus on Interfaces and their implementations. What you call "event" can be implemented as an interface as well (e.g. IHaveAMethodThatYouShouldCall with method being TheMethodToCall).

Now, your Alarm would implement interface IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow, with only method being ItIsTheTime.

Your AlarmScheduler would implement interface IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime with the sole method RegisterAlarm(IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow alarm).

Additionally, your Alarm will take in its constructor an object of type IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime and call its RegisterAlarm passing its own self to it. At this moment the actual AlarmScheduler passed inside will store the pointer to Alarm in its private variable, and call its ItIsTheTime method when needed.

Whenever you create Alarm, you would first create AlarmScheduler and pass its instance to the Alarm's constructor.

I hope this makes sense.


I agree with Rob. AlarmScheduler is not the thing that needs an interface. ITrigger needs an interface.

Modify AlarmScheduler so that it's methods work with ITrigger interfaces. This way, AlarmScheduler can work with implementations of ITrigger without knowing what they are or how they're constructed.

From here, you have choices about how to get ITriggers into the Scheduler.

  1. Create a bootstrapper to manually instantiate them at startup.
  2. Use a Dependency Injection framework to do the same thing.
  3. Create an AlarmFactory class with methods like:

ITrigger CreateMyCustomTriggerType1(param1, param2, param3)
and
ITrigger CreateMyCustomTriggerType2(param1)

And while we're on the subject--there's nothing wrong with using Enums to dictate type. Using an enum would dictate some sort of Switch statement to decide which type to create. From a design perspective, there's no problem with that as long as the switch statement is not repeated. If you isolate the Switch statement behind a FACTORY method, you've done due dilligence as far as good OOP design is concerned.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜