Шаблоны интерфейса расширения

Новые расширения в .Net 3.5 позволяют отделить функциональность от интерфейсов.

Например, в .Net 2.0

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }

    List<IChild> GetChildren()
}

Может (в 3.5) стать:

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }
}

public static class HaveChildrenExtension {
    public static List<IChild> GetChildren( this IHaveChildren ) {
        //logic to get children by parent type and id
        //shared for all classes implementing IHaveChildren 
    }
}

Мне кажется, что это лучший механизм для многих интерфейсов. Им больше не нужна абстрактная база для совместного использования этого кода, и функционально код работает так же. Это может сделать код более удобным в обслуживании и более легким для тестирования.

Единственным недостатком является то, что реализация абстрактных баз может быть виртуальной, но можно ли это обойти (будет ли метод экземпляра скрывать метод расширения с тем же именем? Будет ли это сбивать с толку код?)

Есть ли другие причины не использовать этот шаблон регулярно?


Разъяснение:

Да, я вижу, что у методов расширения есть тенденция использовать их повсюду. Я был бы особенно осторожен с любыми типами значений в .Net без большой экспертной оценки (я думаю, что единственное, что у нас есть в строке, .SplitToDictionary() - аналогично, .Split() но с разделителем значения ключа)

Я думаю, что там ведутся споры о лучших практиках ;-)

(Между прочим: DannySmurf, ваш личный кабинет звучит устрашающе.)

Я специально спрашиваю здесь об использовании методов расширения там, где раньше у нас были методы интерфейса.


Я стараюсь избегать множества уровней абстрактных базовых классов - классы, реализующие эти модели, в основном уже имеют базовые классы. Я думаю, что эта модель могла бы быть более удобной в обслуживании и менее связанной, чем добавление дополнительных иерархий объектов.

Это то, что MS сделала с IEnumerable и IQueryable для Linq?

Ответов (11)

Решение

Я думаю, что разумное использование методов расширения ставит интерфейсы в более уравновешенное положение с (абстрактными) базовыми классами.


Управление версиями. Одно из преимуществ базовых классов над интерфейсами заключается в том, что вы можете легко добавлять новые виртуальные члены в более позднюю версию, тогда как добавление членов в интерфейс нарушит работу средств реализации, созданных для старой версии библиотеки. Вместо этого необходимо создать новую версию интерфейса с новыми членами, и библиотеке придется обходить или ограничивать доступ к устаревшим объектам, реализующим только исходный интерфейс.

В качестве конкретного примера, первая версия библиотеки может определять такой интерфейс:

public interface INode {
  INode Root { get; }
  List<INode> GetChildren( );
}

После выпуска библиотеки мы не сможем изменить интерфейс, не нарушив работу текущих пользователей. Вместо этого в следующем выпуске нам нужно будет определить новый интерфейс для добавления дополнительных функций:

public interface IChildNode : INode {
  INode Parent { get; }
}

Однако только пользователи новой библиотеки смогут реализовать новый интерфейс. Чтобы работать с унаследованным кодом, нам нужно адаптировать старую реализацию, с которой хорошо справляется метод расширения:

public static class NodeExtensions {
  public INode GetParent( this INode node ) {
    // If the node implements the new interface, call it directly.
    var childNode = node as IChildNode;
    if( !object.ReferenceEquals( childNode, null ) )
      return childNode.Parent;

    // Otherwise, fall back on a default implementation.
    return FindParent( node, node.Root );
  }
}

Теперь все пользователи новой библиотеки могут одинаково относиться как к устаревшим, так и к современным реализациям.


Перегрузки. Другая область, в которой могут быть полезны методы расширения, - это предоставление перегрузок для методов интерфейса. У вас может быть метод с несколькими параметрами для управления его действием, из которых только первые один или два важны в случае 90%. Поскольку C# не позволяет устанавливать значения по умолчанию для параметров, пользователи должны либо каждый раз вызывать полностью параметризованный метод, либо каждая реализация должна реализовывать тривиальные перегрузки для основного метода.

Вместо этого можно использовать методы расширения для предоставления тривиальных реализаций перегрузки:

public interface ILongMethod {
  public bool LongMethod( string s, double d, int i, object o, ... );
}

...
public static LongMethodExtensions {
  public bool LongMethod( this ILongMethod lm, string s, double d ) {
    lm.LongMethod( s, d, 0, null );
  }
  ...
}


Обратите внимание, что оба этих случая написаны в терминах операций, предоставляемых интерфейсами, и включают тривиальные или хорошо известные реализации по умолчанию. Тем не менее, вы можете унаследовать от класса только один раз, а целевое использование методов расширения может предоставить ценный способ справиться с некоторыми тонкостями, предоставляемыми базовыми классами, которых нет в интерфейсах :)


Изменить: связанное сообщение Джо Даффи: методы расширения как реализации метода интерфейса по умолчанию

Я вижу, что многие люди выступают за использование базового класса для разделения общих функций. Будьте осторожны с этим - вы должны отдавать предпочтение композиции, а не наследованию. Наследование следует использовать только для полиморфизма, когда это имеет смысл с точки зрения моделирования. Это не лучший инструмент для повторного использования кода.

Что касается вопроса: будьте осторожны с ограничениями при этом - например, в показанном коде использование метода расширения для реализации GetChildren эффективно «запечатывает» эту реализацию и не позволяет IHaveChildren impl предоставлять свою собственную, если это необходимо. Если это нормально, то я не возражаю против подхода метода расширения. Он не высечен в камне и обычно может быть легко переработан, когда позже потребуется больше гибкости.

Для большей гибкости может быть предпочтительнее использовать шаблон стратегии. Что-то вроде:

