Domanda

I have a collection of cooperative classes whose behaviors are interdependent upon one another. But I wish to keep them loosely coupled, so I've created appropriate interfaces.

I want to determine an appropriate pattern to instantiate specific implementations of these objects.

Here's an outline of their interdependencies:

  • IService : IDisposable: listens for messages; exposes a Listen method which:
    • calls IMessageClient.GetNextMessage iteratively
    • invokes a (delegate which creates a?) new IMessageHandler instance in a new thread for each message
    • up to NumberOfThreads concurrent threads
  • IServiceMonitor<IService>: monitors the service:
    • exposes Start method which invokes the IService.Listen()
    • exposes Stop method which disposes IService
    • exposes Pause and Resume methods which respectively zero or reset the IService.NumberOfThreads
    • calls CreateRemoteConfigClient() to get a client every 90 seconds, then IRemoteConfigClient.GetConfig
    • notifies any configuration changes to IMessageClient, IService, and any subsequent IMessageHandler
  • IMessageClient : IDisposable; exposes GetNextMessage which:
    • long polls a message queue for the next request
  • IMessageHandler : IDisposable; exposes HandleMessage which:
    • does something with the message, requesting on the way further IXyzClients from the IFactory to access other services
  • IRemoteConfigClient : IDisposable; exposes GetConfig which:
    • retrieves any remote overrides of the current configuration state

This has led me to create:

  • IFactory; with the following members:
    • CreateMonitor: returns a new IServiceMonitor<IService>
    • GetService: returns the IService created to accompany the most recent IServiceMonitor, or a new IService
      • NB: a Service should be able to be obtained without a Monitor having been created
    • CreateMessageClient: returns a new IMessageClient
    • Either:
      • CreateMessageHandler: returns a new IMessageHandler
      • MessageHandlerDelegate: creates a new IMessageHandler and invokes HandleMessage
    • CreateRemoteConfigClient: returns a new IRemoteConfigClient

Implementations of the core interfaces accept the IFactory in their constructors. This is so that:

  • IService can call CreateMessageClient() to get a single IMessageClient which it will Dispose when it's done
  • IServiceMonitor can call GetService() to allow it to coordinate and monitor the IService
  • IMessageHandler can report its progress back via IMessageClient

IFactory, of course, started out ostensibly as an implementation of the Factory pattern, then it began to lean more towards a Builder pattern, but in reality none of those feel right. I'm Create-ing some objects, Get-ting others, and certain things, like the fact that a subsequent call to CreateMonitor will modify the result of GetService, just feel wrong.

What's the right naming convention for a class which co-ordinates all these others, and IS there an actual pattern that can be followed, am I over-engineering, or am I over-analyzing?!

È stato utile?

Soluzione

You could try the Mediator pattern. We used it a couple of times.

For example:

// interface for all "Collegues" 
public interface IColleague
{
    Mediator Mediator { get; }
    void OnMessageNotification(MediatorMessages message, object args);
}
public enum MediatorMessages
{
    ChangeLocale,
    SetUIBusy,
    SetUIReady
}
public class Mediator
{
    private readonly MultiDictionary<MediatorMessages, IColleague> internalList =
        new MultiDictionary<MediatorMessages, IColleague>(EnumComparer<MediatorMessages>.Instance); //contains more than one value per key
    public void Register(IColleague colleague, IEnumerable<MediatorMessages> messages)
    {
        foreach (MediatorMessages message in messages)
            internalList.AddValue(message, colleague);
    }
    public void NotifyColleagues(MediatorMessages message, object args)
    {
        if (internalList.ContainsKey(message))
        {
            //forward the message to all listeners
            foreach (IColleague colleague in internalList[message])
                colleague.MessageNotification(message, args);
        }
    }

    public void NotifyColleagues(MediatorMessages message)
    {
        NotifyColleagues(message, null);
    }
}

And now the collegues (or controllers in our case implemented it like this):

