Monday, January 21, 2008

Design to Interfaces

This is the 2nd article in the Design Principles to Live By series.


An interface is--for the purposes of this post at least--an abstract definition of the functionality of an object.  It is the signature of a class divorced from its implementation.  Think of it as the list of methods (and properties) that define a class.  Concrete examples are COM interfaces, Interfaces in C# or Java, or even just classes with at least one (but preferably all) pure virtual function in C++.  An interface need not be structural.  In late-binding languages like Smalltalk, it can just be an agreed-upon list of methods that all classes of a certain type will implement.  In the simplest form, it can just be a list of function prototypes in a C .h file.  The important part of an interface is that it abstractly describes a unit of functionality.  It forms the contract between the class implementing it and the class calling that implementation. 


The use of an interface allows client software to be written without a reliance upon or even knowledge of the underlying implementation.  This yields two primary advantages.  First, it allows for parallel development.  Second, it provides for more flexibility in the design.


If a project is being worked on by more than one programmer, interfaces are essential.  The work between multiple coders is usually partitioned into discrete chunks and each person is responsible for one or more of these.  Each of these units will have to interact with the other units in some way.  The wrong way to code is to wait for the other person to finish their work before beginning yours.  If Programmer A is writing some functionality that Programmer B needs, B will have to wait for A to have at least a prototype ready before he can being his work.  However, if programmer A designs an interface, he has created a contract for how his code will be interacted with.  B can then immediately begin work.  The two don't need to coordinate as closely and B is not gates on the work of A.  When they combine their work, it will--in theory-- just work.  Of course, in practice there will be some kins to work out but they should be minimal.


Let's take an example from my RoboRally project of last summer.  There were four of us on the project.  We were scattered across the country and couldn't closely collaborate.  We partitioned the work into four parts and defined the interfaces that formed the seams between our work.  I was coding the game logic.  I was responsible for tracking robot locations, moving pieces around, and calculating the interactions between the robots.  Another person was responsible for the UI that allowed players to choose their moves.  We agreed upon an interface for the players.  They would have a list of cards, and a robot.  We agreed upon the interface for giving a list of robots to my game engine and for the UI to tell me to process a turn.  With that, I was free to write my game engine code without having to worry about how the UI worked.  The UI programmer was also freed up to write the turn-selection logic without needed to know how I would later process the turns.  All he needed to do was to package the data into the agreed-upon format and I would do the rest.


The second advantage of using interfaces is the flexibility they provide.  This advantage is accrued even on a single-coder project.  Because the various functional units in the program are written to interfaces and not to concrete classes or implementations, the details of the implementation can be varied without the client code having to change.  This is the impetus behind the "encapsulate what varies" principle.  It is impossible to predict when the need will arise to change a portion of the code.  The more you use interfaces, the more easily the program will be modified.  This is especially helpful if the variation turns out to be an addition instead of a replacement.  Changing one image encoder for another can be done by changing the implementation of a concrete class, CJpegEncoder.  Clients don't need to know anything changed.  However, if client code is creating the image encoder class directly and statically linking to its functions, adding a second option for image encoding becomes hard.  Each place where the client creates and interacts with the encoder needs to be modified.  If, instead, the client code only uses IImageEncoder, the code doesn't need to care if it is interacting with CJpegEncoder or CPngEncoder.  It makes the same calls.


Another example.  We have tests dating back to the time I began at Microsoft 10 years ago.  A few years back a new testing system was rolled out across the team and our tests needed to conform to it.  The primary thing that needed to change was the logging.  Prior to this testing system, logging was a haphazard affair.  Logs were consumed by humans so as long as the text clearly delineated a pass and a failure, everything was fine.  The new system had a centralized reporting system including an automated log-parsing system.  This required revamping the way each of our tests logged. 


We had 3 main classes of tests.  There was one class which used a common test harness.  This harness could take dlls containing a logger.  The interface to the dll was standardized.  With one new dll we were able to convert a whole class of applications.  No code in the test applications needed to be modified.  The second class did their own logging, but each contained a function, Log(string), that did the logging.  Each of these applications had to be touched, but the modifications were simple.  The Log function was replaced by one that called into the new logging system.  The modifications were quick.  The third class of tests was the minority of our tests, but these were written without any concept of interfaces.  Each did logging in its own way and usually by passing around a file handle which the various test functions used fprintf to log to.  This worked fine, at the time, but the implementation was tightly bound to the client code.  Each line that logged had to be modified.  This took months.  The moral of the story:  Use interfaces.  Even where you don't need them yet.

7 comments:

  1. While I agree that interfaces are indeed useful for data classes, I think your moral is far too simplistic.
    Interfaces have accessibility implications: to be useful within an accessibility scope, they must be implementable by anyone in that scope.  But sometimes you need to limit implementations, or you need to require certain implementations -- perhaps orchestrating event sequences, which you would put in a base class -- without allowing instantiation of the bast type.  
    In those cases, you shouldn't use interface.  You are more likely to need, instead, an abstract class, and possibly a few well-considered sealeds.

    ReplyDelete
  2. Good point.  There are times when you need to disregard this principle.  As long as you have a good reason for doing so, you are okay.  Principles should be generally but not strictly held to.  Thanks for pointing out a place where it is a bad idea.  Knowing where not to utilize it can be as important as knowing where to use it.
    I would point out, however, that "interface" is not coequal with the C# (or Java) notion of interface.  An abstract class is still an interface in the sense of this article.

    ReplyDelete
  3. I'd considered that you were talking abstractly (sic) about interfaces, but I also considered that "interface" has a fairly specific definition in mainstream practice.
    I know it took me a little while to grok why we had abstract classes at all, when we had interfaces.  Figured it was worth saving some newbies the confusion.

    ReplyDelete
  4. I'll bite.  Why /do/ we have both abstract classes and interfaces?

    ReplyDelete
  5. My guess:  Because C# doesn't have multiple inheritance.  You can inherit from any number of interfaces but just one base class (concrete or abstract).  That way you avoid most of the messiness of multiple inheritance but gain many of the benefits.

    ReplyDelete
  6. Actually, one significant difference is that you can version classes -- you can't version interfaces.  
    By version, I of course mean the addition of members.  Do that with an abstract class and (assuming no subtypes conflict in name), things still compile.  Do that with an interface, and *every* subtype breaks.
    Kiril in C#-land actually disussed this a few months ago.  I'd forgotten that this whole thing had an entry in the Framework Design Guidelines:
    http://kirillosenkov.blogspot.com/2007/08/choosing-interface-vs-abstract-class.html
    The FDG entry: http://msdn2.microsoft.com/en-us/library/ms229019.aspx

    ReplyDelete
  7. Object-oriented design and design patterns can seem complex. There are a lot of ideas and cases to consider.

    ReplyDelete