Pattern to use (if any) to co-ordinate loosely coupled classes with strong interdependencies
https://softwareengineering.stackexchange.com/questions/309301
-
12-12-2020 - |
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 aListen
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
- calls
IServiceMonitor<IService>
: monitors the service:- exposes
Start
method which invokes theIService.Listen()
- exposes
Stop
method which disposesIService
- exposes
Pause
andResume
methods which respectively zero or reset theIService.NumberOfThreads
- calls
CreateRemoteConfigClient()
to get a client every 90 seconds, thenIRemoteConfigClient.GetConfig
- notifies any configuration changes to
IMessageClient
,IService
, and any subsequentIMessageHandler
- exposes
IMessageClient : IDisposable
; exposesGetNextMessage
which:- long polls a message queue for the next request
IMessageHandler : IDisposable
; exposesHandleMessage
which:- does something with the message, requesting on the way further
IXyzClient
s from theIFactory
to access other services
- does something with the message, requesting on the way further
IRemoteConfigClient : IDisposable
; exposesGetConfig
which:- retrieves any remote overrides of the current configuration state
This has led me to create:
IFactory
; with the following members:CreateMonitor
: returns a newIServiceMonitor<IService>
GetService
: returns theIService
created to accompany the most recentIServiceMonitor
, or a newIService
- NB: a Service should be able to be obtained without a Monitor having been created
CreateMessageClient
: returns a newIMessageClient
- Either:
CreateMessageHandler
: returns anew IMessageHandler
MessageHandlerDelegate
: creates anew IMessageHandler
and invokesHandleMessage
CreateRemoteConfigClient
: returns anew IRemoteConfigClient
Implementations of the core interfaces accept the IFactory
in their constructors.
This is so that:
IService
can callCreateMessageClient()
to get a singleIMessageClient
which it willDispose
when it's doneIServiceMonitor
can callGetService()
to allow it to coordinate and monitor theIService
IMessageHandler
can report its progress back viaIMessageClient
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?!
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.