Вопрос

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

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

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

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


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

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

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

(Кстати:ДэнниСмерф, твой ПМ звучит устрашающе.)

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


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

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

Это было полезно?

Решение

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


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

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

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


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


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

Другие советы

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

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

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

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

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

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

Мои два кусочка.

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

Например:

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

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

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

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

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

Еще немного.

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

((IFirst)this).AmbigousMethod()

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

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

IInterface variable = new ImplementingClass();

Верно?

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

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

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

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

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

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

Мне нужно было решить что-то подобное:Я хотел, чтобы список<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");
        }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top