Freigeben über


Design Guidelines Update: Optional Features Pattern

This is a relatively major update to the Design Guidelines. It describes and formalizes a very common .NET Framework Pattern/Idiom for exposing optional functionality/service of an API (for example canceling a stream operation, as not all streams support cancellation).

Optional Features

In designing your class library, you may find cases where some implementations of your base classes can provide a feature or a behavior, while other implementations cannot. Instead of providing several different base classes each customized to the set of features, you can simply identify these optional features and provide consumers a mechanism to discover whether the particular instance they have access to supports that feature. Creating a large number of similar classes will simply confuse users of your class library and make it more difficult to find the class that they should use.

One example of this is System.IO.Stream. There is great benefit in having a single Stream class that encapsulates Read, Write and Seek methods, however not all instances of the Stream class can provide all those capabilities. What if the stream is coming from a read-only source? What if it is the response to a web request? What if it is built upon a file? The Stream concept remains the same yet set of features is different depending upon the source/destination of the stream.

When not all implementations or instances of a class can support all the features or behaviors, it is best to implement them as optional features and provide an appropriate mechanism for unfamiliar users of your class library to discover whether the optional features are supported by the instance they have.

* It is recommended that you use a class or abstract class instead of an interface. See section 4.1.5.

* Do use simple Boolean query properties that clients can use to determine whether a feature or behavior is supported. For example, consider the following object model.

public abstract class Command {

    public virtual bool CanExecutePageReader { get { return false; } }

    public virtual DataReader ExecutePageReader(int startRecord, int count) {

        throw new NotSupportedException();

    }

    public abstract DataReader ExecuteReader();

    }

public class MyCommand : Command {

    public override bool CanExecutePageReader { get { return true; } }

    public override DataReader ExecutePageReader(int startRecord, int count) {

        …

    }

    public override DataReader ExecuteReader() {

        …

    }

}

Consumer’s code that uses the abstract base class can query this property at runtime to determine the whether they can use the optional feature or behavior or must provide their own alternative:

Command cmd = GetCommandFromSomewhere();

if (cmd.CanExecutePageReader){

    DataReader rdr = cmd.ExecutePageReader(100,10);

}

else {

    DataReader rdr = cmd.ExecuteReader();

}

* Do use virtual methods on the base class that throw NotSupportedException to define optional features and behaviors.  This provides clear visibility to the consumer of the base class but allows for implementers of the class to provide optional features and behavior when it is feasible.  Throwing the NotSupportedException ensures that the consumer will get an exception distinct from some other invalid operation that may have occurred. 

* Do not simply define interfaces for optional behavior and not hook them up to your abstract class in some way.

interface IPagedCommand {

    DataReader ExecutePageReader(int startRecord, int maxRecords);

}

public abstract class Command {

    public abstract DataReader ExecuteReader();

}

public class MyCommand : Command, IPagedCommand {

}

While this allows you to fully utilize and depend upon the type system to provide optional feature discovery, consumers that are unfamiliar with your class library and that are working with your abstract classes will not know that the object they’re working with is a MyCommand that supports the IPagedCommand optional behavior, because the abstract Command object does not have any indication that there is optional behavior, and will not know to utilize it unless they search through your object model. Consider the following code that a user of your class library would implement.

Command cmd = GetCommandFromSomewhere();

if (cmd is IPagedCommand){

    DataReader rdr = ((IPagedCommand)cmd).ExecutePageReader(100,10);

}

else {

    DataReader rdr = cmd.ExecuteReader();

}

How will the user unfamiliar to your class library know to check for IPagedCommand? Unless you expect the optional feature to be rarely implemented, you should utilize the Boolean property getters on the base class described above to provide the unfamiliar user a hint that there is an optional feature available.

Note: if your expectations are that only a very small percentage of classes deriving from the base class or interface would actually implement the optional feature or behavior, then this may actually be the best pattern.  There is no real need to add additional object model to all derived classes when only one of them provides the feature or behavior.

* Consider using an interface or an abstract class, in addition to the simple Boolean query properties described above, to encapsulate optional features that require several properties, events or methods, adding a GetXxx method to your base class that throws NotSupportedException, and overriding the GetXxx method in the derived classes where it is supported. This approach should be avoided in high-level APIs.

public interface IAsyncCommand {

    public IAsyncResult BeginExecuteReader();

    public IAsyncResult BeginExecuteReader(AsyncCallback callback, object state);

