jonwear.com

jon wear's personal website

A Case for the Interface

3 January 2013

"I don't get interfaces.  If I need a method in a class I'll put the method in the class.  Why waste time putting the method definition in another file and then write the code I was going to anyway in the class definition?"

I may or may not have said something like this at some point in my software career.  I have certainly heard the usefulness of interfaces questioned over the years.  The questioners are almost always self taught developers, such as myself who have managed to write gobs of decent applications without ever seeing the need for an interface.  The fact that the .Net framework is littered with them ought to be a clue that somebody who was smart enough to build the .Net framework thought they were useful.

But we're not building a new framework, are we?  We're building text over data apps, right?  Who needs 'em?

We all do.  In the words of Fire Marshall Bill, "Let me show you something."

It's my first day on the job/project/team and I've been given a task.  The task is simple:

Build a method that takes in a last name and first name argument and returns a list of customers that start with the name arguments.

Easy!

I write a stored procedure, then I add a Linq To SQL designer, drag the stored procedure onto it and I'm done in four lines of code:



static List<GetCustomersResult> GetCustomers(string lastname,string firstname)
        {
            List<GetCustomersResult> result = new List<GetCustomersResult>();
            lqDataContext lq = new lqDataContext();
            result = lq.GetCustomers(lastname, firstname).ToList();
            return result;
        }

I'm all happy until the person who tells me what to do comes by and looks at it and says, "Oh, well that will work for this department, but if I want to search for new customers, they are in a flat file.  They are there for a week or so until they get processed into the main customer database.  And we also have separate customer databases from companies we bought back in the 90s.  One is in Access, another is in Fire Fox."

"You mean Fox Pro?" I ask.

"Fox Pro, yeah.  Fire Fox is the Internet Explorer, I forgot.  Anyway there are those and maybe some others I'm forgetting about."

I smile, nod and turn back to my four lines of code.  This isn't going to cut it.

Quick Aside: The above exchange was the obligatory "All Managers R Stupid" joke.  Your manager most likely is not stupid, they most likely have a whole host of problems that would keep you up screaming at night.  In fact, it probably keeps them up screaming at night.

So in almost no time at all, this project has grown from one data source to at least three.  As additional meetings happen, there could very well be more.  If I've done my job well, it's entirely possible that other departments will hear about it and want to add their subset of customer records to this process.  There's no telling how many data sources this will ultimately use.  All of them will could have different connection info.

Here's where I can use an interface to make things easier.  Now, if you keep scrolling it may look like this is more complicated, but I'm saving myself a lot of future trouble.  This particular boss has already proven to be a slow roller with the requirements, so an ounce of prevention...

First, I create an interface with one required method:

public interface ICustomerGetter
    {
        List<Customer> GetCustomers(string lastname, string firstname);
    }

Quick Aside: All an interface does is list methods that a class has to have if it's going to implement the interface.  It doesn't contain any of the innards, just how methods must be called and what will be returned.

Second, I create a simple POCO to hold the customer info:


public class Customer
    {
        public Customer() { }
        public Customer(int customerID, string lastname, string firstname, DateTime createdon)
        {
            this.customerID = customerID;
            this.lastname = lastname;
            this.firstntame = firstname;
            this.createdon = createdon;
        }
        public int customerID { get; set; }
        public string lastname { get; set; }
        public string firstntame { get; set; }
        public DateTime createdon { get; set; }
    }

Third I create a SQLGetter Class. It implements the ICustomerGetter interface, and has some extra stuff in it to keep track of query strings and linq to sql type stuff. Since it implements the ICustomerGetter interface, it has to have a method called GetCustomers that takes a string lastname and firstname parameters and returns a typed list of customers. The interface says I have to have it, how I must call it and what it must return. What goes on in the middle is up to me.

public class SQLGetter : ICustomerGetter
    {
        string cn = "";
        public SQLGetter(string ConnectionString)
        {
            cn = ConnectionString;
        }
        public List<Customer> GetCustomers(string lastname, string firstname)
        {
            List<Customer> result = new List<Customer>();
            lqDataContext lq = new lqDataContext(cn);
            lq.Open();
            var dbcurrentcustomers = lq.GetCustomers(lastname, firstname).ToList();
            foreach (var i in dbcurrentcustomers)
            {
                result.Add(new Customer(i.customerID, i.lastname, i.firstname, i.createdon));
            }
            lq.Close();
/* You could remove the above foreach loop and use the following Lamba expression. * It accomplishes the same thing. * * result = dbcurrentcustomers.ConvertAll(i => new Customer(i.customerID, i.lastname, i.firstname, i.createdon)); * */ return result; } }
Fourth, I build my flat text file getter.  I create a new class called FileGetter that implements ICustomerGetter.  This class takes a string path argument when it's instantiated, it implements the GetCustomers method as required and looks through a text file to find matching records.  It looks like this:


public class FileGetter:ICustomerGetter
    {
        private string path;
        public FileGetter(string path)
        {
            this.path = path;
        }
        public List<Customer> GetCustomers(string lastname, string firstname)
        {
            List<Customer> result = new List<Customer>();
            var customers = System.IO.File.ReadAllLines(path);
            foreach (string c in customers)
            {
                string[] custdata = c.Split(',');
                Customer customeritem = new Customer(Convert.ToInt32(custdata[0]), custdata[1], custdata[2], new DateTime());
                if (customeritem.lastname.StartsWith(lastname) && customeritem.firstntame.StartsWith(firstname))
                {
                    result.Add(customeritem);
                }
            }
            return result;
        }
    }

Fifth, I create a method that does the main search.  It looks something like this:


static List<Customer> GetMainCustomers(List<ICustomerGetter> getters, string lastname,string firstname)
        {
            List<Customer> result = new List<Customer>();
            foreach (var g in getters)
            {
                result.AddRange(g.GetCustomers(lastname, firstname));
            }

            /* Below is another lamba expression.  Nothing scary here.
             * It's just returning the result list so that the items are ordered by lastname,firstname.
             * */
            return result.OrderBy(ln => ln.lastname).ThenBy(fn => fn.firstntame).ToList();
        }


This GetMainCustomers method takes three arguments, a list of objects that implement ICustomerGetter, a string for lastname and for firstname.  The cool thing about the first argument is that the getter objects don't have to be the same type.  All they have to do is properly implement the ICustomerGetter interface.  They could have different properties, and all sorts of things but as long as they implement everything in the interface, our GetMainCustomers method is happy.

Now, we loop through each ICustomerGetter object, do the search and return the results.

Lastly, the calling method would look like this:


static void Main(string[] args)
        {
            string cn = Properties.Settings.Default.customerdataConnectionString.ToString();
            SQLGetter sqlgetter = new SQLGetter(cn);
            FileGetter fgetter = new FileGetter(@"c:\temp\customers.txt");

            List<ICustomerGetter> getters = new List<ICustomerGetter>();
            getters.Add(sqlgetter);
            getters.Add(fgetter);

            List<Customer> customers = GetMainCustomers(getters,"B", "D");
        }

Now that we've done all this work, what does it give us?

Well, now we can add new customer getters for Access and FoxPro just by making sure they properly implement the ICustomerGetter interface.  We can create multiple instances of each object if there happen to be more than one source file for Access or the text file.

We can also decide to call the method with just one getter if we only wanted new customers from the text file getter, or if we only wanted current customers from the SQL getter.

We can also easily test this code by building hard coded values to see what would happen under certain circumstances.  Let's say one of the text files doesn't have an ID in the first position, or maybe we want to add rules to make sure that the customerID field is always unique, we could make tests that force the method to deal with items with non-unique IDs.

So there you go, that's why you might want to use an interface.  It helps you plan for the unknown.

By Jon Wear

Related articles