简体   繁体   中英

Exposing interfaces instead of concrete classes

I've recently read something about using interfaces when exposing collections instead of concrete implementations (IEnumerable instead of List). I'm trying to do that now in my code. However, when I expose a property that return IEnumerable, I'm having some difficulty of not allowing nulls as a return value. Example:

public class HumanResource
{
    public IEnumerable<EmployeeModel> Employees
    {
        get
        {
            // return what?
        }
    }
}

What should I return in the getter? I don't want to use automatic properties for this as I want to avoid nulls. What I want is to return a new collection with no items. Of course I can return any type that implements IEnumerable but how will the external user of the class know that? Or did I understand this exposing interface instead of concrete implementations wrong?

EDIT: Removed setter

Of course I can return any type that implements IEnumerable but how will the external user of the class know that?

They don't have to know that, that's exactly the point.

Your property promises to return an IEnumerable<EmplyeeModel> , and that's exactly what happens. It doesn't matter which class implementing this interface your code returns.

What I want is to return a new collection with no items.

So, Enumerable.Empty<EmplyeeModel>() or new List<EmployeeModel>() will do just fine.


When designing an API you need to think about what the consumers will do with the data types you return, and decide upon that accordingly.

Usually an IEnumerable<T> for collections suits everyone. When they want it in a list, they can do new List<T>(yourEnumerable) , or yourEnumerable.ToArray() to use it as an array.

What I want is to return a new collection with no items.

Properties let you do that very easily:

public class HumanResource
{
    // This is the real employees that gets returned when its not null
    private IEnumerable<EmployeeModel> employees; // may be null

    // This is the empty IEnumerable that gets returned when employees is null
    private static readonly IEnumerable<EmployeeModel> EmptyEmployees =
        new EmployeeModel[0];

    public IEnumerable<EmployeeModel> Employees
    {
        get
        {
            return employees ?? EmptyEmployees;
        }
        set {};
    }
}

The code returns an empty array when employees variable is set to null . You can set employees to a collection of any type that implements IEnumerable<EmployeeModel> , or even to an array if you prefer. This is possible because you return by interface.

The flip side of this, of course, is that the clients would have no direct access to methods of properties that are not exposed through the interface. For example, if employees is actually a List , the callers would have to use LINQ's Count() instead of obtaining .Count directly. Of course you can expose a different interface, say, IList<EmployeeModel> , to let your clients use additional methods.

You still need to provide an internal backing collection for the property in your class. You can initialize the collection in the constructor, or in the field declaration:

public class HumanResource
{
   private readonly IList<EmployeeModel> _employees = new List<EmployeeModel>();

   public IEnumerable<EmployeeModel> Employees
   {
     get
     {
         return _employees;
     }
     // No Setter - callers may only enumerate the collection
  }
}

As an aside, note that even if you did use an automatic property (eg List<EmployeeModel> ), that it would assume a default value of null, unless otherwise initialized elsewhere, so nothing changes in this respect.

Edit, Re : What are the benefits?

  • By removing the setter, or making it private, we prevent a caller from reassigning the internal collection of a HumanResource
  • By softening the collection from a List<> to an IEnumerable<> , it means the caller can only do read-only actions on the internal collection, such as to iterate it. In addition, IEnumerable<> can be used in a lazy iteration, allowing the caller to quit enumerating as soon as it has the data it needs.
  • As per the comment below, if the caller requires the data represented in a different collection, such as an Array , then LINQ extension methods such as .ToArray() , .ToList() , .ToDictionary() can be used. Doing so will create new collections for the caller, but with references to the same EmployeeModel objects. The performance penalties of doing this are minimal.

One final note is that there is usually no point in making the setter on an IEnumerable property private, or declaring the backing field as an IEnumerable , as this will prevent the class itself from using impure methods to manipulate the collection (ie add or remove objects from it), as doing so would require a cast, eg:

public class HumanResource
{
    public IEnumerable<EmployeeModel> Employees
    {
        get; 
        private set;
    }

    public HumanResource()
    {
        // Although the property can be assigned in the CTor to prevent the null issue ...
        Employees = new List<EmployeeModel>();
    }

    private void AddProductiveEmployee()
    {
        // ... We need to continually cast within the class, which is just silly.
        (Employees as IList).Add(new EmployeeModel());
    }

We would have the same problem with the manual backing field approach with an internal IEnumerable<>

// Unlikely to be useful
private readonly IEnumerable<EmployeeModel> _employees = new List<EmployeeModel>();

TL;DR

  • Use a collection type which is appropriate for the internal usage to the class (OO composition)
  • But on the external interfaces (eg public getter / property), hide the internal implementation to the minimum necessary (OO encapsulation)
  • Initializing the internal collection in the constructor (or inline) will prevent a null collection being exposed

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