0

Let's say I'm building a simple console app which has three commands:

  1. Create category.
  2. Download recipe from API to category.
  3. Display all recipes in a category.

Assuming the app will grow, I use the Command design pattern to handle console commands. I also use a factory design pattern to create the commands from the user input. Correct me if this is not a good approach so far.

1 and 3 are synchronous commands. 2 is asynchronous. I should be able to initiate multiple downloads before they finish.

So in these models, if I create a Command interface to handle these requests and write something back to the console, how do I do this without violating this "rule"?

The goal would be to have a simple loop that parses the lines, transforms them into Commands and executes them. These methods don't know what they are executing as the logic is hidden in the concrete Commands.

Something alone these lines:

For (...)
{
    ...
    ICommand command = CommandFactory.From(string)

    Task t = command.executeAsync()
    tasks.Add(t)
}

Task.Waitall(tasks)

This works but it feels weird to have a executeAsync even when the command is sync. Although it works by doing Task.CompletedTask

6
  • 1
    Why are 1 and 3 not asynchronous in a “this may yield control back to the CPU” way?
    – Telastyn
    Commented Oct 16, 2020 at 23:42
  • 1
    "Let's say I'm building a simple console app which has four commands:" -- What's the fourth command?
    – Erik Eidt
    Commented Oct 17, 2020 at 0:07
  • I meant 3, sorry. How would I do that @Telastyn? The point is that synchronous commands have to be fully executed before another command begins.
    – Lucas
    Commented Oct 17, 2020 at 0:20
  • @Myntekt - that isn’t what the async keyword does in C#.
    – Telastyn
    Commented Oct 17, 2020 at 0:43
  • An async method will somewhere inside the method 'wait' for something. While it's waiting it gives control to the CPU. A synchronous method never waits for anything so it shouldn't be async. I don't know if there's something I'm missing.
    – Lucas
    Commented Oct 17, 2020 at 0:49

2 Answers 2

0

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:

  1. 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.

  1. 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.

0

I would probably rename executeAsync into execute, and make it return null when the command is a synchronous one. This will allow it to implement the command interpreter along the lines of this:

var tasks = new List<Task>();
while(!commandQueue.IsEmpty())
{
    ICommand command = commandQueue.Next(); // inside: CommandFactory.From(string)
    Task t = command.execute();
    if(t==null)
    {
       Task.WaitAll(tasks.ToArray());
       tasks.Clear();
    }
    else
    {
       tasks.Add(t)
    }
}
Task.Waitall(tasks.ToArray());
tasks.Clear();

This way, asynchronous commands which are follow each other sequentially are executed in parallel until a synchronous command occurs.

If using null is not explicit enough for someones taste, one could alternatively add something like an IsAsync method to the ICommand interface, or use Task.CompletedTask as the return value for a synchronous command, but I prefer to keep things as simple as possible.

1
  • While the fact that commands have no real return value allows you to do this, this feels to me as a dirty fix to a problem that could be tackled more elegantly. Returning null to highlight a different scenario is generally not the most advisable approach, regardless of sync/async or other considerations. Nulls have their purpose but shouldn't act as way to force your consumer to null check to alter the consumer's behavior based on it.
    – Flater
    Commented Nov 17, 2020 at 10:12

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.