Kijana Woodard

Software Minimalism


Introducing Liaison

Friday, October 25, 2013

Well nimbus, you had a great run, but now it's over. Make room for Liaison.

While I was building nimbus, something was nagging me. It was great and flexible and web scale and all, but...

Nimbus is utter bloatware!

Mike Pennington summed it up in the comments:

I'm somewhat conflicted about this blog post. I like what you're doing, and the code is very clean and concise. And ISP is followed such that, as you mention, units of work are separate and can be tested as actual units. Although, on the other hand, it feels a little bit like magic.

Even with so much magic removed, so much magic remained. There are many subtle "features". You can mix void handlers with result handlers. You can use base types for message handlers to generalize them. You can choose scalar vs class results. There's a lot squeezed in there.

You can also run into trouble. If you use handlers with a mix of result types or Send using a type not used in Subscribe, you get a RuntimeBinderException. I added comments to give you a head's up, but I found myself lost a couple of times.

I considered making a "strict mode" for nimbus that would throw if you didn't use the same types for handlers and subscribe. You would have to decorate any non-conforming handlers.

Then I started to think: what work is nimbus really doing?

Nimbus is passing the message and result to each handler in the order specified.

The goal is to isolate the units from each other and separate the organization of the units from the units themselves. Ok, what if we just code that?

Now the mediator configuration for Posts on this blog looks like this:

mediator.Subscribe<PostRequest, PostGetViewModel>(message =>
{
    var result = new PostGetViewModel();
    result = new FilteredPostVault().Handle(message, result);
    result = new MarkdownContentStorage(root).Handle(message, result);
    return result;
});

That code could be cut down to two lines, but I found this more readable. It should be clear now how the message is transformed into a result.

If we don't like inline functions, we can do this:

public static void RegisterContainer()
{
    ...
    mediator.Subscribe<PostRequest, PostGetViewModel>(Execute);
    ...
}
...
private static PostGetViewModel Execute(PostRequest message)
{
    var result = new PostGetViewModel();
    result = new FilteredPostVault().Handle(message, result);
    result = new MarkdownContentStorage(root).Handle(message, result);
    return result;
}

If we don't want a bunch of functions, we can use classes:

mediator.Subscribe<PostRequest, PostGetViewModel>(message => 
    new HandlePostGetViewModel().Handle(message));
...
public class HandlePostGetViewModel
{
    public PostGetViewModel Handle(PostRequest message)
    {
        var result = new PostGetViewModel();
        result = new FilteredPostVault().Handle(message, result);
        result = new MarkdownContentStorage(root).Handle(message, result);
        return result;
    }
}

Wait. Wait. Wait! Whoa! Whoa! Whoa! Whoa. Whoa. Whoa. Whoa.

Whoa.

Isn't that where we started??!?

Yes and no.

We've come full circle, but along the way, we've dropped a lot of dead weight and clarified our approach to code considerably.

How do we keep from going off the reservation and making spaghetti in our Subscriptions?

  • Handlers will have 0 or 1 dependencies.
  • The 1 dependency will be the mediator or a fully constructed singleton.
  • Prefer a derivation of the singleton - store.OpenSession().
  • The singleton should generally be from another library - i.e. persistence lib.
  • Handlers will have void/Unit return type or the same return type specified in the Subscribe.

I think I prefer either the inline function or methods within the configuration class over classes. I'll try it out in a couple projects and see. As always, copy/paste into your own project and salt to taste.

The Liaison source code is now 60 lines. About half of that is cruft due to the fact that c# has void Actions as opposed to having Func<Unit>. I thought about forcing a result to reduce the LoC, but I'd rather have a nicer api.

Another nice side effect of the simpler code is a 3x performance boost vs nimbus. I'm happy with 9M operations per second.

One thing I think I miss is the IHandle interface. Maybe I'm just being sentimental, but it does enforce rules for method names [Handle vs Execute vs ???]. Add the interfaces if it helps keep your codebase consistent.

On a minor note, I named nimbus with the project, solution, folders, etc all lowercase. It turns out, I prefer being idiomatic for the language in play. Javascript methods should be doSomething() and c# methods should be DoSomething(). Liaison is cased properly.

;-]



If you see a mistake in the post *or* you want to make a comment, please submit an edit.

You can also contact me and I'll post the comment.

15 Comments...
Joey Guerra
Joey Guerra • 4 years ago

This reminds me of a pub sub implementation, which of course, I LOVE!

Joey Guerra
Joey Guerra • 4 years ago

And I couldn't help but http://www.youtube.com/watch?v... to the web scale link.

khalidabuhakmeh
khalidabuhakmeh • 4 years ago

This reminds me of what Fubu MVC is doing, where actions can be chained. I am still on the fence of whether I would use something like this or not, since the cognitive overhead might not be worth the addition of the code.

Afif Mohammed
Afif Mohammed • 4 years ago

Have you looked at TInyMessenger (part of TinyIoC)? Its a more full fledged in memory bus with pub sub semantics, all rolled into one file.

