The proxy design pattern is widely used in remote communication scenarios, e.g. DCOM or WCF. But it has just as many uses closer to home.
For example, perhaps you need the ability to read and write files in a directory, but you want to be safe. By that I mean you can read whenever you want, but you must fulfill a set of preconditions before you overwrite an existing file — say, it has to be copied/backed up. Then maybe you need a protection proxy:
public interface IDataDirectory { Task<MyData> ReadAsync(string name); Task WriteAsync(string name, MyData data); } public class ProtectedDataDirectory : IDataDirectory { private readonly IDataDirectory innerDirectory; public ProtectedDataDirectory(IDataDirectory innerDirectory) { this.innerDirectory = innerDirectory; } public Task<MyData> ReadAsync(string name) { // pass-through, no special logic here return this.innerDirectory.ReadAsync(name); } public async Task WriteAsync(string name, MyData data) { MyData oldData = await this.innerDirectory.ReadAsync(name); // Back it up first! (we'll assume the inner implementation is smart enough // not to write an empty file if there is no data here) await this.innerDirectory.WriteAsync(name + ".backup", oldData); // Now overwrite by handing off to the inner implementation await this.innerDirectory.WriteAsync(name, data); } }
How about adding a bit of fault tolerance to the file operations? We can use proxy here, too!
public class RetryOnErrorDataDirectory : IDataDirectory { private readonly IDataDirectory innerDirectory; private readonly int maxRetryCount; public RetryOnErrorDataDirectory(IDataDirectory innerDirectory, int maxRetryCount) { this.innerDirectory = innerDirectory; this.maxRetryCount = maxRetryCount; } public async Task ReadAsync(string name) { for (int i = 0; i < this.maxRetryCount; ++i) { try { return await this.innerDirectory.ReadAsync(name); } catch (SomeTransientException e) { // Rethrow only if we fail on the final attempt if (i == this.maxRetryCount - 1) { throw; } } } } // ... }
(The retry algorithm here is pretty naïve but you can integrate it with something a bit smarter such as that discussed in my earlier post.)
The great thing about a proxy is that it looks just like the underlying object it is wrapping. You can pass it around anywhere the base object is expected and it should just work (barring slight behavioral changes like the examples described above). You could even have a proxy of a proxy, and make a nice fluent interface around it:
IDataDirectory original = /* get base object */; IDataDirectory saferAndMoreResilient = original.WithBackupLogic().WithRetries(5); // We can achieve the above with extension methods: public static class DataDirectoryExtensions { public static IDataDirectory WithBackupLogic(this IDataDirectory innerDirectory) { return new ProtectedDataDirectory(innerDirectory); } public static IDataDirectory WithRetries(this IDataDirectory innerDirectory, int maxRetryCount) { return new RetryOnErrorDataDirectory(innerDirectory, maxRetryCount); } }
The next time you see an error-prone operation, see if a protection proxy is appropriate to achieve a “safer by design” outcome.