5

I'm having trouble coming up with a good way to structure the interfaces for two modules that are logically independent but whose implementations may be combined for the purposes of performance or efficiency.

My specific situation is the replacement of the separate queuing and logging modules in a message routing application with a combined database-backed implementation (let's call it DbQueueLog). It's easy enough for DbQueueLog to implement both the IQueue and ILogger interfaces so clients of the old queuing and logging modules can use it seamlessly. The challenge is that the most efficient and performant implementation of DbQueueLog involves combining the EnqueueMessage(Message m) and LogMessage(Message m, List<LogParams> p) methods into an EnqueueAndLogMessage(Message m, List<LogParams> p) method that minimizes the number of cross-process database calls and the amount of data written to disk. I could create a new IQueueLog interface with these new methods, but I'm uncomfortable with what that would mean if a future iteration of the application moved back to separate queuing and logging modules.

Are there any design approaches to this situation that would allow me to build the efficient, performant DbQueueLog implementation now without permanently coupling together the application's queuing and logging modules?

Edit: The application is built on Windows using C# in case any there are any platform-specific techniques that might be available.

  • +1 great question. I don't have an actual answer, but for example, Apache Solr, which is built on top of Lucene had to deal with a similar issue to yours when it came to facets: in Lucene facets are stored and queried independently of the main index. This can be slow in certain situations. Solr rewrote the whole faceting mechanism. Now facets are built in to the main index, there's no separate management of them, and performance is better: blog.mikemccandless.com/2013/02/… . – Shivan Dragon Sep 23 '13 at 14:27
  • @Dan I think, it's platform specific question, and there is no generic answer. Both late binding and compile time bindings may allow for "queuedlogger", by different mechanisms, without actually interfering with logic of one implementation or another. This may be the case for multiple inheritance. So, I guess, you need to disclose what kind of platform you are using. Again, if the system supports it, multimethods could be used. And so on. – Roman Susi Sep 23 '13 at 15:55
  • @RomanSusi Thanks, Roman. I added that it's a C#/Windows app if that helps. – Dan Sep 23 '13 at 17:12
  • @Dan Hopefully, someone helps. I am not closely familiar with C#. – Roman Susi Sep 23 '13 at 17:17
  • What would happen to the DbQueueLog example if you did decide to split it later? Or are you saying that in the sense that you might want to add an EmailQueueLog later which has two separate implementations? – Bobson Sep 25 '13 at 19:17
1

It sounds like EnqueueAndLogMessage is a pretty typical Service. My suggestion is to expose an interface to EnqueueAndLogMessage. Then, in the concrete implementation of the service, use two instance variables for your EnqueueMessage and LogMessage interfaces. This will allow you to provide one service which handles both the queing and logging capabilities, without tying you to the implementation of either. If you no longer need the combined service, it's as simple as calling a different service.

Psuedo-code:

Interface EnqueueAndLogMessageService {
   public void queAndLogMessage(Message message);
}

Class EnqueueAndLogMessageServiceImpl {
   EnqueueMessage enqueueMessageSvc;
   LogMessage logMessageSvc;

   public void queAndLogMessage(Message message) {
      enqueueMessageSvc.queMessage(message);
      logMessageSvc.logMessage(message);
   }
}
  • Unfortunately, this approach doesn't let me combine the implementations of the queue and log services. It just combines separate calls to the methods in separate service implementations. – Dan Sep 25 '13 at 16:38
  • It does, if you take the abstraction one step further to the service level. Let EnqueueAndLogMessageService extend a generic MessageService, with an interface method called "performLogging". Then, EnqueueAndLogMessageService can implement that method by calling both EnqueueMessage and LogMessage services as above. In the methods that call performLogging, make the variable an instance of the interface MessageService. This way, you can inject a different log/message service (i.e. one that just logs or queues) later on. – Steve Sep 26 '13 at 14:52
1

If they are logically independent modules that happen to share in part a common implementation, you should not create a combined interface for them, because it will haunt you if you ever decide to use separate implementations again.


On the other hand, you mentioned that one of the modules is a logging module.
If all the calls to IQueue should (ideally) be accompanied by a corresponding call to the logger, then you could decide to place that responsibility with the implementation of IQueue. A function can call EnqueueMessage and get that event logged as well at the same time without any effort.

That way, you can use your optimised implementation now, and if you ever need to split them, you can do that without affecting the users of IQueue and the logger.

If the logging interface takes additional parameters for which there is no reasonable default and the value also can't be deduced from the existing arguments to the IQueue method, then it should be clear that adding the responsibility of performing the logging to the IQueue implementation could mean that you have to change the interface of some (or all) the IQueue methods.

  • Thanks for the suggestion. I should have included the full signatures for the EnqueueMessage, LogMessage, and EnqueueAndLogMessage methods. The logging that coincides with queue operations requires additional parameters. In other words, the parameters passed to the EnqueueMessage method contain insufficient information for the required log entries to be written. – Dan Sep 25 '13 at 16:35
  • @Dan: See my edit – Bart van Ingen Schenau Sep 25 '13 at 16:57
1

I see two cases here that could be applicable to the situation that call for different approaches.

EnqueueMessage is a special case of LogMessage

Its not entirely clear from the message, but it seems if you're willing to combine EnqueueMessage and LogMessage into a single function that EnqueueMessage is simply a special case (null "p") of LogMessage. If that is the case, rather than combining interfaces I would keep them separate and use composition to your advantage. In this case ILogger would be a member of the class implementing IQueue. The EnqueueMessage method would simply use the instance of ILogger and call its LogMessage method. If in the future the implementation of an IQueue class had to change you would simply no longer instantiate an ILogger.

EnqueueMessage contains different functionality then LogMessage

In this case, I would strongly recommend they be separate classes and not have a combined interface. If they do indeed have different functionality then by combining them together you're breaking encapsulation and creating difficult to maintain code. That said, you do have to balance maintainability against performance. Just be careful you're not overestimating the efficiency gain of combining the implementation. Build it both ways and measure the difference so you can make an informed decision.

  • I am coming to a similar conclusion. Because this combination of interfaces actually requires modifications to clients if they are to make use of a new EnqueueAndLogMessage method, I am really creating a new kind of class, not simply replacing the implementation of the existing IQueue and ILogger interfaces. – Dan Sep 26 '13 at 18:38
  • Hope my answer was at least somewhat helpful. Unfortunately, real world programming problems are rarely trivial and don't always fit nice and neat into our pre-defined patterns. Good luck! – Mike Cellini Sep 26 '13 at 20:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.