简体   繁体   中英

Design a class, what's wrong if using IList as type of property?

I have a simple class below, it has a property Pages of type IList.

There are some options to implement this property, it can be an Array or a Collection / List / even ReadOnlyCollection

public class Book
    {
        private string[] _pages;

        public Book(string[] pages)
        {
            _pages = pages;
        }

        public IList<string> Pages
        {
            get
            {
                return _pages;
                //return new Collection<string>(_pages);
                //return new List<string>(_pages);
                //return new ReadOnlyCollection<string>(_pages);
            }
        }
    }

At design time , I do not know which actions that its clients will use the property but choosing any option above will affect its clients.

If a client uses Book class as below

var book = new Book(new[] {"A", "B"});
var pages = book.Pages;
pages[0] = "A2";

Not all implementation options of the property Pages will work for the client.

Option 1: returning an Array for Pages // OK, it works

public IList<string> Pages
    {
        get
        {
            return _pages;
        }
    }

Option 2: returning a Collection for Pages // KO, it throws an exception NotSupportedException Collection is read-only

public IList<string> Pages
    {
        get
        {
            return new Collection<string>(_pages);
        }
    }

Option 3: returning a List for Pages // OK, it works

public IList<string> Pages
    {
        get
        {
            return new List<string>(_pages);
        }
    }

Option 4: returning a ReadOnlyCollection for Pages // KO, it throws an exception SystemException Collection is read-only

public IList<string> Pages
    {
        get
        {
            return new ReadOnlyCollection<string>(_pages);
        }
    }

I don't think it could be wrong on the client side. Could anyone please give some explanation and suggest a good type for the Page ?

Another option would be to define your own collection type PageCollection for example and use it in your Book class. The advantage of this is, that you hide the detail of how the collection is implemented. Another advantage of this approach is, that you can provide "special" methods for the collection.

For example you could extend an existing collection (it may also be a good advise to create a custom class for Page , instead of using plain strings:

public class PageCollection : List<Page>
{
    // additional methods
}

You can also wrap an existing collection. This has the advantage that you have full controll over the methods you want to provide to the "user".

public class PageCollection : IEnumerable<Page>
{
    private List<Page> _innerCollection = new List<Page>();

    public void RipOut(IEnumerable<Page> pages)
    {
        foreach (var page in pages)
        {
            _innerCollection.Remove(page);
        }
    }

    // other methods

    public IEnumerator<Page> GetEnumerator()
    {
        return _innerCollection.GetEnumerator();
    }

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

If I understand your question, you are asking: How do I design a class without knowing how it will be used?

The answer is, design it by putting in the functionality you want to support.

In your example: You have a book with pages. Would you like the user to be able to go to page 0, rip it out, and replace it with something else? If so, make it a List. If not, make it a ReadOnlyCollection.

I would recommend exposing the property as what it actually is (List or ReadOnlyCollection), that way you give clear intent into what is allowed and don't have the client guessing if they can or cannot replace the page.

So finally I come with this design. The custom PageCollection has Add/Remove methods will allow me to add validation behavior inside. If using the List<Page> , this is not possible.

The Book now exposes IPageCollection Pages which has less methods than the IList.

Updated: Below are reasons to create the new interface:

  • IList / List is not conform in this situation because it's a little fat as some one said already.
  • List does not allow to have custom validation on Add / Remove a page from the list
  • Using a generic list does not satisfy a Code Analysis rule
  • If needed other methods like AddRange, Clear... it's easy to implement

.

public class Page
{
}

public interface IPageCollection : IReadOnlyList<Page>
{
    void Add(Page page);
    void Remove(Page page);
}

public class PageCollection : IPageCollection
{
    public PageCollection(IList<Page> pages)
        : base(pages)
    {
    }

    public void Add(Page page)
    {
    }

    public void Remove(Page page)
    {
    }

    ...
}

public class Book
{
    private readonly PageCollection _pages;

    public Book(IList<Page> pages)
    {
        _pages = new PageCollection(pages);
    }

    public IPageCollection Pages
    {
        get { return _pages; }
    }
}

(thanks for suggestions from guys John, Thomas...)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM