Шаблоны интерфейса расширения - PullRequest
23 голосов
/ 11 августа 2008

Новые расширения в .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 ]

11 голосов
/ 12 августа 2008

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

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

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

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

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


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

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

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


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


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

6 голосов
/ 11 августа 2008

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

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

Мои два бита.

3 голосов
/ 17 августа 2008

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

Например:

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

2 голосов
/ 08 января 2010

Чуть больше.

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

((IFirst)this).AmbigousMethod()
2 голосов
/ 18 августа 2008

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

Что касается вопроса: остерегайтесь ограничений при выполнении этого - например, в показанном коде, использование метода расширения для реализации 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 голосов
/ 17 августа 2008

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

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

1 голос
/ 16 октября 2008

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

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

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

1 голос
/ 12 августа 2008

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

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

IInterface variable = new ImplementingClass();

правый

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

0 голосов
/ 07 декабря 2009

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