1
\$\begingroup\$

We have three methods, the first calls the second one. How would you refactor so the third one can call the second one too as they have similar code?

public async Task UnpublishPersonAsync(string id)
{
    await QueueAndUpdateStatusAsync(id, PersonStatus.Unpublishing, QueueNames.Unpublish);
}


private async Task QueueAndUpdateStatusAsync(string id, PersonStatus status, string queueName)
{
    var personDto = await _cosmosDbService.GetItemAsync<PersonDto>(id, id);
    var operationIsValid = ValidateOperation(status, personDto);

    if (operationIsValid)
    {
        personDto.Status = status;
        personDto = await _cosmosDbService.UpdateItemAsync(id, id, personDto);
        var person = _mappingService.MapToPerson(personDto);

        await _queue.PublishToStorageQueueAsync(person, queueName);
    }
    else
    {
        throw new InvalidOperationException();
    }
}

public async Task DeletePersonAsync(string id, bool deleteAssociatedData)
{
    var personDto = await _cosmosDbService.GetItemAsync<PersonDto>(id, id);
    var operationIsValid = ValidateOperation(PersonStatus.Deleting, personDto);

    if (operationIsValid)
    {
        personDto.Status = PersonStatus.Deleting;
        personDto = await _cosmosDbService.UpdateItemAsync(id, id, personDto);
        var person = _mappingService.MapToPerson(personDto);

        var deleteCommand = new DeletePersonCommand()
        {
            Person = person,
            DeleteAssociatedData = deleteAssociatedData
        };

        await _queue.PublishToStorageQueueAsync(deleteCommand, QueueNames.Delete);
    }
    else
    {
        throw new InvalidOperationException();
    }
}

I thought about using generics and make it optional, but it seems that's not possible. By the way, PublishToStorageQueueAsync is already generic for the first argument.

\$\endgroup\$
4
  • 3
    \$\begingroup\$ The current question title, which states your concerns about the code, is too general to be useful here. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How to get the best value out of Code Review: Asking Questions for guidance on writing good question titles. \$\endgroup\$ Commented Jul 27, 2020 at 23:47
  • \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ I'm submitting the code for review as I think there are usually others with better ideas. As you can see, there is nothing particular about these methods or what the code does, the problem is that I don't have enough experience nor knowledge to improve it, but enough to feel there should be a way somehow. I'm starting with code improvement as making code work was (and still is) a huge milestone for me, as I guess for everyone at some point. If you want to throw some ideas, you are welcome people, if not, don't worry :) \$\endgroup\$ Commented Jul 28, 2020 at 0:44
  • 1
    \$\begingroup\$ That doesn't address the issue with the title. \$\endgroup\$ Commented Jul 28, 2020 at 11:18
  • 1
    \$\begingroup\$ Please state the purpose of the program. \$\endgroup\$ Commented Jul 28, 2020 at 11:19

1 Answer 1

4
\$\begingroup\$

Disclaimer


From the comments:

the problem is that I don't have enough experience nor knowledge to improve it

I will stop you right there. If you don't have enough experience nor knowledge then I suggest you don't do refactoring on this specific code, not because you can't but because you endanger yourself of making undesired behaviour or making actually harder to read code.

