- Types and Namespaces
- Choosing Between Class and Struct
- Choosing Between Class and Interface
- Abstract Class Design
- Static Class Design
- Interface Design
- Struct Design
- Enum Design
- Nested Types
- Summary
4.3 Choosing Between Class and Interface
In general, classes are the preferred construct for exposing abstractions.
The main drawback of interfaces is that they are much less flexible than classes when it comes to allowing for evolution of APIs. Once you ship an interface, the set of its members is fixed forever. Any additions to the interface would break existing types implementing the interface.
A class offers much more flexibility. You can add members to classes that have already shipped. As long as the method is not abstract (i.e., as long as you provide a default implementation of the method), any existing derived classes continue to function unchanged.
Let's illustrate the concept with a real example from the .NET Framework. The System.IO.Stream abstract class shipped in version 1.0 of the framework without any support for timing out pending I/O operations. In version 2.0, several members were added to Stream to allow subclasses to support timeout-related operations, even when accessed through their base class APIs.
public abstract class Stream { public virtual bool CanTimeout { get { return false; } } public virtual int ReadTimeout{ get{ throw new NotSupportedException( ); { set { throw new NotSupportedException( ); } } } public class FileStream : Stream { public override bool CanTimeout { get { return true; } } public override int ReadTimeout{ get{ { set { } } }
The only way to evolve interface-based APIs is to add a new interface with the additional members. This might seem like a good option, but it suffers from several problems. Let's illustrate this on a hypothetical IStream interface. Let's assume we had shipped the following APIs in version 1.0 of the Framework.
public interface IStream { } public class FileStream : IStream { }
If we wanted to add support for timeouts to streams in version 2.0, we would have to do something like the following:
public interface ITimeoutEnabledStream : IStream { int ReadTimeout{ get; set; } } public class FileStream : ITimeoutEnabledStream { public int ReadTimeout{ get{ { set { } } }
But now we would have a problem with all the existing APIs that consume and return IStream. For example StreamReader has several constructor overloads and a property typed as Stream.
public class StreamReader { public StreamReader(IStream stream){ } public IStream BaseStream { get { } } }
How would we add support for ITimeoutEnabledStream to StreamReader? We would have several options, each with substantial development cost and usability issues:
Leave the StreamReader as is, and ask users who want to access the timeout-related APIs on the instance returned from BaseStream property to use a dynamic cast and query for the ITimeoutEnabledStream interface.
StreamReader reader = GetSomeReader(); ITimeoutEnabledStream stream = reader.BaseStream as ITimeoutEnabledStream; if(stream != null){ stream.ReadTimeout = 100; }
This option unfortunately does not perform well in usability studies. The fact that some streams can now support the new operations is not immediately visible to the users of StreamReader APIs. Also, some developers have difficulties understanding and using dynamic casts.
Add a new property to StreamReader that would return ITimeoutEnabledStream if one was passed to the constructor or null if IStream was passed.
StreamReader reader = GetSomeReader(); ITimeoutEnabledStream stream = reader.TimeoutEnabledBaseStream; if(stream!= null){ stream.ReadTimeout = 100; }
Such APIs are only marginally better in terms of usability. It's really not obvious to the user that the TimeoutEnabledBaseStream property getter might return null, which results in confusing and often unexpected NullReferenceExceptions.
Add a new type called TimeoutEnabledStreamReader that would take ITimeoutEnabledStream parameters to the constructor overloads and return ITimeoutEnabledStream from the BaseStream property. The problem with this approach is that every additional type in the framework adds complexity for the users. What's worse, the solution usually creates more problems like the one it is trying to solve. StreamReader itself is used in other APIs. These other APIs will now need new versions that can operate on the new TimeoutEnabledStreamReader.
The Framework streaming APIs are based on an abstract class. This allowed for an addition of timeout functionality in version 2.0 of the Framework. The addition is straightforward, discoverable, and had little impact on other parts of the framework.
StreamReader reader = GetSomeReader(); if(reader.BaseStream.CanTimeout){ reader.BaseStream.ReadTimeout = 100; }
One of the most common arguments in favor of interfaces is that they allow separating contract from the implementation. However, the argument incorrectly assumes that you cannot separate contracts from implementation using classes. Abstract classes residing in a separate assembly from their concrete implementations are a great way to achieve such separation. For example, the contract of IList<T> says that when an item is added to a collection, the Count property is incremented by one. Such a simple contract can be expressed and, what's more important, locked for all subtypes, using the following abstract class:
public abstract class CollectionContract<T> : IList<T> { public void Add(T item){ AddCore(item); this.count++; } public int Count { get { return this.count; } } protected abstract void AddCore(T item); private int count; }
COM exposed APIs exclusively through interfaces, but you should not assume that COM did this because interfaces were superior. COM did it because COM is an interface standard that was intended to be supported on many execution environments. CLR is an execution standard and it provides a great benefit for libraries that rely on portable implementation.
For example, System.IDisposable and System.ICloneable are both interfaces so types, like System.Drawing.Image, can be both disposable, cloneable, and still inherit from System.MarshalByRefObject class.
public class Image : MarshalByRefObject, IDisposable, ICloneable { }