There are many books dedicated to the subject of Software Design Patterns and the subject is becoming increasingly hard to ignore. Microsoft for instance has dedicated an entire Community dedicated to the subject of patterns and practices. I'm not going to try to hard sell the concept of Pattern based design on you. All I will say is that for a while, I had a somewhat difficult time understanding the real world applicability of using patterns but as I became more and more familiar with them - it's hard to imagine life without them.
There are many different types of Patterns like Data Access, Integration and Testing Software , but this just scarcely scratches the surface. And to be perfectly honest, in the last two jobs I interviewed for, I qot a pretty good amount of questions on patterns, which ones I've used and why I chose them. I'm hearing this more and more.
Anyway, I'm going to focus on Data Access patterns, one in particular named the Retryer. Before I start - I want to point out that I did not write this pattern and in no way claim I did it. Actually, I got it out of Clifton Nock's Data Access Patterns Database Interactions in Object Oriented Applications. The code used in there was done in Java and JDBC - but patterns are ways to solve problems - the implementation will vary from language to language or framework to framework. I've ported it over to C# and did a small integration with Microsoft's Data Access Application Block.
According to the description on page 171 - Retryer should "Automatically retries operations whose failure is expected under certain defined conditions. This pattern enables fault-tolerance for data access operations"
First off - Data Access operations are notably different from many other types of functions in that there is a lot of interdependency among different components and failure of any one piece will cause the system to fail. This is particularly true in Client/Server or N-Tier scenarioes.
For instance, if I have a traditional Client/Server application that queries a database - my code can fail because the database goes down. My code should bow out gracefully - but the show can't go on until we get another connection to the db. This can be mitigated somewhat in more disconnected scenarios - but at some point that data needs to get back into the database or my app won't be worth anything to the users. But let's say the Network goes down. All of a sudden, I can't continue. What if my Database goes down, same thing? Network card? Same. You get the idea. None of this would be considered a bug on my part - but it's still a problem (unless I somehow coded it in a way that caused the problem directly but I think you get the idea).
Now, let's look at a "bad" way of dealing with this. You can a method that calls the .Open method on the dbConnection and if it doens't immediately open, a new connection is created inside a loop until we get a clean open or some interval passes, which happens first. This is probably the worst possible way to handle this situation but one that's very common in practice. Why is it so bad?
You have a limited number of connections out there. So something could be causing the first one to open slowly - maybe it's waiting for an available connection, but since it doesn't respond fast enough - you start piling more stress on it. So does every user out there. Each one of those Connection.Open()'s isn't probably getting a Connection.Close() - that would be an ugly thing to implement considering how they were opened, so in a nutshell, your 'solution' to a timeout is precisely something that WILL in all likelihood cause a whole bunch of other timeouts and exceptions. Hardly a solution right?
In comes patterns to the rescue.
First a refresher course on Interfaces and Inheritance.
Let's say that I have the following Interface:
| using System; using System.Data; namespace InterfacesSample { /// <summary> /// Summary description for IGenericInterface. /// public interface IGenericInterface { System.String ClassName(); System.DateTime AccessTime(); } } --------------- Next, I decide to create three separate classes that are totally different, but all implement this interface: using System; using System.Data; namespace InterfacesSample { public class FirstIGenericClass : IGenericInterface { public FirstIGenericClass(){} #region IGenericInterface Members public String ClassName() { return "FirstIGenericClass"; } public DateTime AccessTime() { return DateTime.Now; } #endregion } } using System; using System.Data; namespace InterfacesSample { public class SecondIGenericClass : IGenericInterface { public SecondIGenericClass(){} #region IGenericInterface Members public String ClassName() { return "SecondIGenericClass"; } public DateTime AccessTime() { return DateTime.Now; } #endregion } } using System; using System.Data; namespace InterfacesSample { public class ThirdIGenericClass : IGenericInterface { public ThirdIGenericClass(){} #region IGenericInterface Members public String ClassName() { return "ThirdIGenericClass"; } public DateTime AccessTime() { return DateTime.Now; } #endregion } } -------------- Now assume that I have the following function: private void DemoInterfaceBehavior(IGenericInterface obj) { rtb.Text += obj.ClassName() + " " + obj.AccessTime().ToString() + "\r\n"; } Would you expect that the following code will work, successfully passing in each of the objects and then outputting the respective information? private void button1_Click(object sender, System.EventArgs e) { FirstIGenericClass cs = new FirstIGenericClass(); SecondIGenericClass cs2 = new SecondIGenericClass(); ThirdIGenericClass cs3 = new ThirdIGenericClass(); this.DemoInterfaceBehavior(cs); this.DemoInterfaceBehavior(cs2); this.DemoInterfaceBehavior(cs3); } |
| using System; using System.Data; namespace Ryan.Patterns.Data.Retryer.Core { /// <summary> /// Summary description for IRetryable. /// public interface IRetryable { #region Public Methods /// <summary> /// Attemps to retry something - once. /// /// <returns> System.Boolean Attempt(); /// <summary> /// Recovers from an operation failure /// void Recover(); #endregion } } Pretty simple - actually, it couldn't be much easier to create. Now for the Retryer class: using System; using System.Data; using System.Threading; using Ryan.Patterns.Data.Retryer.Exceptions; namespace Ryan.Patterns.Data.Retryer.Core { /// <summary> /// Summary description for Retryer. /// public class Retryer { private IRetryable operation; private System.Int32 maxAttempts; private System.Int32 retryInterval; public System.Int32 MaximumAttempts { get { return this.maxAttempts; } set { this.maxAttempts = value; } } public System.Int32 RetryInterval { get { return this.retryInterval; } set { this.retryInterval = value; } } public Retryer(IRetryable operation) { this.operation = operation; } public void Perform(System.Int32 maximumAttempts, System.Int32 attemptInterval){ this.MaximumAttempts = maximumAttempts; this.RetryInterval = attemptInterval; System.Boolean succeeded = false; for(System.Int32 x = 1; (x <= thisMaximumAttempts || this.MaximumAttempts < 0) && !succeeded; x++){<BR> succeeded = this.operation.Attempt(); if(!succeeded){ this.operation.Recover(); Thread.Sleep(this.RetryInterval); } } if(!succeeded){ System.String Message = String.Format("After {0} tries, this operation could " + " not be completed. {1}" , this.MaximumAttempts , DateTime.Now.ToString()); throw new RetryFailedException(Message); } } } } |
using System; using System.Data; using System.Data.SqlClient; using System.Diagnostics; using Microsoft.ApplicationBlocks.Data; using System.Configuration; namespace Ryan.Patterns.Data.Retryer.Core { /// <summary> /// Summary description for Class1. /// public class UpdateHanlder : IRetryable { public UpdateHanlder(){} #region IRetryable Members public void Recover() { // ADD Logging code or send the stuff to another db - whatever you want } public bool Attempt() { // TODO: Add UpdateHanlder.Attempt implementation SqlConnection cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings ["ConnectionString"]); System.String sql = "INSERT INTO TESTTABLE (VEHICLEYEAR) VALUES (@VehicleYear)"; SqlParameter prm = new SqlParameter("@VehicleYear", SqlDbType.Int); prm.Value = 2010; SqlParameter[] Params = {prm}; System.Int32 RecsAffected = SqlHelper.ExecuteNonQuery(cn, CommandType.Text, sql, Params); return (RecsAffected > 0) ? true : false; } #endregion } } |