[.NET, C#, ООП, Программирование] Творческое использование методов расширения в C# (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет, Хабр!
Продолжая исследование темы 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();
}
}
}
Выполнив рефакторинг исходного интерфейса, мы сделали его гораздо более разносторонним и, благодаря использованию методов расширения, ничуть не пожертвовали удобством использования.
Таким образом, я считаю методы расширения бесценным инструментом, который позволяет сохранить простое простым, а сложное превратить в возможное.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Alexey Golub
===========Похожие новости:
- [Программирование, Управление персоналом] Полюбите программиста
- [JavaScript, ReactJS, Программирование] Почему я разочаровался в хуках (перевод)
- [.NET, C#, Алгоритмы] GetHashCode() и философский камень, или краткий очерк о граблях
- [Python, Программирование] Напишем и поймем Decision Tree на Python с нуля! Часть 2. Основы Python, необходимые для генерации Decision Tree (перевод)
- [C++, Программирование] Антипаттерн “константа размера массива” (перевод)
- [Дизайн мобильных приложений, Программирование, Разработка мобильных приложений, Разработка под iOS] Российские пасхалки в мобильных приложениях. Какие они?
- [Разработка веб-сайтов, Программирование, Разработка мобильных приложений] Как захватить новую страну за 3 недели
- [Системное администрирование, Программирование, DevOps] Какой язык программирования быстрее работает, проще изучается, легче пишется и вообще всех прекрасней на свете?
- [JavaScript, Программирование, Браузеры, Стандарты связи] (Почти) бесполезный стриминг вебкамеры из браузера. Часть 2. WebRTC
- [Python, Программирование, Java, Kotlin] Распознавание текста на картинке с помощью tesseract на Kotlin
Теги для поиска: #_.net, #_c#, #_oop (ООП), #_programmirovanie (Программирование), #_c#, #_metody_rasshirenija (методы расширения), #_oop (ООП), #_proektirovanie_interfejsov (проектирование интерфейсов), #_chistyj_kod (чистый код), [url=https://torrents-local.xyz/search.php?nm=%23_blog_kompanii_izdatelskij_dom_«piter»&to=0&allw=0&o=1&s=0&f%5B%5D=820&f%5B%5D=959&f%5B%5D=958&f%5B%5D=872&f%5B%5D=967&f%5B%5D=954&f%5B%5D=885&f%5B%5D=882&f%5B%5D=863&f%5B%5D=881&f%5B%5D=860&f%5B%5D=884&f%5B%5D=865&f%5B%5D=873&f%5B%5D=861&f%5B%5D=864&f%5B%5D=883&f%5B%5D=957&f%5B%5D=859&f%5B%5D=966&f%5B%5D=956&f%5B%5D=955]#_blog_kompanii_izdatelskij_dom_«piter» (
Блог компании Издательский дом «Питер»
)[/url], #_.net, #_c#, #_oop (
ООП
), #_programmirovanie (
Программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:14
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет, Хабр! Продолжая исследование темы 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(); } } } Выполнив рефакторинг исходного интерфейса, мы сделали его гораздо более разносторонним и, благодаря использованию методов расширения, ничуть не пожертвовали удобством использования. Таким образом, я считаю методы расширения бесценным инструментом, который позволяет сохранить простое простым, а сложное превратить в возможное. =========== Источник: habr.com =========== =========== Автор оригинала: Alexey Golub ===========Похожие новости:
Блог компании Издательский дом «Питер» )[/url], #_.net, #_c#, #_oop ( ООП ), #_programmirovanie ( Программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:14
Часовой пояс: UTC + 5