Kijana Woodard
Kijana Woodard • 4 years ago

Nice. Thanks for the heads up.

Liaison is aiming to find a minimum level of abstraction. It's more an exercise in understanding than anything. For instance, just responding to this comment, I've thought of a couple way to further simplify my stack. :-]

Afif Mohammed
Afif Mohammed • 4 years ago

And you're doing great. I love minimalism too, but often, in that pursuit I think I am probably going to miss out on important concerns that are there for a reason in other libraries. I am searching hard for a great in memory bus, that lets me do true event driven programming (think NSB API), where one can do a send and publish, and send is from 1 to many, handled by only one, and publish is from one and only one, handled by zero to many. The hardest I find in this pursuit is implementing Unit of work around the message handler, and still keeping it dead simple.

Kijana Woodard
Kijana Woodard • 4 years ago

Thanks.

I started this journey looking at NSB 4.0 "In Memory Publish" and realized I really wanted to do Send, which it didn't have. I didn't realize how far that would take me. :-]

I started out with nimbus (http://kijanawoodard.com/intro...) so that I could register handlers for messages anywhere, like nsb. What I quickly realized is that you get into cases like "I need the handlers to run in a particular order", "This handler needs two parameters", "This handler needs zero parameters", "This handler uses the result of the work of the first 3 handlers (an Event)".

Once there, I either needed to retreat to a "full featured" container, although that doesn't solve the ordering issue (see nsb's .First<t>(), etc), or I needed something else.

What I've found is that the "orchestration code" is fairly concise to write manually and keeps a lot of if/switch logic out of the handler code. Not to mention, I don't have to learn the incantations of the container.

I still owe you a blog post with more details and code. :-]

Kijana Woodard
Kijana Woodard • 4 years ago

Also, it's interesting that you wrote this, because I was thinking of adding a "publish" feature, which really amounts to allowing multiple subscriptions for a message.

I've tried not to impose concepts like Command vs Event and I think I can continue to do that, but allow "send to many".

I haven't decided whether it should just be Send -> "send to whatever is registered" or if I should do something like Send -> "send to subscriptions[0]" and Publish -> "send to all subscriptions".

I'll write the code and let it decide. :-]

Afif Mohammed
Afif Mohammed • 4 years ago

To be honest, at my last assignment, I did create an in memory pub sub mechanism to let the team get their head around event driven architecture without all the queues and distribution. We used that to move from procedural style to events and commands, and that paved the way for NSB to come in later. I am itching to write that again, but want to look around so I can borrow 'good' ideas, or even better just use something that fits the bill.

Kijana Woodard
Kijana Woodard • 4 years ago

I'm sorta pushing "roll your own" with this code since it's single file inclusion only.

I have used it in enough mini projects that I'm considering putting it on nuget just for my own sake. But I want to let the api settle a bit. I still think it's a bit bloated, if you can believe that.

The key to "in memory publish" in a web request is transactions. If we're honest, we really only get one. Beyond that you're pushing your luck or opening up a can of worms. If the request fatals, where are you? Can you restart? From which point?

So a real "publish" where "this is an Event that happened in the past" (meaning saved to disk somewhere for all time), isn't friendly in memory.

I _think_ I'd like to add a way for disconnected code to register "background tasks" within the same unit of work as the main request handler code. The background tasks would then carry out follow on options like "send an email", "update the stats screen", etc. I'm not sure that this use case is worth the mental disconnect of not being able to trivially see what's going to be saved when the commit occurs.

Afif Mohammed
Afif Mohammed • 4 years ago

Also, the moment you have something that lets you do messaging, I feel its imperative to demonstrate the difference between command and event. Without that it feels messages are flying everywhere for no rhyme or reason. Hiding in the sheep's clothing of decoupling behind messages developers will write code that has all interesting side effects.

Afif Mohammed
Afif Mohammed • 4 years ago

That looks familiar to what Oren has done with Racoon blog. Interesting thoughts.

Kijana Woodard
Kijana Woodard • 4 years ago

That's where I stole the idea.

Oren's Limit your Abstractions series is pivotal to my line of thinking on this. If I can drop IFoo, IGoo, IThisService, and IThatService and boil things down to IMediator (or whatever), clarity is what emerges.

I find the constraint similar to ReST constraints. At first you find it hampers you. But soon, you see that what you were doing before was "making stuff up" and not focusing on the essence of what needed to be done. At the end, your code becomes a tightly coupled mess of Interfaces that were suppose to solve the coupling problem but didn't because you just wrote the same old procedural spaghetti code hidden behind an interface.

Afif Mohammed
Afif Mohammed • 4 years ago

Couldn't agree more.

Kijana Woodard
Kijana Woodard • 4 years ago

I know how you feel. Since I'm going for minimalism, I didn't want to impose that view _from_ Liaison. So if you want IEvent and ICommand and IHandle<t>, etc go ahead. Liaison won't _force_ you to do that, but it's easy to overlay. Plus, I didn't want to write "unobtrusive mode". :-]