4

I have a bit of code which uses a plugin and core model, revolving around four major objects: the Core, Server, Logger, and Parser, in slightly particular order.

The Core acts as the central factory, with a catch: it's bound to a single render context (much of this is graphics related, but the other details are not important). The objects it creates and manages are specific to that core, and multiple cores can be created with different contexts. Cores can never share resources with each other. The Core is initialized from a configuration file; more on this later. The Core must have, from its creation, a Server, Parser, and Logger.

The Server handles loading and initializing all the plugins, as well as creating objects from them. It acts as a single, in-process plugin server and object registry, similar in many ways to a STA in-process COM server. As plugins are loaded into the process, not a particular Core, I'm fairly sure this is appropriate. The Server must have a Parser and Logger.

The Logger handles logging messages, particularly errors, to disk. It does very simple pre-formatted file writing, with some basic "log level" filtering. The Logger requires a Parser for resolving file paths.

The Parser handles variable replacement, particularly in paths. For example, a plugin may request the file $(root)/resources/a.txt and the Parser will replace that with the application's root directory; or $(startup)/a.txt for the startup directory. Simple stuff. Variables are contained in the Parser itself, and many are specific to the thread. However, a few are tied to a single Core, and thus enters the...

Config file. Each Core is initialized from a config file, which contains a multitude of settings: plugins for the Server, variables for the Parser, and a file for the Logger. Each Core can use a different config file, and most likely will.

In my current model, the Server, Logger, and Parser are singletons. They all must provide a single global instance, accessible from the core and plugins (and each other), potentially before a Core has been created. All handle process-level functions, but the Logger and Parser do handle some Core-level functionality.

The issue with this is that, in order to make the Logger and/or Parser Core-specific, the Server would then need its own instances, which would be potentially lacking important characteristics (like what file to use for the Logger). The Server also handles work that is very clearly common to the whole process: there's just no way to load a plugin into a single Core, that's not how shared libraries work.

In a previous version, I had all four of these created when a Core was created, and belong to a specific Core. This caused some issues and oddities, however: the Core loads itself as a "plugin" when it starts up, but the Server had the same lifetime as the Core, making this seem odd. The Parser has a number of variables that belong to the process: the initial directory, root directory, and so on. The Logger must precede the Core in some way, particularly the plugin-loading stage, so errors can be accurately logged.

My questions then are:

  • Is this setup too over-engineered? The separation of responsibility between objects seems good, but perhaps too split.
  • What design pattern(s), be it singleton or otherwise, is/are appropriate in this case, for these objects?
11
  • Can you have a fifth Main object that instantiates Core, Server, Logger, and Parser? Then you can have it instantiate a Logger and Parser first, and then pass those objects as parameters to the constructors for Core and Server.
    – jhocking
    Commented Feb 16, 2012 at 19:57
  • Let me just restate your problem to make sure I understand it correctly. Your Server object is a singleton. You instantiate a number of Core objects and each Core object loads a different config file. Each Core has a Logger and Parse and the Server also has a Logger and Parser. The Core's config file has data needed by the Core's Logger and Parser. Correct so far? If so, where does the config data for the Server's Logger and Parser come from? Commented Feb 16, 2012 at 20:34
  • @RaySaltrelli That one of the problems in one of my ideas, not the current state of affairs. At the moment, the Logger and Parser are also singletons, with some finalization done when a Core is created (adding of variables and such). If the Server was to get its own instances, then the data source would become an issue.
    – ssube
    Commented Feb 16, 2012 at 20:36
  • @jhocking Why? What benefit does that provide over the current model? Having a singleton that creates, holds and passes instances vs having each object be a singleton gives the same end behavior.
    – ssube
    Commented Feb 16, 2012 at 20:37
  • OK, so let me try again :-) The Server is a singleton. You instantiate a number of Core objects and each Core object loads a different config file. The Server and Cores all share the same Logger and Parse which are also singletons. You want the Server to use the Logger and Parser with a certain default configuration and you want each Core to add additional configuration to the existing Logger and Parser from its config file when it is created. Am I getting warmer? Commented Feb 16, 2012 at 20:50

1 Answer 1

3

Here are my points:

  1. There is a difference between having only one instance of the object in the entire application vs. an object becoming singleton so that any one calling new() will magically find theInstance.

  2. In your case, there is no benefit of having server being singleton because no one is creating server other than main(). It's just plain simple to have it as ordinary object. Even if it is singleton, it will be called only once!

  3. Parser should never be singleton either for a different reason here. If i understood correctly, parser's only role is when core is launched; Ideally it should follow use-and-throw pattern rather than singleton

  4. Even parser continues to serve some specific queries till core wants it, by design parser should be one instance per core because same instance of parser should never share information about various different cores. It makes parser dedicated per core.

  5. Most people design logger as a singleton. It is not really much big deal, however, if you are a multi-threaded system a singleton logger is a big villain. First off, every singleton needs to be threadsafe. Suppose even if you make it thread safe, every time a thread x calls logger it is busy doing printf for thread y so as a result thread x waits whereas ideally you should be worrying about getting the maximum out of your quad-Core system and serve as many requests as possible.

  6. Ideally the only genuine case where logger should be centralized or singleton is where the exact order of events must be registered one-by-one. For most purposes, different files and messages with their own timestamps are usually better even for debugging.

So for me Server (which is one instance) is created by main() and core, parser and logger are a tuple that is born and dies per request (always together). Each tuple doesn't know the state or existence of other such tuples, and that is good so that arbitrary number of threads can be instantiated in parallel, (one thread per tuple) without losing scalability but preserving loose coupling.

Only thing is - it means you are not using any design pattern! But that's not a bad thing. It is not necessary that including a design pattern makes your design necessarily better.

Remember:

There are really rare reasons where using singleton becomes must; everywhere else, using singleton is always an invitation to problems.

3
  • For 2, any plugin or most others bits of code may access the Server in order to create objects or load other plugins, but there must never be competing servers, hence the use of a singleton. I am well aware of all the implications of the (anti-)pattern, but that object is the ideal use case for it. For 3 , the Parser is also available to any plugin and at any time, but (4) it is Core-specific, so trying to move it there will likely help. For 5, the singleton creation and all calls are thread-safe (all the objects and their methods are, in fact)...
    – ssube
    Commented Feb 16, 2012 at 23:33
  • The log messages are formatted as-needed, so if the Logger can early-out based on severity level, the format will never occur, otherwise the format is triggered, then a brief lock for atomic write. What I do want is for all the log info from a single run of a single process to go into a single file, for a variety of reasons which include 6. Most logging only happens during error handling, at which point performance is tenth concern to in-order logging (stamped to the ms), making sure the log makes it to disk (the log is flushed if an error is logged), and so on.
    – ssube
    Commented Feb 16, 2012 at 23:53
  • I do think you're onto something with the tuple idea and 4, potentially with a global variable dictionary instead of a global parser.
    – ssube
    Commented Feb 16, 2012 at 23:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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