Предпочитаете методы расширения для инкапсуляции и повторного использования? - PullRequest
14 голосов
/ 14 июня 2010

edit4: wikified, поскольку кажется, что это переросло в обсуждение больше, чем в конкретный вопрос.

В программировании на C ++ обычно считается хорошей практикой«предпочесть не являющиеся членами не дружественные функции» вместо методов экземпляра.Это было рекомендовано Скоттом Мейерсом в этой классической статье доктора Доббса и повторено Хербом Саттером и Андреем Александреску в C ++ Стандарты кодирования (пункт 44);общий аргумент состоит в том, что если функция может выполнять свою работу исключительно, полагаясь на открытый интерфейс, предоставляемый классом, она фактически увеличивает инкапсуляцию , чтобы сделать ее внешней.Хотя это в некоторой степени смущает «упаковку» класса, преимущества, как правило, стоят того.

Теперь, с тех пор, как я начал программировать на C #, у меня было ощущение, что здесь является окончательным выражением концепции , которую они пытаютсядостичь с помощью «не принадлежащих, не дружественных функций, которые являются частью интерфейса класса».C # добавляет два важных компонента в микс - первый - интерфейсы , а второй методы расширения :

  • Интерфейсы позволяют классу формально определять свои открытыеконтракт, методы и свойства, которые они выставляют миру.
  • Любой другой класс может выбрать реализацию того же интерфейса и выполнить тот же контракт.
  • Методы расширения могут быть определены на интерфейсе, предоставляя любые функциональные возможности, которые могут быть реализованы через интерфейс для всех исполнителей автоматически .
  • И, что самое приятное, из-за поддержки сахара и IDE «синтаксиса экземпляра» их можно вызывать так же, как и любой другой метод экземпляра, , исключая когнитивные издержки!

Таким образом, вы получаете преимущества инкапсуляции функций «не член, не друг» с удобством членов.Похоже, лучшее из обоих миров для меня;библиотека .NET сама по себе является ярким примером в LINQ.Однако везде, где я смотрю, я вижу людей, предупреждающих против чрезмерного использования метода расширения;даже сама страница MSDN гласит:

В общем, мы рекомендуем применять методы расширения экономно и только тогда, когда это необходимо.

( edit: Даже в текущей библиотеке .NET я могу видеть места, где было бы полезно иметь расширения вместо методов экземпляра - например, всефункции полезности List<T> (Sort, BinarySearch, FindIndex и т. д.) были бы невероятно полезными, если бы их число было увеличено до IList<T> - получение бесплатной бонусной функциональности, подобной этой, добавляет гораздо больше преимуществ для реализацииинтерфейс.)

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

( edit2: В ответ на Томаса - в то время как C # начинался с Java (чрезмерно, imo)ОО-менталитет, кажется, с каждым новым выпуском охватывает все больше парадигмального программирования, и основной вопрос в этом вопросе: полезны или целесообразны ли использование методов расширения для изменения стиля (в сторону более общего / функционального C #) ...)

edit3: переопределяемые методы расширения

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

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

public static int MyMethod(this MyInterface obj, int arg, bool attemptCast=true)
{
    if (attemptCast && obj is MyExtensionOverrider)
    {
        return ((MyExtensionOverrider)obj).MyMethod(arg);
    }
    // regular implementation here
}

Интерфейс переопределения отражает все методы, определенные в MyExtension, за исключением параметров this или attemptCast:

public interface MyExtensionOverrider
{
    int MyMethod(int arg);
    string MyOtherMethod();
}

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

public class MyClass : MyInterface { ... }

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

public class MySpecializedClass : MyInterface, MyExtensionOverrider
{
    public int MyMethod(int arg) 
    { 
        //specialized implementation for one method
    }
    public string MyOtherMethod() 
    {   // fallback to default for others
        MyExtension.MyOtherMethod(this, attemptCast: false); 
    }
}

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

Я вижу три проблемы с этим подходом -

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

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

Ответы [ 3 ]

5 голосов
/ 14 июня 2010

Я думаю, что C # следует немного другой логике - как и в Java, аксиома в C # состоит в том, что все является объектом, и все функциональные возможности должны быть инкапсулированы в классе (как методы).C # не такой строгий - есть типы значений, которые на самом деле не являются объектами, и есть статические члены, которые также не принадлежат никаким объектам .

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

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

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

3 голосов
/ 15 июня 2010

Мне обычно нравятся методы расширения, особенно на интерфейсах, но у меня есть две проблемы с ними:

Во-первых, если у реализации есть более эффективный способ достижения цели метода расширения, нет общего способа выразитьтот.Например, Enumerable.Count() явно знает о ICollection / ICollection<T> и особых случаях его.Альтернативой для этого было бы, если бы интерфейсы могли фактически содержать реализации напрямую, ссылаясь только на другие методы интерфейса и не объявляя поля.Методы затем могут быть переопределены в соответствующих реализациях.Конечно, это означает, что вам необходимо владеть интерфейсом ... но в некоторых случаях он будет чище, чем существующие методы расширения.(Избегая возможности введения полей, я полагаю, что вы можете обойти некоторые проблемы реализации, которые могут возникнуть при множественном наследовании классов.)

Во-вторых, мне не нравится, как открываются методы расширения.Невозможно сказать «Я хочу методы расширения из класса X», не перетаскивая методы расширения из других классов в том же пространстве имен.Я хотел бы, чтобы вы могли написать:

using static System.Linq.Enumerable;

, чтобы подобрать только эти методы расширения.

(Кстати, я буду говорить больше об обоихиз этих пунктов в NDC 2010 в четверг. Надеемся, что разговор будет записан.)

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

Кстати, было бы неплохо иметь возможность писать методы в типе, но сказать: «Ограничьте меня использованием только открытого API. "

0 голосов
/ 14 июня 2010

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

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

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

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

...