The sole purpose of refactoring is making a code easier to understand. Now this can be tricky because sometimes some sections of your application have to be very performant, other sections need to be very readable as most of the business logic is there, some parts need to be very flexible since they are changing often there are many cases. The key to refactoring is first realizing what are you refactoring for - performance (very hard, if you don't have experience I would suggest not to do it), readability, extensibility and so on.

Back to your case


Playing with tasks if you are inexperienced often leads to some code running either slower or harder to reason about.

First (and probably the last one given the fact I don't know the business logic) change I would do to this code is postponing Task's awaiter.

// instead of this
public async Task UnpublishPersonAsync(string id)
{
    await QueueAndUpdateStatusAsync(id, PersonStatus.Unpublishing, QueueNames.Unpublish);
}

// strive to do this
public Task UnpublishPersonAsync(string id)
{
    return QueueAndUpdateStatusAsync(id, PersonStatus.Unpublishing, QueueNames.Unpublish);
}

// or arrow notation if your team likes it
public Task UnpublishPersonAsync(string id)
    => QueueAndUpdateStatusAsync(id, PersonStatus.Unpublishing, QueueNames.Unpublish);

Why is this?

I am always trying to explain the guys in the team that for a change that is so insignificant can give us little tiny bit of improvement. What I mean is that

  1. it does not create async state machine class for your method

  2. it does not introduce unnecessary code that has to be run (goto statements) really really small improvement but if the change is so little.

  3. it leaves the consumer of your method to decide when he wants to await or if he wants to await.

Both methods(QueueAndUpdateStatusAsync and DeletePersonAsync) look the same, alas, they use different overloads of PublishToStorageQueueAsync methods and it will make the code harder to read if you try to abstract them.

What I don't like is throwing exceptions - I use them as a last resort really really last resort. In your case I will refactor both of the methods to follow the same pattern (not that they don't now) like so:

public async Task<bool> DeletePersonAsync(string id, bool deleteAssociatedData)
{
    var personDto = await _cosmosDbService.GetItemAsync<PersonDto>(id, id);
    var operationIsValid = ValidateOperation(PersonStatus.Deleting, personDto);

    // personal preference you can do it with brackets and newlines
    if (!operationIsValid) return false; 
    
    personDto.Status = PersonStatus.Deleting;
    personDto = await _cosmosDbService.UpdateItemAsync(id, id, personDto);
    var person = _mappingService.MapToPerson(personDto);

    var deleteCommand = new DeletePersonCommand()
    {
        Person = person,
        DeleteAssociatedData = deleteAssociatedData
    };

    await _queue.PublishToStorageQueueAsync(deleteCommand, QueueNames.Delete);
    return true;
}

As you can see now you return if the whole transaction is successful or not simply by changing return type to Task<bool> let consumers handle errors the way they want don't force them to handle your exception especially for something so easy to fix.

If you have decided to go with exceptions nevertheless, then I strongly suggest you encode some information inside it. Say some message like "Validation of X failed." anything that can help you give meaning to it later when you happen to debug it. Also if this is some kind of library code, creating custom specific exception is also an option if you like exceptions so much, but as a rule of thumb I strongly suggest you to stay away from exceptions at all cost no matter the language or framework.

P.S. They also incur performance drawback for unwinding the stack later. Your clients need to catch it and probably re-throw it also if necessary, overall it gets messy IMO.

Conclusion


Other than that I think its relatively easy code to follow, don't refactor it unless you find problems with it - whether that will be performance problems, readability problems and etc.

\$\endgroup\$
8
  • \$\begingroup\$ Thanks for your feedback, really appreciated. Specially returning the task, instead of awaiting it. Regarding PublishToStorageQueueAsync, I made it generic (not an overload) and shamely the other code I posted was also made by me. The similar methods have almost duplicated code, you are saying they shouldn't be merged. I'm wondering if there is an alternative though. \$\endgroup\$ Commented Jul 28, 2020 at 13:54
  • \$\begingroup\$ They can be merged but then you are loosing the fluency - if I were you I would make one generic private method and many public fluent methods. "Duplicating" code is not always a bad thing. \$\endgroup\$ Commented Jul 28, 2020 at 14:23
  • \$\begingroup\$ I got interested in the can be merged part, to analyze that approach, how would you do it? \$\endgroup\$ Commented Jul 28, 2020 at 14:28
  • \$\begingroup\$ Well you can make methods that you want to merge accept function which will be passed later and keep validation and exception handling in the merged method, but this will only make it less readable IMO thats why I havent suggested it \$\endgroup\$ Commented Jul 28, 2020 at 14:32
  • 1
    \$\begingroup\$ Thanks for this reference @Peter usually I am going for performance always, but its always nice to see guidelines from such a good software engineer :) \$\endgroup\$ Commented Jul 28, 2020 at 15:04

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.