    public DataReader EndExecuteReader(IAsyncResult asyncResult);

}

public abstract class Command {

    public virtual bool CanGetAsyncCommand { get { return false; } }

    public virtual IAsyncCommand GetAsyncCommand() {

        throw new NotSupportedException();

    }

    public abstract DataReader ExecuteReader();

}

public class MyCommand : Command, IAsyncCommand {

    public override bool CanGetAsyncCommand { get { return true; } }

    public override IAsyncCommand GetAsyncCommand () {

        return this;

    }

    public IAsyncResult BeginExecuteReader() {

        return BeginExecuteReader(null, null);

    }

    public IAsyncResult BeginExecuteReader(AsyncCallback callback, object state) {

        …

    }

    public DataReader EndExecuteReader(IAsyncResult asyncResult) {

        …

    }

}

Combining the related optional methods onto an interface or abstract class prevents the class from becoming overly cluttered with optional feature discovery methods and properties, and allows you to create groups of methods that are likely to be used together, also assisting the user of your class library in finding the methods they need. The user of your class will then be able to write the following code to use the optional feature, when it is available, and to fall back to the basic level when it is not.

Command cmd = GetCommandFromSomewhere();

  

if (cmd.CanGetAsyncCommand){

    IAsyncCommand asyncCmd = cmd.GetAsyncCommand();

    IAsyncResult asyncResult = asyncCmd.BeginExecuteReader();
// Async processing while execution occurs

    DataReader rdr = asyncCmd.EndExecuteReader(asyncResult);

}

else {

    DataReader rdr = cmd.ExecuteReader();

}

While this is essentially what the type system does for you, it provides the unfamiliar user of the base class the visibility of the optional IAsyncCommand interface that they would otherwise not have had without scanning the object model.

* Do not use enums to list the optional behaviors and return them from an abstract method on the abstract class. For example, consider the following object model.

[Flags()]

public enum CommandOptionalFeatures {

    None = 0,

    ExecutePageReader = (1 << 0),

}

abstract public class Command {

    public virtual CommandOptionalFeatures OptionalFeatures {

        get { return CommandOptionalFeatures.None; }

    }

}

The CommandOptionalFeatures enumeration is problematic from a versioning standpoint – it may not be any more extensible than an interface and the implementation above parallels what the type system does for you anyway.

Furthermore, there really isn’t a much of a connection between the enumeration value and the method name. Consider the following code that a user of your class library would implement:

Command cmd = GetCommandFromSomewhere();

if ((cmd.OptionalFeatures & CommandOptionalFeatures.ExecutePageReader) != 0){

    DataReader rdr = ((IPagedCommand)cmd).ExecutePageReader(100,10);

}

else {

    DataReader rdr = cmd.ExecuteReader();

}

How will the user unfamiliar with your class library know to check the OptionalFeatures property, and look for the ExecutePageReader flag? The simple Boolean property per optional feature described above provides a much more visible clue.

Comments

  • Anonymous
    June 17, 2004
    The comment has been removed
  • Anonymous
    June 22, 2004
    Frank, thanks for the feedback. I agree with you that the guideline probably needs some more explanation on when it should be used and when inheritance may be a better choice. For example nobody denies that having ICollection<T> (non-indexed collection) and IList<T> extending ICollection<T> and adding indexing is the right design. On the other hand, majority of people involved in the design of the Framework’s Base Class Libraries believe that IsReadOnly property on ICollection<T> is better than splitting the hierarchy into IReadOnlyCollection<T> and ICollection<T> and almost everybody thinks that Stream.CanCancel + Cancel pair is superior to splitting the hierarchy into Stream and CancelableStream.
    I will try to write up something formal about our thinking here and post to the blog. But briefly, I think the differentiating factor is that there are many distinct consumers (APIs operating on) of IList<T> and ICollection<T>, but there are not that many consumers of just CancelableStream but not Stream. Usually people would write code to consume Stream and as an option, they would allow the code to cancel.
  • Anonymous
    June 22, 2004
    The comment has been removed
  • Anonymous
    August 13, 2009
    Frank, I see your point in that it definitely can be confusing.  But right now I'm running into an issue where implementing a whole new class for maybe 1 to 2 properties is just not significant enough.  And to say in other derived classes that this property is not to be used sounds a bit more realistic than having a ton of derived classes because of 1 or 2 properties and the key word here is that amount of consumers of those classes. Very interesting post.  Even though it was written 5 years ago!  Its still very relevant.