More DI: Adding Retry with the Decorator Pattern
The Decorator Pattern lets us add functionality without changing existing classes. When I first saw it in action, I started thinking about different possibilities where it could be useful in my code. It’s time to take a closer look.
In this article, we’ll look at adding retry functionality to an application. When we call a service, sometimes there are hiccups in the network or on the server, and the service does not respond. If this happens, we want to wait a few seconds and try again. That’s the code we’ll see here.
The Decorator Pattern lets us add functionality without changing existing classes.
This article is one of a series about dependency injection. The articles can be found here:More DI and the code is available on GitHub: https://github.com/jeremybytes/di-decorators .
The Decorator Pattern
We’ll add the functionality with a Decorator. Here’s the description from Gang of Four Design Patterns (Gamma, et al. Design Patterns: Elements of Reusable Object-Oriented Software . Addison-Wesley, 1995.):
“Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”
Since we’re using an interface, this means that we take an existing implementation of our interface (such as our Service Reader) and wrap it in another class that adds the retry functionality (our Retry Reader). That new class also exposes the same interface, so it plugs into our application just like any other interface.
The previous article describes the interface that we’re using as well as the the code for the Service Reader — More DI: Async Interfaces .
Behavior without Retry
To understand the pattern better, we’ll look at the existing functionality of the application and then add the retry functionality.
Compose the Objects
Here’s the composition root for the application in the “PeopleViewer” project, App.xaml.cs file . (Note: this much simpler than the code for this method in the GitHub repository since that code includes all the decorators we’ll eventually explore. To follow along, just replace the “ComposeObjects” method with this code.)
Starting with the last line, we create the MainWindow of the application. The MainWindow constructor needs a view model, so just above that we create a PeopleReaderViewModel. The view model constructor needs an IPersonReader, so just above that we create a ServiceReader that will get data from a web service.
This is where we are snapping together our loosely couple pieces, and it is constructor injection in action. If we want to use the CSVReader or SQLReader, we just need to swap them in here.
Start the Service
The service that provides the data is in the People.Service project . This is a self-hosted ASP.NET MVC Core WebAPI service (that’s quite a mouthful). The easiest way to start the service is from the command line. (Theprevious article describes the shortcut I use to get the correct folder.)
From a prompt at the project folder location, just type “dotnet run” to start the service.
When the service is running, we see “Now listening on…” in the output. Also note that there is no prompt since the service is running.
Run the Application
When we run the application, and click the “RefreshPeople” button, we see the expected output.
Everything is working; now let’s break it.
Stopping the Service
To stop the service, just go back to the command window and press “Ctrl-C” to stop the service. When it’s stopped, there should be a prompt.
Run the Application Again
Since the service is no longer running, if we click “Clear People” and then “Refresh People”, we get an error. (Note: if you’re running in the debugger, just click “Continue” (or press F5) to keep running past the exception.)
The application has a global exception handler (set up in the App.xaml.cs file ) that gives us this popup message.
If we restart the service, and try again, then things are back to working.
Retrying the Service Call
We could add retry functionality to the ServiceReader class itself, or create a subclass such as a RetryServiceReader. But if we use the decorator pattern, we can add the functionality without needing to modify the existing classes. In addition, it will work with *any* IPersonReader implementation.
Let’s look at the code. This is in the PersonReader.Decorators project, RetryReader.cs file .
We’ll look at the “GetPeople” method in a moment (this is where we have the retry functionality). First, let’s understand how this decorator works at a higher level.
The first thing to notice is that this class implements the “IPersonReader” interface. This means that our class has the interface members “GetPeople” and “GetPerson”. And this is the same interface implemented by the ServiceReader (and our other data readers). And it is the interface that the PeopleReaderViewModel needs. Since RetryReader implements the IPersonReader interface, it can plug into our view model just like any other reader.
Now take a look at the constructor. This takes an “IPersonReader” as a parameter. This parameter is the “real” data reader (which will be our Service Reader in this case). This is stored in a private field that we can use in the rest of the class.
So the basic idea is that we wrap an implementation of the interface (such as a Service Reader). When calls are made to the interface members (such as “GetPeople”), we can add our own functionality before (or after) passing the call through to the wrapped reader.
The Retry Functionality
At its core, we want to pass functionality through to the wrapped reader’s “GetPeople” method. If it works, then we don’t need to do anything else. But if it fails, we want to wait 3 seconds and try again, up to a total of 3 tries.
There are several ways to implement this retry functionality. I opted to go with a recursive call. This may or may not be the best approach depending on the situation. Here is the code for the “GetPeople” method:
Walking through this code, first we increment our retry count. This is a class-level field that starts at “0”. I opted to increment this count first due to some complications with putting it in other places in the try/catch block.
In the “try” block we call the “GetPeople” method on the wrapped reader. If this is successful (i.e., no exceptions), then the retry count is reset and the data is returned.
If the “GetPeople” method on the wrapped reader throws an exception, we hit the “catch” block. If the retry count has hit our limit (3 total tries), we throw the exception and leave it up to the upstream code to handle it.
If we haven’t hit the retry limit yet, then we wait for 3 seconds. When we “await Task.Delay(3000)”, it will pause this method for 3 seconds (similar to Thread.Sleep(3000)), but it does *not* block the current thread.
After waiting 3 seconds, we call the “GetPeople” method on the Retry Reader again. This is the recursive call.
The end result is that the RetryReader will try calling the wrapped reader 3 times. If it succeeds, the data is returned. If it fails 3 times, then it rethrows the exception.
The “GetPerson” method has a similar implementation. You can see the code for details.
We could adjust the number of retries and the delay between retries with parameters on the class. We won’t do that here, but we’ll explore decorator configuration when we look at our other decorators.
Using the Retry Decorator
The great thing about using the decorator pattern is that we do not need to change any of our existing objects in order to add this functionality. All we need to do is snap our pieces together in a different way when we compose the objects.
Let’s go back to the App.xaml.cs file and add the decorator:
Again, we’ll start from the bottom. When we create the MainWindow, we need a view model. When we create the view model (PeopleReaderViewModel), we need an IPersonReader.
Instead of giving the ServiceReader directly to the view model, we give it a RetryReader. Since the RetryReader implements IPersonReader, the view model doesn’t care. It sees it as any other data reader.
The RetryReader needs an IPersonReader to wrap, so we create the ServiceReader and pass it to the retry reader.
Running the Application (Success)
To test the application, we’ll check the behavior that we had before. So first, we’ll start the service (using “dotnet run” from the command line) and make sure the service is running.
Then when we click the “Refresh People” button, we get our data:
So we haven’t changed the success state. That’s good.
Running the Application (Failure)
Next, we’ll stop the service (with Ctrl-C on the command line) and try again.
When we click “Clear Data” and “Refresh People” we get a bit of a delay (6 seconds, in fact). After the delay, we get the same exception behavior as above.
So we changed the behavior a little bit here. We get the same error, but we had to wait longer to get it. And that’s because our retry functionality is running. So let’s see if the application can actually recover.
Running the Application (Retry)
This time, we’ll leave the service stopped. Then we click the “Clear People” and “Refresh People” and wait a couple of seconds.
Then we flip over to the command line and start the service (using the up arrow arrow on the keyboard is a really easy way to get “dotnet run” at the prompt, then just press Enter). If the service is started quickly enough (before the 6 seconds has elapsed), then we get our data in the application.
So this shows that our retry operation is working. Our application recovers from a temporary blip in the service availability.
Dependency Injection and the Decorator Pattern
Using the decorator pattern with dependency injection is very powerful. We were able to add functionality with just a little bit of effort. We created our retry decorator and then snapped our pieces together in a different order.
We did *not* need to change the view (MainWindow). We did *not* need to change the view model (PeopleReaderViewModel). We did *not* need to change the existing data reader (ServiceReader). We just snap our pieces together in a different order.
And because the decorator is dealing with an interface, it will work with *any* of the data readers that implement IPersonReader, including the CSVReader and SQLReader.
When I first saw this pattern used this way, I started thinking about a lot of possibilities. We can add authorization code through a decorator. We can add logging code through a decorator. We can add caching code through a decorator.
The decorator we used here is pretty specific: it is limited to “IPersonReader” implementers. But we could also make some more general-purpose interfaces so that we can share decorator functionality with a variety of different objects. We will take a look at that in a future article.
The next several articles will explore the other decorators that we have in the project, including a caching decorator that adds a client-side cache as well as an exception logging decorator that logs exceptions that happen in our reader calls.
Before getting to the other decorators, we’ll take a quick detour into unit testing. We’ll see how to unit test the RetryReader that we have here, including how to fake up a broken reader and how to unit test asynchronous methods. There’s still a lot more to explore.
原文 : Jeremy Bytes