Tuesday, February 12, 2008

Exposing collections

Keyvan Nayyeri has a blog post about exposing generic collections rather than lists in API's.

The basis of his post is that he says that it's a bad idea to expose List<T> publicly, I couldn't agree more. Actually I think it's such a bad idea that I didn't realize that it was a wide spread habit. If you're going to expose a collection it should be exposed in one of two ways in my opinion.

  1. The best alternative in most cases is a strongly typed collection named after what it contains (for example StudentCollection contains Student-objects), that implements at least the IEnumerable<Student> or more specific interfaces (ICollection<Student> or IList<Student>) if so needed.
  2. Expose the collections interface, not the implementation and don't specialize the interface more than needed. If the api just exposes a collection of students in no particular order and it should be read only expose it as an IEnumerable<Student>, not ICollection<Student> or IList<Student>.

Also when you take collections as parameters in your methods use the strongly typed collection or interfaces.

EDIT: I've found that there is actually a generic counterpart of the System.Collections.CollectionBase-class, which can be found in the namespace System.Collections.ObjectModel and it's simply called Collection<T>. This saves us the labor of having to implement our own such base class but the principles in this article still holds true.

Before generics were introduced in .net the simplest way to implement a strongly typed collection was to inherit from the System.Collections.CollectionBase-class, for some reason there is no generic version of this class so I've created one of my own (that implements bot the ICollection<T> and the ICollection interfaces). The nice thing about this abstract class is that it uses explicit interface implementation so none of the interface-methods are exposed (if the collection is not cast to the interface) this allows the developer to expose only the wanted methods through the API. The benefit of this is a much clearer API that is a lot easier to use with Intellisense since only the defined methods are shown.

For example to implement a FooCollection you'd inherit from the CollectionBase<T> class as follows:

public class Foo
{

}

public class FooCollection
    : CollectionBase<Foo>
{
    public void Add(Foo item)
    {
        this.InnerCollection.Add(item);
    }

    protected override ICollection<Foo> CreateInnerCollection()
    {
        return new List<Foo>();
    }
}

In this way we have created a strongly typed collection that also implements both the ICollection<Foo> and the non generic ICollection interfaces we also only expose the Add-method keeping the intellisense simple and public API simple (of course all interface methods are implemented and accessible through a cast).

My implementation of CollectionBase<T> is shown below.

/// <summary>
/// An abstract base class used to create strongly typed collections.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
public abstract class CollectionBase<T> :
    ICollection<T>, ICollection
{
    #region Constructor
    /// <summary>
    /// Creates a new instance.
    /// </summary>
    protected CollectionBase()
    {            
        this.InnerCollection = this.CreateInnerCollection();
    }

    /// <summary>
    /// Creates a new instance and sets the inner collection to the supplied
    /// collection.
    /// </summary>
    /// <param name="innerCollection">The collection to use to store
    /// the items internally.</param>
    /// <exception cref="ArgumentNullException">Thrown if the innerCollection parameter
    /// is null (Nothing in VB).</exception>
    protected CollectionBase(ICollection<T> innerCollection)
    {
        if (innerCollection == null)
            throw new ArgumentNullException("innerCollection");

        this.InnerCollection = innerCollection;
    }
    #endregion

    #region Properties
    #region InnerCollection
    [DebuggerBrowsable(DebuggerBrowsableState.Never), EditorBrowsable(EditorBrowsableState.Never)]
    private ICollection<T> _innerCollection;

    /// <summary>
    /// A collection that stores the items internally.
    /// </summary>
    protected ICollection<T> InnerCollection
    {
        [DebuggerStepThrough]
        get { return _innerCollection; }
        [DebuggerStepThrough]
        set { _innerCollection = value; }
    }
    #endregion InnerCollection
    #endregion

    #region Methods
    #region CreateInnerCollection
    /// <summary>
    /// When implemented by a sub class this method creates a collection
    /// that can be used as inner collection.
    /// </summary>
    /// <returns>A new ICollecion instance.</returns>
    protected abstract ICollection<T> CreateInnerCollection(); 
    #endregion CreateInnerCollection
    #endregion Methods

    #region ICollection<T> Members

    void ICollection<T>.Add(T item)
    {
        this.InnerCollection.Add(item);
    }

    void ICollection<T>.Clear()
    {
        this.InnerCollection.Clear();
    }

    bool ICollection<T>.Contains(T item)
    {
        return this.InnerCollection.Contains(item);
    }

    void ICollection<T>.CopyTo(T[] array, int arrayIndex)
    {
        this.InnerCollection.CopyTo(array, arrayIndex);
    }

    /// <summary>
    /// Gets the number of items in the collection.
    /// </summary>
    public int Count
    {
        get { return this.InnerCollection.Count; }
    }

    bool ICollection<T>.IsReadOnly
    {
        get { return this.InnerCollection.IsReadOnly; }
    }

    bool ICollection<T>.Remove(T item)
    {
        return this.InnerCollection.Remove(item);
    }

    #endregion

    #region IEnumerable<T> Members

    public IEnumerator<T> GetEnumerator()
    {
        return this.InnerCollection.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.InnerCollection.GetEnumerator();
    }

    #endregion

    #region ICollection Members

    void ICollection.CopyTo(Array array, int index)
    {
        this.InnerCollection.CopyTo((T[])array, index);
    }

    int ICollection.Count
    {
        get { return this.InnerCollection.Count; }
    }

    bool ICollection.IsSynchronized
    {
        get { return false; }
    }

    object ICollection.SyncRoot
    {
        get { return this.InnerCollection; }
    }

    #endregion
}

 

Technorati-taggar: ,,

4 comments:

  1. Do you have a VB.Net conversion of your generic collections class?

    ReplyDelete
  2. I think that the converter at http://www.developerfusion.com/tools/convert/csharp-to-vb/ should take care of this just fine.

    ReplyDelete
  3. I know the point to this post is to NOT expose a List. However, if I want to consume the collection and cast it to a List<T>... how would I do that? I tried some code and get an error saying the cast from the generic collection to List is invalid.

    ReplyDelete
  4. Since it's not a list you can not _cast_ it to one, however you could use the extension method "ToList" from the Linq-namespace. Or you could use "new List<YourType>(theCollectionYouHaveAtHand)".

    ReplyDelete