Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Паттерны методов расширения

9 комментариев

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

Метод совмещенных вызовов (composite extension method)

Этот метод создается тогда, когда нужно вызвать два метода вместе. Ситуаций таких море – например у класса StringBuilder есть методы AppendFormat и AppendLine, но что если нам хочется получить форматированный текст с переносом строки? Метод расширения позволяет нам комбинировать два метода:

public static StringBuilder AppendFormatLine(
  this StringBuilder sb, string format, params object[] args)
{
  return sb.AppendFormat(format, args).AppendLine();
}

На самом деле, комбинировать различные вызовы можно по-разному, но почему-то последовательный вызов (“вызови A() а потом B()”) очень часто нужен, особенно в ситуациях где API не совсем продуман.

Метод рекурсивных вызовов (recursion extension method)

Позволяет бороться с рекурсией путем использования “плоской” коллекции в качестве субъекта методов расширения. Например, у вас есть Path.Combine(), которая берет только 2 параметра, а соединить нужно 3 отрезка пути. Как быть?

// нужно писать так
Path.Combine(a, Path.Combine(b, c));
// а хочется так
new[]{a, b, c}.PathCombine();

Это легко достигается путем написания метода расширения для string[]:

public static string PathCombine(this string[] paths)
{
  var result = string.Empty;
  foreach (var path in paths)
    result = Path.Combine(result, path);
  return result;
}

Ну или что-то в этом роде. Этот метод реализован в Mono.Rocks.

Метод добавления множественных параметров (params extension method)

Поскольку в .Net T[] и params T[] не эквивалентны, мы не можем просто набросать несколько параметров типа T когда, например, хочется добавить произвольное количество элементов в коллекцию. Поэтому на помощь приходит метод расширения с неограниченным числом параметров.

public static void AddRange<T>(this IList<T> list, params T[] objects)
{
  foreach (T obj in objects)
    list.Add(obj);
}

Вот пример использования подобного метода:

var list = new List<int>();
// раньше нужно было писать так
list.AddRange(new[] {1, 2, 3});
// а теперь можно так
list.AddRange(1, 2, 3);

Перенос статического метода в метод расширения (antistatic extension method)

Вместо того, чтобы вызывать статический метод класса X, можно вызвать метод расширения на конкретном экземпляре класса X (если это конечно возможно). Классический пример подхода – замещение метода String.Format() методом расширения:

public static string ƒ(this string format, params object[] args)
{
  return string.Format(format, args);
}

Использование символа ƒ – это личное препочтение, которое периодически дает проблемы с использованием исходников на машинах других пользователей, но у меня работает идеально. :)

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

Фабричные методы расширения (factory extension method)

Эти методы позволяют полностью нивелировать вызов конструктора объекта, спрятав его за методом расширения для первого параметра этого самого конструктора. Самый популярный пример – создание даты из fluent-выражения вроде 19.June(1976). Заметьте что само имя метода расширения тоже является неявным параметром для конструктора. Реализация достаточно простая:

public static DateTime June(this int day, int year)
{
  return new DateTime(year, 6, day);
}

Для фабричого метода не обязательно вешать этот метод на первый параметр. Например, если вашей целью является создание объекта (скажем, строки XML-кода) из key-value структуры, то можно передать весь объект. Достаточно вспомнить что в C# можно использовать анонимные типы. Тогда создание произвольного отрезка XML может выглядеть вот так:

new { Name="Dmitri", Age=25}.ToXElements();
// вернет нам элементы <Name>Dmitri</Name> и <Age>25</Age>

Я не буду здесь приводить реализацию этого метода – его можно найти в исходниках MiscUtil – но надеюсь что это хорошее напоминание читателю, что анонимный класс де факто является key-value коллекцией. А что, тот же Asp.Net MVC с удовольствием использует эту возможность языка.

Монадические методы расширения (monadic extension method)

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

public static TResult With<TInput, TResult>(
  this TInput o, Func<TInput, TResult> evaluator)
  where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

К слову замечу, что монадические методы расширения применимы в тех предметных областях, где язык F# более полезен чем C#. А у F#, как вы помните, можно расширять не только методами но и свойствами, что придает лаконичности некоторым аспектам вашего API за счет, эээ… всего того месива, которые вы получаете в связи с немутабельностью языка и прочими его капризами. :)

Методы расширения для создания прокси-объектов (proxy extension methods)

Иногда разработчикам для создания полноценного fluent-интерфейса полезно хранить какое-либо состояние, ассоциированное с данным объектом. Поскольку в условиях статичности (классы содержащие методы расшериния всегда статичны) крайне тяжело, для создания fluent-интерфейса используют прокси-объект, который в последствии может являться каким-то конфигуратором или умным параметром для чего-то еще. Помимо этого, он может реализовать целую уйму методов, которые не заполонят global scope как если бы они были extension-методами на object.

Вот пример – класс SomeProxy<T> который выступает конвертируемой оберткой для T:

public class SomeProxy<T>
{
  private readonly T subject;
  private object something;
  internal SomeProxy(T subject, object something)
  {
    this.subject = subject;
    this.something = something;
  }
  public static implicit operator T(SomeProxy<T> obj)
  {
    return obj.subject;
  }
  // и другие полезные операторы
}

Этот класс можно подставлять с помощью следующего метода расширения.

public static SomeProxy<T> Configure<T>(this T subject, object something)
{
  return new SomeProxy<T>(subject, something);
}