public interface IHaveChildren 
{
    string ParentType { get; }
    int ParentId { get; }
}

public interface IChildIterator
{
    IEnumerable<IChild> GetChildren();
}

public void DefaultChildIterator : IChildIterator
{
    private readonly IHaveChildren _parent;

    public DefaultChildIterator(IHaveChildren parent)
    {
        _parent = parent; 
    }

    public IEnumerable<IChild> GetChildren() 
    { 
        // default child iterator impl
    }
}

public class Node : IHaveChildren, IChildIterator
{ 
    // *snip*

    public IEnumerable<IChild> GetChildren()
    {
        return new DefaultChildIterator(this).GetChildren();
    }
}

Я думаю, что лучшее, что заменяют методы расширения, - это все те служебные классы, которые вы найдете в каждом проекте.

По крайней мере, на данный момент я чувствую, что любое другое использование методов расширения вызовет путаницу на рабочем месте.

Мои два бита.

Одна проблема, которую я вижу, заключается в том, что в большой компании этот шаблон может сделать код трудным (если не невозможным) для понимания и использования кем-либо. Если несколько разработчиков постоянно добавляют свои собственные методы к существующим классам, отдельно от этих классов (и, Боже, помоги нам всем, даже к классам BCL), я мог бы увидеть, как база кода довольно быстро выходит из-под контроля.

Даже на моей собственной работе я мог видеть, как это происходит, с желанием моего менеджера по маркетингу добавить каждый бит кода, над которым мы работаем, либо в пользовательский интерфейс, либо на уровень доступа к данным, я мог полностью видеть, что он настаивает на добавлении 20 или 30 методов в System.String, которые только косвенно связаны с обработкой строк.

Методы расширения следует использовать просто так: расширения. Любой критически важный код, связанный со структурой / дизайном, или нетривиальная операция, должны быть помещены в объект, который составлен / унаследован от класса или интерфейса.

Как только другой объект попытается использовать расширенный, он не увидит расширения и, возможно, придется повторно реализовать / повторно ссылаться на них снова.

Традиционно считается, что методы расширения следует использовать только для:

  • служебные классы, как упомянул Вайбхав
  • расширение закрытых сторонних API

Ой. Пожалуйста, не расширяйте интерфейсы.
Интерфейс - это чистый контракт, который должен реализовать класс, и ваше использование указанных классов должно быть ограничено тем, что находится в основном интерфейсе, чтобы это работало правильно.

Вот почему вы всегда объявляете интерфейс как тип, а не как фактический класс.

IInterface variable = new ImplementingClass();

Верно?

Если вам действительно нужен контракт с некоторыми дополнительными функциями, абстрактные классы - ваши друзья.

Нет ничего плохого в расширении интерфейсов, по сути, именно так работает LINQ, добавляя методы расширения к классам коллекции.

При этом вы действительно должны делать это только в том случае, когда вам нужно предоставить одинаковые функциональные возможности для всех классов, реализующих этот интерфейс, и эта функциональность не является (и, вероятно, не должна быть) частью «официальной» реализации любого производного классы. Расширение интерфейса также хорошо, если просто нецелесообразно писать метод расширения для каждого возможного производного типа, который требует новой функциональности.

Я считаю, что разделение функциональных возможностей домена / модели и пользовательского интерфейса / представления с использованием методов расширения - это хорошо, тем более что они могут находиться в отдельных пространствах имен.

Например:

namespace Model
{
    class Person
    {
        public string Title { get; set; }
        public string FirstName { get; set; }
        public string Surname { get; set; }
    }
}

namespace View
{
    static class PersonExtensions
    {
        public static string FullName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName + " " + p.Surname;
        }

        public static string FormalName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName[0] + ". " + p.Surname;
        }
    }
}

Таким образом, методы расширения можно использовать аналогично шаблонам данных XAML. Вы не можете получить доступ к частным / защищенным членам класса, но это позволяет поддерживать абстракцию данных без чрезмерного дублирования кода во всем приложении.

Роб Коннери (Subsonic и MVC Storefront) реализовал шаблон, подобный IRepository, в своем приложении Storefront. Это не совсем то же самое, что и выше, но у него есть некоторые общие черты.

Уровень данных возвращает IQueryable, который позволяет слою-потребителю применять поверх него выражения фильтрации и сортировки. Бонусом является возможность, например, указать один метод GetProducts, а затем соответствующим образом решить на уровне потребления, как вы хотите эту сортировку, фильтрацию или даже только определенный диапазон результатов.

Не традиционный подход, но очень крутой и определенно случай DRY.

Мне нужно было решить нечто подобное: я хотел передать List <IIDable> функции расширения, где IIDable - это интерфейс с длинной функцией getId (). Я попытался использовать GetIds (этот List <IIDable> bla), но компилятор не позволил мне это сделать. Вместо этого я использовал шаблоны, а затем ввел внутри функции тип интерфейса. Мне нужна эта функция для некоторых классов, сгенерированных linq to sql.

Надеюсь, это поможет :)

    public static List<long> GetIds<T>(this List<T> original){
        List<long> ret = new List<long>();
        if (original == null)
            return ret;

        try
        {
            foreach (T t in original)
            {
                IIDable idable = (IIDable)t;
                ret.Add(idable.getId());
            }
            return ret;
        }
        catch (Exception)
        {
            throw new Exception("Class calling this extension must implement IIDable interface");
        }

Немного больше.

Если несколько интерфейсов имеют одинаковую сигнатуру метода расширения, вам нужно будет явно преобразовать вызывающий объект в один тип интерфейса, а затем вызвать метод. Например

((IFirst)this).AmbigousMethod()