I've never been completely happy with the way exception handling works, there's a lot exceptions and try/catch brings to the table (stack unwinding, etc.), but it seems to break a lot of the OO model in the process.
Anyway, here's the problem:
Let's say you have some class which wraps or includes networked file IO operations (e.g. reading and writing to some file at some particular UNC path somewhere). For various reasons you don't want those IO operations to fail, so if you detect that they fail you retry them and you keep retrying them until they succeed or you reach a timeout. I already have a convenient RetryTimer class which I can instantiate and use to sleep the current thread between retries and determine when the timeout period has elapsed, etc.
The problem is that you have a bunch of IO operations in several methods of this class, and you need to wrap each of them in try-catch / retry logic.
Here's an example code snippet:
RetryTimer fileIORetryTimer = new RetryTimer(TimeSpan.FromHours(10)); bool success = false; while (!success) { try { // do some file IO which may succeed or fail success = true; } catch (IOException e) { if (fileIORetryTimer.HasExceededRetryTimeout) { throw e; } fileIORetryTimer.SleepUntilNextRetry(); } }
So, how do you avoid duplicating most of this code for every file IO operation throughout the class? My solution was to use anonymous delegate blocks and a single method in the class which executed the delegate block passed to it. This allowed me to do things like this in other methods:
this.RetryFileIO( delegate() { // some code block } );
I like this somewhat, but it leaves a lot to be desired. I'd like to hear how other people would solve this sort of problem.
To avoid the problem of duplicated bugs, never reuse code by copying and pasting existing code fragments. Instead, put it in a method if it is not already in one, so that you can call it the second time that you need it.
If the duplicate code is inside a constructor, use Pull Up Constructor Body. If the duplicate code is similar but not completely identical, use Form Template Method. If two methods do the same thing but use different algorithms, select the best algorithm and apply Substitute Algorithm.
The conventional approach to reduce this kind of code duplication is to move the common code to a member function, which can be called from all the constructors. Usually, that member function is called init.
Duplicate code can be hard to find, especially in a large project. But PMD's Copy/Paste Detector (CPD) can find it for you! CPD works with Java, JSP, C/C++, C#, Go, Kotlin, Ruby, Swift and many more languages. It can be used via command-line, or via an Ant task.
This looks like an excellent opportunity to have a look at Aspect Oriented Programming. Here is a good article on AOP in .NET. The general idea is that you'd extract the cross-functional concern (i.e. Retry for x hours) into a separate class and then you'd annotate any methods that need to modify their behaviour in that way. Here's how it might look (with a nice extension method on Int32)
[RetryFor( 10.Hours() )] public void DeleteArchive() { //.. code to just delete the archive }
Just wondering, what do you feel your method leaves to be desired? You could replace the anonymous delegate with a.. named? delegate, something like
public delegate void IoOperation(params string[] parameters); public void FileDeleteOperation(params string[] fileName) { File.Delete(fileName[0]); } public void FileCopyOperation(params string[] fileNames) { File.Copy(fileNames[0], fileNames[1]); } public void RetryFileIO(IoOperation operation, params string[] parameters) { RetryTimer fileIORetryTimer = new RetryTimer(TimeSpan.FromHours(10)); bool success = false; while (!success) { try { operation(parameters); success = true; } catch (IOException e) { if (fileIORetryTimer.HasExceededRetryTimeout) { throw; } fileIORetryTimer.SleepUntilNextRetry(); } } } public void Foo() { this.RetryFileIO(FileDeleteOperation, "L:\file.to.delete" ); this.RetryFileIO(FileCopyOperation, "L:\file.to.copy.source", "L:\file.to.copy.destination" ); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With