Эта реализация может показаться чем-то напоминающей “примесь” (mixin) или декоратор – ведь мы хитрым образом добавляем объекту состояние. Как вы думаете, что будет если попытаться “навесить” на объект несколько таких прокси-классов одновременно. Будет ли работать вывод типов? Если создать два объекта, например NameProxy и AgeProxy для навешивания имени и возраста на объекты…

public class NameProxy<T>
{
  private readonly T subject;
  public string Name { get; private set; }
  internal NameProxy(T subject, string name)
  {
    this.subject = subject;
    Name = name;
  }
  public static implicit operator T(NameProxy<T> obj)
  {
    return obj.subject;
  }
}
public class AgeProxy<T>
{
  private readonly T subject;
  public int Age { get; private set; }
  internal AgeProxy(T subject, int age)
  {
    this.subject = subject;
    Age = age;
  }
  public static implicit operator T(AgeProxy<T> obj)
  {
    return obj.subject;
  }
}

и добавить соответствующие методы расширения, то навесив оба эти “примеси” на какой-нибудь объект

object o = new object();
var processed = o.WithName("Dmitri").WithAge(25);

вы не сможете достучаться до “первоначального” свойства Name. Чтобы его получить, придется заниматься выведением типов (причем тех типов, которые пользователь в принципе не должен трогать):

// не очень-то гламурно
NameProxy<object> processed = o.WithName("Dmitri").WithAge(25);
Console.WriteLine(processed.Name);

В связи с этим можно заключить, что “наслаивать” примеси можно только если у пользователя не появится потребность в просмотре сгенерированных структур.

Функциональные методы расширения (functionality extension method)

В эту категорию попадают все остальные методы расширения. Методы расширения которые просто выполняют какое-то действие над объектом (грубо говоря, некий Action<T>) имеют правно на жизнь, особенно если учесть альтернативы которыми их можно реализовать. Если коротко, то “просто добавление функционала” к классу через методы расширения решает, во-первых, проблему сложности использования (как если бы мы создали XHelper для класса X чтобы производить определенные действия) и проблему загрязнения интерфейсов (как если бы мы реализовали методы в том классе, в котором X используется).

Одна из интересных задачек – это реализация метода расширения ForEach. Эрик Липперт в свое время написал пост на тему того, почему Microsoft не поставили этот метод в LINQ на ряду с Select(), Where() и прочими. Если коротко, то причина идеологическая – все что происходит в LINQ не имеет побочных эффектов (side effect), и тем самым делает возможным тот же PLINQ. Но в случае с ForEach(), все как раз наоборот – единственная цель этого метода – это производить сайд-эффекты. Нестыковочка получается.

Тем не менее, на просторах интернета можно найти не только прямую реализацию ForEach(), но также реализации, которые делают то же самое неявно. Например, тот же Mono.Rocks реализуем методы вроде Step(), которые сначала генерируют последовательности, а потом применяют ко всем сгенерированным элементам определенный делегат:

// все числа от 5 до 9 с шагом 2 пишутся в консоль
5.Step (9, 2, i => Console.WriteLine (i));

Заключение

Можно много дискутировать на тему “неочевидности” методов расширения а также того факта, что они “загрязняют” IntelliSense (особенно если вы делаете расширение для object или для <T>). Но лично я считаю что в некоторых случаях их использование вполне оправдано, и серьезно экономит время, которое могло быть банально потеряно на поиск нужного метода.

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

Advertisements

Written by Dmitri

22 марта 2010 в 21:50

Опубликовано в .NET, C#

комментариев 9

Subscribe to comments with RSS.

  1. Age = name; ;)

    dogwatch

    22 марта 2010 at 23:26

  2. Спасибо за пост, Дмитрий!
    Иногда я совсем не прочь упростить себе жизнь, написав пару-тройку методов-расширений (xm), где прячутся с десяток строк простого, но рутинного кода (главное — контролировать неймспейс, а то при неаккуратном перемещении понадобится reference resolver). Как хелперы они удобны скорее с точки зрения наглядности происходящего, но вряд ли могут породить «паттерны», я бы не делал на них «архитектурную» ставку. Уж точно, как сторонник статической типизации везде, где это возможно, не писал бы
    public static Object TryDoSomething(this Object _obj)
    {
    ///…
    }
    Для фабрик они тоже годятся с натяжкой, т.к. на самом деле вовсе не скрывают конструкторы расширяемых классов, поскольку не имею доступа к их внутренностям.
    Другое дело — функциональные расширения. Здесь соглашусь потому, что xm (с лямбда-выражениями) для них подходят прекрасно. С их помощью очень удобно делать свои итераторы, мемоизацию и каррирование или анонимную рекурсию в стиле чистой «функциональщины». Не хотел бы видеть Rx без методов-расширений :)

    Pavel Korotkov

    23 марта 2010 at 1:20

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

      Dmitri

      23 марта 2010 at 9:44

  3. Супер! Большое спасибо :)

    Blush

    24 марта 2010 at 8:46

  4. Опечатка?
    «Поскольку в условиях статичности (классы содержащие методы расшерения всегда статичны)» — видимо, все-таки расшИрения?

    Спасибо за блог, очень интересно!

    Кирилл Темненков

    26 марта 2010 at 21:11

    • Спасибо, поправил опечатку. Рад что вам блог нравится.

      Dmitri

      26 марта 2010 at 21:26

  5. За пост респект, поставлю эти рефакторинги в избранное :)

    hack2root

    11 августа 2010 at 17:42

  6. :)

    Daticc77

    24 декабря 2011 at 16:35


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: