Привет, Хабр!
Продолжая исследование темы C#, мы перевели для вас следующую небольшую статью, касающуюся оригинального использования extension methods. Рекомендуем обратить особое внимание на последний раздел, касающийся интерфейсов, а также на профиль автора.

Уверен, что любой, хотя бы немного имевший дело с C#, знает о существовании методов расширений (extension methods). Это приятная фича, позволяющая разработчикам расширять имеющиеся типы новыми методами.
Это исключительно удобно в случаях, когда вы хотите добавить функционал к таким типам, которых не контролируете. Фактически, любому рано или поздно приходилось писать расширение для BCL, просто чтобы сделать некоторые вещи более доступными.
Но, наряду со сравнительно очевидными случаями использования, есть и очень интересные паттерны, завязанные непосредственно на использовании методов расширений и демонстрирующие, как их можно задействовать не самым традиционным образом.
Добавление методов к перечислениям
Перечисление – это просто набор константных числовых значений, каждому из которых присвоено уникальное имя. Хотя, перечисления в C# и наследуют от абстрактного класса Enum, они не трактуются как настоящие классы. В частности, это ограничение не позволяет им иметь методы.
В некоторых случаях может быть полезно запрограммировать логику в перечисление. Например, если значение перечисления может существовать в нескольких разных представлениях, и вы хотели бы легко преобразовывать одно в другое.
Например, представьте себе следующий тип в обычном приложении, позволяющем сохранять файлы в различных форматах:
Данное перечисление определяет список форматов, поддерживаемых в приложении, и может использоваться в разных частях приложения для инициирования логики ветвления в зависимости от конкретного значения.
Поскольку каждый формат файла может быть представлен в виде файлового расширения, было бы хорошо, если бы в каждом
Что, в свою очередь, позволяет нам поступить так:
Рефакторинг классов моделей
Бывает, что вы не хотите добавлять метод непосредственно к классу, например, если работаете с анемичной моделью.
Анемичные модели обычно представлены набором публичных неизменяемых свойств, только для получения. Поэтому при добавлении методов к классу модели может создаться впечатление, что нарушается чистота кода, либо можно заподозрить, что методы обращаются к какому-либо приватному состоянию. Методы расширения такой проблемы не вызывают, поскольку не имеют доступа к приватным членам модели и по природе своей не являются частью модели.
Итак, рассмотрим следующий пример с двумя моделями: одна представляет закрытый список титров, а другая – отдельную строку титров:
В текущем состоянии, если потребуется получить строку субтитров, отображенную в конкретный момент времени, мы запустим LINQ такого рода:
Здесь в самом деле напрашивается какой-либо вспомогательный метод, который можно было бы реализовать либо как метод члена, либо как метод расширения. Я предпочитаю второй вариант.
В данном случае метод расширения позволяет добиться того же, что и обычный, но дает ряд неочевидных бонусов:
В целом, при использовании подхода с методами расширений удобно проводить линию между необходимым и полезным.
Как сделать интерфейсы разностороннее
При проектировании интерфейса всегда хочется, чтобы контракт оставался минимальным, поскольку так его будет легче реализовать. Это очень помогает, когда интерфейс предоставляет функционал в самом обобщенном виде, так что ваши коллеги (или вы сами) можете надстраивать над ним обработку более специфичных случаев.
Если вам это кажется нонсенсом, рассмотрим типичный интерфейс, сохраняющий модель в файл:
Все работает нормально, но через пару недель может подоспеть новое требование: классы, реализующие
Таким образом, чтобы выполнить это требование, мы добавляем к контракту новый метод:
Это изменение только что сломало все имеющиеся реализации
Но, чтобы не делать всего этого, мы могли с самого начала спроектировать интерфейс немного иначе:
В таком виде интерфейс вынуждает прописывать место назначения в максимально обобщенном виде, то есть, это
Единственный недостаток такого подхода заключается в том, что самые базовые операции становятся не столь простыми, как мы привыкли: теперь приходится задавать конкретный экземпляр
К счастью, этот недостаток полностью обнуляется при использовании методов расширений:
Выполнив рефакторинг исходного интерфейса, мы сделали его гораздо более разносторонним и, благодаря использованию методов расширения, ничуть не пожертвовали удобством использования.
Таким образом, я считаю методы расширения бесценным инструментом, который позволяет сохранить простое простым, а сложное превратить в возможное.
Продолжая исследование темы C#, мы перевели для вас следующую небольшую статью, касающуюся оригинального использования extension methods. Рекомендуем обратить особое внимание на последний раздел, касающийся интерфейсов, а также на профиль автора.

