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

Новые расширения в .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.08.2008 18:13:44
11 ОТВЕТОВ
РЕШЕНИЕ

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


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

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

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


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


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

9
25.04.2016 08:51:32

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

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

0
11.08.2008 18:21:37

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

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

Мои два бита.

6
11.08.2008 18:56:39

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

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

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

  • служебные классы, как отметил Вайбхав
  • расширение закрытых сторонних API
11
12.08.2008 07:23:55

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

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

IInterface variable = new ImplementingClass();

Правильно?

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

1
12.08.2008 07:38:23

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

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

2
17.08.2008 04:40:58

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

Например:

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

3
17.08.2008 09:00:27
Мне также нравится этот подход zooba, однако я пошел по пути частичного класса, где частичный содержал все «расширения».
Matthew Zielonka.co.uk 2.01.2013 09:42:57

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

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

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

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();
    }
}
2
18.08.2008 09:20:41

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

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

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

1
15.10.2008 20:32:03

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

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

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

((IFirst)this).AmbigousMethod()
2
8.01.2010 04:06:05
Согласен, хотя, если вам нужно сделать это, я предлагаю, чтобы либо интерфейсы были плохо спроектированы, либо чтобы ваш класс не реализовал их оба.
Keith 8.01.2010 08:09:58