A common example of this occurs when you use constructor dependency injection. For example, if you have a Foo class that takes a Bar as a constructor argument.
class Foo...In the above example Foo cannot be tested without creating an instance of the Bar class. However, the dependency could be mocked or stubbed (depending upon intent) if Foo instead depended upon an interface.
public Foo(Bar bar)
{
this.bar = bar;
}
..
}
class Foo...Unfortunately, this approach is often met with criticism.
public Foo(IBar bar)
{
this.bar = bar;
}
..
}
The common complaints include.
- Complaint: Creating extra code to support testing. Response: Code written to support testing should be minimized. However, any code that allows for better testing is well worth adding. It will need to be maintained, but this level of effort should be far less than the level of effort required when working with detestable code.
- Complaint: The overhead of maintaining the class and the interface. Response: Static languages are great at alerting you when your class/interface relationship has become out of synch. Additionally, refactoring tools such as ReShaper ensure that little effort will be necessary to maintain a good relationship.
- Easy mocking or stubbing. Interfaces dependencies allow you to create mocks or stubs that behave specifically as your test requires. This improves the durability of the tests you write.
- Truly unit testable classes. Classes that have constructor arguments that are not interfaces require instances of concrete types with defined behavior. When this dependency exists any change to the behavior of a dependency can cause false test failures. Classes that have interfaces as dependencies can have their behavior tested in isolation.
- Using other types (as long as they implement the interface) as dependencies. While developing applications, requirements change. Using the example above, today it may appear that Foo will always depend on Bar. However, in the future it may turn out that Foo depends on BarNorth and BarSouth. If BarNorth and BarSouth are nothing alike they could share a base class, but will more likely simply implement the same interface. When this occurs, if Foo depends on Bar, the constructor (and likely every place that uses type Bar) will need to be changed to use the interface instead. If an interface were used from the beginning, no changes to the existing codebase would be required.
Actually using an interface in the first place is considered good design by many developers (myself included) and testing be damned for adding it to the design. Honestly the high decoupling that the interface gives you in static languages is generally considered desired
ReplyDeleteI agree that using an interface in the first place is good design. GOF seemed to popularize the idea "to program to an interface, not an implementation." Unfortunately, I don't see this guideline followed as often as I'd prefer.
ReplyDeletePerhaps my entry is better stated as "testing purposes" is another reason in favor of programming to an interface, not the main reason to program to an interface.
I can see why people complain if you give your interfaces such bad names.
ReplyDeleteA common code smell is classes and interfaces with the same name, but with some naming wart to avoid name clash. E.g. Foo and IFoo, or FooImpl and Foo. An interface is not a "header file" for a class; it represents a relationship between objects. What is that relationship? That's what the interface should be named after. What does an object do, and how? That's what a class should be named after.