Уверен, что любой, хотя бы немного имевший дело с C#, знает о существовании методов расширений (extension methods). Это приятная фича, позволяющая разработчикам расширять имеющиеся типы новыми методами.
Это исключительно удобно в случаях, когда вы хотите добавить функционал к таким типам, которых не контролируете. Фактически, любому рано или поздно приходилось писать расширение для BCL, просто чтобы сделать некоторые вещи более доступными.
Но, наряду со сравнительно очевидными случаями использования, есть и очень интересные паттерны, завязанные непосредственно на использовании методов расширений и демонстрирующие, как их можно задействовать не самым традиционным образом.
Добавление методов к перечислениям
Перечисление – это просто набор константных числовых значений, каждому из которых присвоено уникальное имя. Хотя, перечисления в C# и наследуют от абстрактного класса Enum, они не трактуются как настоящие классы. В частности, это ограничение не позволяет им иметь методы.
В некоторых случаях может быть полезно запрограммировать логику в перечисление. Например, если значение перечисления может существовать в нескольких разных представлениях, и вы хотели бы легко преобразовывать одно в другое.
Например, представьте себе следующий тип в обычном приложении, позволяющем сохранять файлы в различных форматах:
public enum FileFormat { PlainText, OfficeWord, Markdown }
Данное перечисление определяет список форматов, поддерживаемых в приложении, и может использоваться в разных частях приложения для инициирования логики ветвления в зависимости от конкретного значения.
Поскольку каждый формат файла может быть представлен в виде файлового расширения, было бы хорошо, если бы в каждом
FileFormat содержался метод для получения этой информации. Как раз при помощи метода расширения это и можно сделать, примерно так:public static class FileFormatExtensions { public static string GetFileExtension(this FileFormat self) { if (self == FileFormat.PlainText) return "txt"; if (self == FileFormat.OfficeWord) return "docx"; if (self == FileFormat.Markdown) return "md"; // Будет выброшено, если мы забудем новый формат файла, // но забудем добавить соответствующее расширение файла throw new ArgumentOutOfRangeException(nameof(self)); } }
Что, в свою очередь, позволяет нам поступить так:
var format = FileFormat.Markdown; var fileExt = format.GetFileExtension(); // "md" var fileName = $"output.{fileExt}"; // "output.md"
Рефакторинг классов моделей
Бывает, что вы не хотите добавлять метод непосредственно к классу, например, если работаете с анемичной моделью.
Анемичные модели обычно представлены набором публичных неизменяемых свойств, только для получения. Поэтому при добавлении методов к классу модели может создаться впечатление, что нарушается чистота кода, либо можно заподозрить, что методы обращаются к какому-либо приватному состоянию. Методы расширения такой проблемы не вызывают, поскольку не имеют доступа к приватным членам модели и по природе своей не являются частью модели.
Итак, рассмотрим следующий пример с двумя моделями: одна представляет закрытый список титров, а другая – отдельную строку титров:
public class ClosedCaption { // Отображаемый текст public string Text { get; } // Когда он отображается относительно начала трека public TimeSpan Offset { get; } // Как долго текст остается на экране public TimeSpan Duration { get; } public ClosedCaption(string text, TimeSpan offset, TimeSpan duration) { Text = text; Offset = offset; Duration = duration; } } public class ClosedCaptionTrack { // Язык, на котором написаны субтитры public string Language { get; } // Коллекция закрытых надписей public IReadOnlyList<ClosedCaption> Captions { get; } public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions) { Language = language; Captions = captions; } }
В текущем состоянии, если потребуется получить строку субтитров, отображенную в конкретный момент времени, мы запустим LINQ такого рода:
var time = TimeSpan.FromSeconds(67); // 1:07 var caption = track.Captions .FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
Здесь в самом деле напрашивается какой-либо вспомогательный метод, который можно было бы реализовать либо как метод члена, либо как метод расширения. Я предпочитаю второй вариант.
public static class ClosedCaptionTrackExtensions { public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) => self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time); }
В данном случае метод расширения позволяет добиться того же, что и обычный, но дает ряд неочевидных бонусов:
- Понятно, что этот метод работает только с публичными членами класса и не изменяет его приватного состояния каким-нибудь таинственным образом.
- Очевидно, что этот метод просто позволяет срезать угол и предусмотрен здесь только для удобства.
- Этот метод относится к совершенно отдельному классу (или даже сборке), назначение которых – отделять данные от логики.
В целом, при использовании подхода с методами расширений удобно проводить линию между необходимым и полезным.
Как сделать интерфейсы разностороннее
При проектировании интерфейса всегда хочется, чтобы контракт оставался минимальным, поскольку так его будет легче реализовать. Это очень помогает, когда интерфейс предоставляет функционал в самом обобщенном виде, так что ваши коллеги (или вы сами) можете надстраивать над ним обработку более специфичных случаев.
Если вам это кажется нонсенсом, рассмотрим типичный интерфейс, сохраняющий модель в файл:
public interface IExportService { FileInfo SaveToFile(Model model, string filePath); }
Все работает нормально, но через пару недель может подоспеть новое требование: классы, реализующие
IExportService, должны не только экспортировать в файл, но и уметь писать в файл.Таким образом, чтобы выполнить это требование, мы добавляем к контракту новый метод:
public interface IExportService { FileInfo SaveToFile(Model model, string filePath); byte[] SaveToMemory(Model model); }
Это изменение только что сломало все имеющиеся реализации
IExportService, поскольку теперь все их нужно обновить, чтобы они поддерживали и запись в память. Но, чтобы не делать всего этого, мы могли с самого начала спроектировать интерфейс немного иначе:
public interface IExportService { void Save(Model model, Stream output); }
В таком виде интерфейс вынуждает прописывать место назначения в максимально обобщенном виде, то есть, это
Stream. Теперь при работе мы более не ограничены файлами и можем также нацеливаться на различные другие варианты вывода.Единственный недостаток такого подхода заключается в том, что самые базовые операции становятся не столь простыми, как мы привыкли: теперь приходится задавать конкретный экземпляр
Stream, обертывать его в инструкцию using и передавать как параметр.К счастью, этот недостаток полностью обнуляется при использовании методов расширений:
public static class ExportServiceExtensions { public static FileInfo SaveToFile(this IExportService self, Model model, string filePath) { using (var output = File.Create(filePath)) { self.Save(model, output); return new FileInfo(filePath); } } public static byte[] SaveToMemory(this IExportService self, Model model) { using (var output = new MemoryStream()) { self.Save(model, output); return output.ToArray(); } } }
Выполнив рефакторинг исходного интерфейса, мы сделали его гораздо более разносторонним и, благодаря использованию методов расширения, ничуть не пожертвовали удобством использования.
Таким образом, я считаю методы расширения бесценным инструментом, который позволяет сохранить простое простым, а сложное превратить в возможное.