public class AddInController : IColleague
{
    public Mediator Mediator
    {
        get { return mediatorInstance; }
    }

    public AddInController()
    {
        Mediator.Register(this, new[]
                                    {
                                        MediatorMessages.ChangeLocale,
                                        MediatorMessages.SetUIBusy,
                                        MediatorMessages.SetUIReady
                                    });
    }

    public void OnMessageNotification(MediatorMessages message, object args)
    {
        switch(message)
        {
            case MediatorMessages.ChangeLocale:
                UpdateUILocale();
                break;
            case MediatorMessages.SetUIBusy:
                SetBusyUI();
                break;
            case MediatorMessages.SetUIReady:
                SetReadyUI();
                break;
        }
    }
}

Altri suggerimenti

 I have a collection of cooperative classes whose behaviors are 
 interdependent upon one another. But I wish to keep them loosely coupled...

Careful now!

When you are thinking that exact thought it's time to take a step back and look at what you are trying to achieve and why. Loose coupling is great, but it is not a goal of its own. Every time you make looser coupling you are also getting lower cohesion. Cohesion is just as important for the maintainability of a code base as coupling (although we tend to talk about it less).

Maybe it's hard to coordinate the interactions between these classes because they are too loosely coupled? I'm not saying that it's certain, but it is something that you should consider.

  • Are all of these classes always used as a single unit or are you sometimes only using one or two of these classes somewhere else?
  • Are you actually making multiple implementations of each class or are you just introducing looser coupling in order to easier unit test or follow "best practices"?

If you answer yes to the first question and "only single implementations" to the second one then you may be missing an abstraction level. In this case, I'd say that this entire subsystem containing this collection of classes is actually your "unit", not each class in itself. And then you may actually want to get rid of the loose coupling and treat the entire system as a single unit instead of treating each part as a unit in an of itself.

This advice may be wrong though. But I find that when I am asking myself questions such as what you are asking then I generally end up with the best code when I choose high cohesion over loose coupling. So as usual, think carefully and determine what is best for you in your situation.

At this point, all you have observed is that in a server-side application, classes have a wide variety of dependency, creation, interaction, and scope management requirements. Some classes have temporal coupling between them; some classes need to Lazy<> all of their dependencies to delay their instantiations to the last possible moment. And threads are interesting beasts.

If you are using a very good C# dependency injection (DI) framework that is in active development and has a good active user base, it should be able to handle the entire range of requirements for all of your classes. But first you have to be very sure of these requirements. Using the wrong settings will introduce subtle weaknesses into your software.

If you are in fact trying to implement your own dependency injection framework, it is important that you start reading about the design documents of some existing DI frameworks. Otherwise you will need much longer time to research the best approaches.

The fear of over-engineering is deserved. The most likely outcome for your current code base is that it will be abandoned some time in the near future, since it is a product of your learning process. With better knowledge you will soon find it beneficial to start over from a blank slate (while borrowing lots of code fragments, patterns, and knowledge).

That said, use of factory or DI seems to be necessary, because some of your classes are singletons by definition - there can only be one instance. Classes that need to communicate with these singletons should not be allowed to instantiate them, otherwise multiple instances might be created.

After you have experimented with the DI / Factory for a while, you may discover that loose coupling might not be the biggest concern of your project. You may find that these other concerns deserve more of your attention:

  • Network timeouts, because the network could be disconnected anytime, or some packets may never arrive
  • Duplicated messages
  • Malformed or invalid messages
  • Local execution timeouts or failure.
  • Local resource exhaustion
  • Retries
    • Exponential backoff for certain types of failures
  • Graceful handling of unknown or totally unexpected forms of failures
    • Example: one process is stopped inside a debugger but another process is still running
    • Example: the network drive stops responding

Your software may have to have the capability to parse older versions of message formats, but the fear of having to load different build versions of C# assemblies is sometimes overblown. You can always rebuild the entire set of C# assembly under your control so that they are always consistent.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
scroll top