Uniformity: good or bad?
Note: If you're not interested in the real world examples and philosophy at play here, skip this section.
Uniformity is great. Everything looks and feels the same, it creates a sense of order, and it's a lot easier to handle many things when all things handle the same. But, at the same time, uniformity can have its annoyances. By doing its best to get everyone to conform to those common rules, it also willfully ignores the need for someone to be different, and the conflict that arises from not being able to be different.
Taking a simple real world example, it's much easier for a manufacturer to only have to create one-size-fits-all caps, than it is to create them in different sizes, manage each size's stock levels, and having to selectively ship caps per available size. On the other hand, customers might only barely fit their one-size cap, whereas they can get a much better fit when they have the freedom to choose between different sizes.
Another real world example is fighter plane cockpits. The US used to have standardized cockpit layout, based on the average measurements of every fighter pilot. However, they eventually discovered that, counterintuitively, the average cockpit layout was a bad fit for pretty much everyone.
If you took the sum of the "too big" and "too small" complaints, they'd average out to a near-zero, but the vast majority of feedback was negative, which shows how bad uniformity was for the case of cockpit layout and size.
Why am I telling you these non-coding stories? Because there is no right or wrong here. It's a matter of focusing on what's important to you.
- The ball cap manufacturer may prioritize its manufacturing cost over customer satisfaction, which is common with large scale producers. Or, if they're targeting a more niche audience, they may instead focus on customer satisfaction at the cost of some production optimizations.
- The air force has to choose whether the added comfort and decreased risk from having an adjustable cockpit outweighs the cost that comes with making those cockpits adjustable.
You have two options, and you have to choose between them. One offers increased efficiency, the other offers higher quality. You have to decide which matters most to you. As with all things in life, it's a cost/benefit analysis.
Your expectations
The uniformity in creating a reusable approach between sync and async tasks comes at the cost of some small annoyances; so is the uniformity worth it to you?
To put my question more concretely: would you rather have two different ways of handling commands (sync/async), and spend the needed time and effort to always make sure you're using the right one; or would you rather be able to handle any command (whether sync or async) the same way and not have to bother with differentiating between the two?
ICommand command = CommandFactory.From(string)
This code suggests to me that you want to streamline how your commands are handled, and you don't want to have to handle sync vs async commands differently. And that makes a lot of sense, since doing something along the lines of...
ISyncCommand syncCommand = SyncCommandFactory.From(string);
IAsyncCommand asyncCommand = AsyncCommandFactory.From(string);
...wouldn't make a lot of sense, because how can the consumer know if the intended command is intended to be sync or async, if it doesn't even parse the string itself? It's the factory's job to decide which command it is, and it's up to the command itself to define whether it's async or sync. The consumer of the command factory really doesn't factor into that decision-making process.
This works but it feels weird to have a executeAsync even when the command is sync.
The only reason this is really an issue, is because of the naming convention that expects Async
to be tagged onto the name of any async (or otherwise task-returning) method. Had that naming convention not existed, you would've just called your method Execute
and the naming conflict would disappear.
Cost vs benefit
The issue here is one that is best encapsulated by one of the quotes I use surprisingly often in the context of software development:
With great power comes great responsibility
In other words, if you want the ability to knowingly handle sync/async commands differently; then you take on the responsibility of needing to know that a command is sync or async, and how to handle both sync and async commands.
Comparatively, if you instead relinquish that control, you don't need to know the difference between the commands, and can use them interchangeably.
The cost of implementing a separate handling for sync/async commands is clear. But what are the benefits? Based on your explanation and my experience with developers talking about this, the alleged benefits are twofold:
- It feels weird to have a
executeAsync
even when the command is sync.
As far as naming conflicts go, this is a very mild one. Additionally, this is an exceptional circumstance because of the naming convention for async methods.
I can't fully dismiss this concern because there is a grain of truth in there, but I do think that this is the tiniest of concerns, and is without a doubt negligible here compared to the cost of implementation.
- Returning a
Task
even though no async code is used, i.e. nothing is awaited internally.
The most terse counterargument I have for this is that if this wasn't supposed to be done, then Task.FromResult()
and Task.CompletedTask
wouldn't have existed. The existence of the screwdriver suggests the existence of the screw.
Yes, it's correct that wrapping a sync method around an async wrapper does not improve the sync method in any way. It's an added wrapper, for no benefit to the method itself. That part is correct.
But there is a benefit. It allows the consumer of this method to interact with this method as if it were async. Like I said, this doesn't matter to the sync method itself, but it can significantly streamline the process if that consumer has to handle both async and sync methods.
We get back to the same uniformity argument. Does the added cost of the wrapper outweigh the benefit of being able to reusable handle both sync and async methods? My answer is no, the benefit is bigger than the cost.
Case study
As a last example, I want to point out a real scenario where the decision is made for you: the mediator pattern. I know you're not using it (or at least you did not mention it), but it's an interesting case to study here.
The mediator pattern effectively forces your communication to talk to the exact same mediator, so there is only one possible communication channel. This effectively removes the option of having different sync/async paths.
If you look at implementations of this, most notably Mediatr, you'll see that they favor taking an async-only approach, where sync requests are expected to be async-wrapped to ensure that all requests can be handled uniformly.
When you think back to your command factory, this isn't all too different. The only difference is that the command factory doesn't trigger the execution of the command itself (whereas the mediator does execute its request automatically), but it essentially acts as the same streamlined single point of contact to access all available commands.
This strongly suggests that your command factory should take a page out of the same book, and enforce a more reusable and uniform async-only approach, which expects synchronous implementations to confirm to async handling.