Chaining layers with IoC, setting lower callback t

2019-06-01 10:46发布

问题:

I have a scenario where I need a lower layer to be controlled by an upper layer much like a puppet master pulling on strings.

The lower layer also will call back to the upper layer as some internal events are generated from time to time.

I am using SimpleInjector, I inject the ILower in to the Upper constructor. I cannot inject the Upper in to the Lower as it would cause a circular reference.

Instead I have a register callback function to link the two layers. However, I have to scatter my code with null checks.

Are there any nicer ways or different architectures to achieve this linking of objects?

// an interface that transport can callback from transport to client
public interface ILowerToUpperCallback
{
    void ReplyA();
    void ReplyB();
}

// transport interface that client calls
public interface ILower
{
    void Test1();
    void Test2();
    void RegisterCallback(ILowerToUpperCallback callback);
}

public class Upper : ILowerToUpperCallback
{
    private readonly ILower lower;

    public Upper(ILower lower)
    {
        this.lower = lower;
        this.lower.RegisterCallback(this);
    }

    void ReplyA()
    {
    }

    void ReplyB()
    {
    }
}

public class Lower : ILower
{
    private ILowerToUpperCallback callback;

    /* this is not possible, would cause a circular reference
    public Lower(ILowerToUpperCallback callback)
    {
        this.callback = callback;
    }
    */

    // set by different method instead, what happens if this is never set?!
    void RegisterCallback(ILowerToUpperCallback callback)
    {
        this.callback = callback;
    }

    void OnTimer()
    {
        // some timer function

        if(this.callback != null) // these null checks are everywhere :(
            this.callback.ReplyA();
    }
}

回答1:

I don't think there is anything wrong with your design, although you already noticed that it isn't the easiest thing to configure. The problem isn't in the limitations of your DI framework, but more in the mental gymnastics you'll have to perform.

Here is an idea. Change your classes to the following:

public class Upper : IUpper, ILowerToUpperCallback
{
   public Upper(/* all depedencies except ILower */) { }

    // Promote ILower to property dependency
    public ILower Lower { get; set; }
}

public class Lower : ILower
{
    // Use the Null Object Pattern for default implementation to prevent
    // null checks.
    private ILowerToUpperCallback callback = new NullCallback();

    public Upper(/* all dependencies except ILowerToUpperCallback */)
    {
        this.callback = callback;
    }

    // Allow overriding the default implementation using a method, just
    // as you are already did.
    public SetCallback(ILowerToUpperCallback callback)
    {
        if (callback == null) throw new ArgumentNullException("callback");
        this.callback = callback;
    }
}

With this design you can wire everything up as follows:

container.Register<ILower, Lower>();
container.Register<IUpper, Upper>();

container.RegisterInitializer<Upper>(upper =>
{
    var lower = (Lower)container.GetInstance<ILower>();
    lower.SetCallback(upper);
    upper.Lower = lower;
});

Since Lower and Upper are normal services you can resolve them as usual. By registering an initializer delegate for Upper, you can do some extra initialization after the container created Upper. This initializer wires Lower and Upper together. Since the Composition Root knows about ILower and Lower, we can safely cast from ILower to Lower, without breaking any rule. Best about this design is that the ILower interface is kept clean, and oblivious of the ILowerToUpperCallback, which is in fact an implementation detail.



回答2:

One of possible approaches would be to have a messaging subsystem in one of your lowest layers so that both Lower and Upper layers can participate in a pub/sub model.

The messaging subsystem could for example be an EventAggregator. It does a good job in decoupling publishers from subscribers. With a dedicated event model, accessible from both layers, you can pretty much control any object with yet another object.

However, I am not against your approach. A circular dependency is nothing wrong. For example, the MVP (Model-View-Presenter) is based on a dependency between Views and Presenters.



回答3:

Your example is a little abstract and hard to get a feel for your architecture, but it seems like you could possibly benefit from the concept of Domain Events. It seems like your callback idea is along the same lines as well. I recently implemented a simple version of the domain event pattern that lets my major business layer components work together without having the components reference each other. As you said, Upper can reference ILower, but then having Lower reference IUpper causes a circular reference and could lead to issues.

Here is the SO question that helped me work towards my solution.

Business Layer Facade vs Mingled Business Components

I also found these two links helpful in understanding the Domain Events concept and implementation.

http://jasondentler.com/blog/2009/11/simple-domain-events/

http://blog.robustsoftware.co.uk/2009/08/better-domain-event-raiser.html



回答4:

First of all, your code looks fine to me.

However, this looks like the Observer pattern.

Your second concern (the null checks) could be handled by initializing the callback/observer field with a Null Object (which is a do-nothing implementation of IObserver/ILowerToUpperCallback).