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

Новые расширения в .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() я думаю, что у нас есть только строка .SplitToDictionary() - аналогично .Split() но с ключом разделитель тоже)

Я думаю, что там есть целая дискуссия о лучшей практике ;-)

(Между прочим: DannySmurf, ваш премьер-министр звучит страшно.)

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


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

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

Ответ 1

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


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

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

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%. Поскольку С# не позволяет устанавливать значения по умолчанию для параметров, пользователи либо должны каждый раз вызывать полностью параметризованный метод, либо каждая реализация должна реализовывать тривиальные перегрузки для основного метода.

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

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 );
  }
  ...
}


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


Изменить: Связанный пост Joe Duffy: Методы расширения в качестве реализации метода интерфейса по умолчанию

Ответ 2

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

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

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

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

Ответ 3

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

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

Мои два бита.

Ответ 4

Я вижу, что разделение функции domain/model и UI/view с использованием методов расширения является хорошим, особенно потому, что они могут находиться в разных пространствах имен.

Например:

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. Вы не можете получить доступ к частным/защищенным членам класса, но он позволяет поддерживать абстракцию данных без чрезмерного дублирования кода во всем приложении.

Ответ 5

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

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

Ответ 6

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

Что касается вопроса: Будьте осторожны при выполнении этого - например, в показанном коде, используя метод расширения для реализации 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();
    }
}

Ответ 7

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

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

((IFirst)this).AmbigousMethod()

Ответ 8

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

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

IInterface variable = new ImplementingClass();

Right?

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

Ответ 9

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

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

Не традиционный подход, но очень крутой и, безусловно, случай СУХОЙ.

Ответ 10

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

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

Ответ 11

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