Pull to refresh

Comments 55

Спорить по частностям тут можно много (например, всегда есть «места стыков», где криворукие имена требуются из-за механизмов стыковки предметных областей, есть DRY, который опять же порой порождает кривые имена, которыми, однако, будет назван часто вызываемый код), но вообще посыл статьи мне очень нравится.

Действительно, хорошие имена — это всего лишь один, но зато огромный шаг к поддерживаемости кода.
Пока код можно нормально и осознанно описать — идея с именованием заходит.
А иногда что писать — знаешь, а как описать — нет. Это что же, долго перебирать варианты как описать, чтобы решить как назвать, чтобы наконец то написать? Да не, ерунда какая то, лучше очередной Utils\Manager зафигачить =)

ПС: есть на эту тему «шутка» о всего двух проблемах программирования — именовании и инвалидации кеша =)
Если б это была шутка!
Программирование в современном понимании это, по сути, это написание описания алгоритмов понятных как людям (специально обученным), так и вычислительным устройствам.
А это написание на 90% состоит как раз из придумывания имён (остальные 10 придумали за нас авторы языка и библиотек).
А иногда что писать — знаешь, а как описать — нет. Это что же, долго перебирать варианты как описать, чтобы решить как назвать, чтобы наконец то написать?

На мой взгляд, да, нужно перебирать. Не что писать, а как — вот что важно.

Только и итоговый пример у него странный. Sorted ничем не лучше =_=

Разница между ListSorter и SortedList в том, что в первом случае мы говорим о списке, его сортировщике и отсортированном списке:


var list = new List<int>() { ... }; // Список.
var sortedList = ListSorter.Sort(list); // Сортировщик + отсортированный список.

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


Например:


var list = new List<int>();
var sortedList = new SortedList<int>(list);
// или
var list = new List<int>();
var sortedList = list.Sort();

Смотрится элегантнее, на мой взгляд. Работа не перенаправляется на аутсорс какому-то третьему лицу.

`Sort` — это не то, что должен уметь делать `List`, это то, что некто посторонний может сделать с `List`. Именно сортировщий сортирует набор элементов, а не сами они выстраиваются по росту.
Буква «S» в наборе «SOLID» прямым текстом говорит нам, что не должно быть так, чтобы объект сам себя менеджил, провайдил, гетил, сетил, ридил, райтил, валидэйтил, энкодил, декодил, диспетчил. Это бы прямо нарушало принцип «single responsibility». SOLID — святое. Догма. Принимается без дискуссии и включения мозга. Вот и городим зоопарк.
Респонсобилити — понятие расплывчатое и контекстнозависимое. В контексте небольших программ или библиотек с десятком классов — это вполне сингл респонобилити, если объект все действия делает только в рамках себя самого и не лезет в «чужие» предметные области
Расплывчатое понимание догмы и контекстнозависимое её применение — это Вы какую-то бездуховность сейчас проповедуете. Прямо фашизм какой-то.
Буква «S» в наборе «SOLID» прямым текстом говорит нам, что не должно быть так, чтобы объект сам себя менеджил, провайдил, гетил, сетил, ридил, райтил, валидэйтил, энкодил, декодил, диспетчил.

Боюсь, она ровно это и утверждает. Положим, JSON-сериализацию можно сделать так:


public class Weapon
{
    public int Damage;
}

public static class WeaponJsonSerializer // Как будто бы Single Responsibility.
{
    public static string Serialize(Weapon weapon) =>
        $"\{\"Damage\": {weapon.Damage}\}";
}

Хотя мы и вынесли сериализацию в отдельную сущность, принцип S, как мне видится, был нарушен: теперь каждое изменение полей Weapon потребует изменений в WeaponSerializer.


Можно:


public class Weapon : IJson
{
    public int Damage;

    public string AsJson() => $"\{\"Damage\": {Damage}\}";
}

Теперь лучше, поскольку Weapon ответственен за всё, что с ним происходит, но так работать попросту неудобно, да и, к тому же, иногда не так велика разница, идти ли вниз к методу AsJson() или к лежащему недалеко WeaponJsonSerializer.


Куда сильнее — использовать возможности метапрограммирования и реализовать обобщённую сериализацию с помощью рефлексии или кодогенерации. Библиотек полно.

То есть навернуть уровень абстракции. Как говорится, мы всегда так делаем.

Ну ОК, навернули. Дальше начинается прикольное:
1. Оказывается, вместе с Weapon в JSON полезно закатывать объекты, на которые weapon ссылается. Например, если Damage у нас не общий, а для каждого врага разный (огнемёт лавовому монстру только в радость), то в Weapon у нас массив объектов, в которых Damage и ссылка на тип врага. Кое-что из свойств врага, кстати, тоже оказывается полезно закатать в JSON. Как всегда происходит в подобных случаях, наш новый прекрасный уровень абстракции начинает усложняться, разрастаться, и в результате сам превращается в монстра хуже лавового.
2. Нежданчик. Оказывается, нам нужно иногда генерить разные JSONы. Ну то есть для хранения один, для сайта другой, для отправки в налоговую инспекцию по электронному документообороту третий, для годового отчёта Вельзевулу четвёртый. Будем усложнять уровень абстракции?

Это у нас только JSON. А есть ещё отображение, динамика, печать на бланке, контроль консистентности, репликация и штучки три интеграции с другими системами по ETL (как водится, в обе стороны, и совсем не через JSON). В какой-то момент времени мы титаническими усилиями приходим к тому, что со всем справились. Но тут вдруг возникает необходимость (не «хотелка», а именно суровая необходимость) добавить новую сущность. Какое-нибудь Remedy. По аналогии с Weapon. Мы смотрим, как у нас обвешан загадочными гроздьями мета-штук Weapon и понимаем, что зря пошли в программисты.

Боюсь, я не до конца понял вашу мысль.


То есть навернуть уровень абстракции

Что вы понимаете под уровнем абстракции в примере с Weapon, как он повысился и в какой момент?


Мы смотрим, как у нас обвешан загадочными гроздьями мета-штук Weapon

Вот это, кстати, понял. Возможно, не достаточно точно выразился, но если под "мета-штуками" вы имеете ввиду фразу про "метапрограммирование", то речь же была о простейшей библиотеке сериализации вроде Newtonsoft.Json.


Так что Weapon ничем, выходит, и не будет обвешан. Ответственность перенесли, как того требует S, но сохранили удобство, и никаких WeaponDamageGetter, WeaponJsonProvider, WeaponManager после себя не оставили.

Можно сделать же serializer не статичным и с ссылкой на объект. И asJson будет возвращать актуальное значение

Идея KISS для именования конечно хороша, но зачастую сложно найти баланс. В примере orders вместо orderRepository есть и плюсы и минусы. Как минимум, сходу такое имя воспринимается как коллекция. Да и через время не факт что в памяти всплывет нужная ассоциация, пока на тип не глянешь.

Примеры с DirectoryCleaner/Directory.Clean и file.Changes.OnNext могут работать в некоторых случаях, но в других могут порождать God Object с кучей ответственностей и зависимостей. Хотя конечно, все зависит от конкретного случая. Я заметил, что такие упрощения имен допустимо делать только в зрелом коде, который уже не так часто меняется, при условии что изменение API не создаст проблем. Но если изначально писать код в таком стиле, то это создаст больше путаницы чем принесет пользы. Потому-что с ходу тяжело адекватно оценить восприятие кода. Тут конечно code review хорошо помогает, особенно если ревьювер знаком с кодом только поверхностно.
В примере orders вместо orderRepository есть и плюсы и минусы

Полностью согласен. Среди множества всех возможных сценариев, существуют такие, в которых _ordersRepository смотрится лучше, чем _orders, но в большинстве (95%), на мой взгляд, Repository — избыточное уточнение.


Как минимум, сходу такое имя воспринимается как коллекция

Мне кажется, IOrdersRepository как раз имеет семантику коллекции: получить заказы, добавить, удалить. Так что он вполне себе может представлять множество заказов (или заказы), а уж тип, если потребуется, дополнит происходящее.


Я заметил, что такие упрощения имен допустимо делать только в зрелом коде, который уже не так часто меняется, при условии что изменение API не создаст проблем

На моём опыте люди, как правило, напротив, всё слишком переусложняют и переуточняют: _currentSelectedSpecificItemIndex. Превратить такую запись в _selectedIndex (убираем Item, поскольку, скорее всего, итак работаем в контексте какого-то типа SomeItem) — не упростить, но убрать избыточность.

Мне кажется, IOrdersRepository как раз имеет семантику коллекции: получить заказы, добавить, удалить.

Вот только семантика коллекции тут ложная: она маскирует стоимость всех этих операций.

Вот только семантика коллекции тут ложная: она маскирует стоимость всех этих операций.

Именно поэтому я уточнил: "а уж тип, если потребуется, дополнит происходящее".

Иногда существование классов бывает вызвано необходимостью.


Так, большинство Params-, Args- и Options-классов нужны для того, чтобы уменьшить число параметров у метода, и иметь возможность добавлять в будущем новые параметры сохраняя совместимость со старым кодом.


А ещё персистентность всегда всё портит. Ради возможности положить объект в базу или прочитать его оттуда приходится либо держать открытыми все внутренности объекта, либо создавать те самые Data и Info.


Куда живее звучит ISequence, а не IEnumerable; IBlueprint, а не ICreator; IButton, а не IButtonPainter; IPredicate, а не IFilter; IGate, а не IOpeneable; IToggle, а не IEnableable.

Вот только интерфейсы — это не сущности. Интерфейсы — это качества и роли сущностей. Нет никаких проблем если класс Sequence будет реализовывать интерфейс IEnumerable, класс Blueprint будет ICreator, а класс Gate будет IOpeneable.


Impl, Abstract, Custom, Base, Concrete, Internal, Raw — признак неустойчивости, расплывчатости архитектуры, который, как и ружье из первой сцены, позже обязательно выстрелит.

А как ещё разделять внешнее и внутреннее API? Если всё делать публичным — это и будет то самое ружье.


Например, поле типа IOrdersRepository так и называют — _ordersRepository. Но насколько важно сообщать о том, что заказы представлены репозиторием? Ведь куда проще — _orders.

Проще-то проще, но когда рядом находятся IOrdersRepository, List<Order> и Dictionary<Guid, Order> — приходится их хоть как-то различать.


Ещё, бывает, в LINQ-запросах пишут полные имена аргументов лямбда-выражений, например, Player.Items.Where(item => item.IsWeapon), хотя что это предмет (item) мы и без того понимаем, глядя на Player.Items

В таких простых ситуациях — да, понимаем. А вот в трехуровневых запросах на самом внутреннем уровне уже хотелось бы видеть item вместо простого x.


Лично мне каждый раз приходится вспоминать, чем отличаются Set от Reset и что вообще такое "вручную сбрасывающееся событие" в контексте работы с потоками.

А вот это уже просто незнание английского языка и стандартной терминологии. Надо выучить термины, а не подводить философию под своё незнание, иначе вы однажды будете сами вынуждены назвать монитор компьютером, а системный блок процессором.


Set переводится как "установить", Reset — как "сбросить". Название же вашего класса "ThreadGate" ничего не говорит о том, будут ли ворота закрыты после выхода из WaitForOpen или нет, а ведь это важная информация.

Так, большинство Params-, Args- и Options-классов нужны для того, чтобы уменьшить число параметров у метода, и иметь возможность добавлять в будущем новые параметры сохраняя совместимость со старым кодом.

Тут тогда, думается мне, есть проблема посерьёзнее: недостаточно разбили предметную область на сущности, поэтому есть методы с большим количеством параметров. Расширять это дело настоятельно не рекомендую. Спрятать много параметров в объект не поможет, ведь нагрузка на ум осталась!


А ещё персистентность всегда всё портит. Ради возможности положить объект в базу или прочитать его оттуда приходится либо держать открытыми все внутренности объекта, либо создавать те самые Data и Info.

Согласен, и такое бывает! Но это не оправдывает другие случаи, когда Info и Data — наспех сочинённая декомпозиция бизнес-логики.


Вот только интерфейсы — это не сущности. Интерфейсы — это качества и роли сущностей.

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


Нет никаких проблем если класс Sequence будет реализовывать интерфейс IEnumerable

Проблемы есть, ведь как понятия Sequence и Enumerable очень схожи между собой, только одно основывается на уже известной последовательности, тогда как второе — на глаголе перечислять, из которого выводится, что перечисляем мы последовательность.


Идея вот какая: важно подобрать такое слово, чтобы оно как можно глубже вплеталось в опыт. IOpeneable не восходит к картинкам, а IGate — напротив.


А как ещё разделять внешнее и внутреннее API? Если всё делать публичным — это и будет то самое ружье.

Думаю, нет никакой связи между тем, что пишут HumanBase (вместо Animal, например), и публичностью и непубличностью. Речь об архитектурной сложности решения. С Base, Impl, Internal, Raw и прочим ничего понятного и простого, как правило, не получается.


Проще-то проще, но когда рядом находятся IOrdersRepository, List<Order> и Dictionary<Guid, Order> — приходится их хоть как-то различать.

Полагаю, наличие частного случая никак не влияет на все остальные, где IOrdersRepository — единственный объект, представляющий заказы.


В таких простых ситуациях — да, понимаем. А вот в трехуровневых запросах на самом внутреннем уровне уже хотелось бы видеть item вместо простого x.

Если вы имеете ввиду нечто вроде:


Inventory.Items
    .Where(item => item.IsWeapon)
    .Select(item => item.Damage)
    .Where(damage => damage > 10);

То, мне кажется, куда просторнее смотрится:


Inventory.Items
    .Where(x => x.IsWeapon)
    .Select(x => x.Damage)
    .Where(x => x > 10);

Что x > 10 — про урон, думаю, всё ещё понятно, а дышать стало легче.


Set переводится как "установить", Reset — как "сбросить". Название же вашего класса "ThreadGate" ничего не говорит о том, будут ли ворота закрыты после выхода из WaitForOpen или нет, а ведь это важная информация.

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


Например, можно убрать оттуда слово Thread (блокировка потока как будто следует из WaitForOpen) и получится два типа: Gate и AutoCloseGate. Всё ещё понятнее, чем стандартный аналог.

Проблемы есть, ведь как понятия Sequence и Enumerable очень схожи между собой, только одно основывается на уже известной последовательности, тогда как второе — на глаголе перечислять, из которого выводится, что перечисляем мы последовательность.

Вот только IEnumerable — не последовательность. В математике в последовательности можно напрямую получить второй элемент (берем и пишем a2). У IEnumerable нельзя получить второй элемент иначе как перебором.


Думаю, нет никакой связи между тем, что пишут HumanBase (вместо Animal, например), и публичностью и непубличностью. Речь об архитектурной сложности решения. С Base, Impl, Internal, Raw и прочим ничего понятного и простого, как правило, не получается.

У вас странные правила.


Если вы имеете ввиду нечто вроде: [...]

Нет, я имел в виду что-то вроде


db.Projects.SelectMany(x => 
    x.Tasks.SelectMany(y => 
        y.Items.Select(z => new { x.Foo, y.Bar, z.Baz })
    )
)

Если вам все еще понятно что там написано внутри — добавьте ещё пару уровней.


Например, можно убрать оттуда слово Thread (блокировка потока как будто следует из WaitForOpen) и получится два типа: Gate и AutoCloseGate. Всё ещё понятнее, чем стандартный аналог.

Было: есть событие наличия элементов в очереди, и мы ждём его наступления.
Стало: есть ворота наличия элементов в очереди, и мы ждём их открытия...


Ну да, ну да, можете называть это "понятнее" если вам так нравится.

Если вам все еще понятно что там написано внутри — добавьте ещё пару уровней.

И уже не будет иметь значения, x там или orderOfSomeItemOfSomeTaskOfSomeProject. Такой код исправляется другими средствами.


Было: есть событие наличия элементов в очереди, и мы ждём его наступления.

Хм. Ранее вы писали, что Set — это "установить", а тут уже, кажется, "наступить". Также заметил несколько смутное "наличие элементов в очереди", которое никак не следует из ManualResetEvent и того, что обсуждалось ранее.


Стало: есть ворота наличия элементов в очереди, и мы ждём их открытия...

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


Как выглядит ManualResetEvent, являющийся EventWaitHandle, неясно. А ворота выглядят так:
image

несколько смутное "наличие элементов в очереди", которое никак не следует из ManualResetEvent

"Наличие элементов в очереди" — это семантика переменной, а не её типа. Тот самый случай, когда имя переменной тоже важно.


Нет, тут всё гораздо проще: есть ворота; когда они закрыты, пройти дальше нельзя; когда открыты — можно.

Но что эти ворота означают?

Вот ещё интересный вопрос. Какие названия вы дадите семафорам, мьютексам, барьерам, критическим секциям и условным переменным если ворота уже заняты?

Боюсь, мы тогда отдалимся от темы.


Мысль была вот какая: порой метафора, восходящая к опыту, понятнее и проще, чем низкоуровневая деталь. ManualResetEvent — наглядный пример.

Это не наглядный пример. Любой, кто изучал синхронизацию потоков, знает что такое ManualResetEvent, но не знает что такое Gate.
Сколько раз встречаю это мнение, что данные и исполняемый код объекта в ООП различаются. Это не так. Объекты все свое носят с собой. Данные и код. Когда вы получаете объект в руки, даже если это конкретный тип, вы не можете знать какой код отработает в методе, так же как не можете знать какие в нем данные. В этом, в общем, вся суть ООП.
И ведь не GitUtils, а IRepository, ICommit, IBranch; не ExcelHelper, а ExcelDocument, ExcelSheet; не GoogleDocsService, а GoogleDocs.


С другой стороны, «Система контроля версий», а не «Коммиты». «Текстовый редактор», а не «Тексты». «Веб-браузер», а не «Веб-страницы».

«Кофемолка», «Холодильник», «Калькулятор», «Эскалатор», «Эвакуатор» — а не «Кофе», «Продукты», «Вычисления», «Ступеньки», «Беспредельщики»
«Система контроля версий», а не «Коммиты»

Если описываете "систему контроля версий", то VersionControlSystem; если "коммиты репозитория"Commits; если "текстовый редактор"TextEditor, если "тексты" (редкий какой-то случай) — Texts; если "веб-браузер"WebBrowser, если "веб-страницы"WebPages.


На мой взгляд, всё банально, но эту банальность часто избегают с помощью всяческих VCSManager, CommitsHelper, TextEditorUtils, WebHelper и т.д., как если бы простота была чем-то преступным и недостойным.

Ну тут больше вопрос архитектуры — как вы делите программу на куски: фасадами или фабриками?


Фасады — это именно ExcelHelper, ConnectionManager, "Текстовый редактор".
Фабрики (и всякие иные подставлялки CI) — это именно ExcelSheet, IPeer, "Текст" подними перо, опусти перо.


Как именно разбивать — дело вкуса и наследия.

Ну тут больше вопрос архитектуры — как вы делите программу на куски: фасадами или фабриками?

На мой взгляд, такое разделение не совсем корректно. Возьмём выражение:


var document = new ExcelDocument();
var sheet = document.NewSheet();

Будет ли ExcelDocument фасадом или фабрикой, не имеет решительно никакого значения, поскольку главное — с помощью названия подвести к уже существующему опыту (о создании документов и таблиц в Excel многие имеют представление).


Как только мы согласились написать ExcelDocument вместо ExcelHelper, мы уже как бы предначертали его судьбу и что за методы в нём можно ожидать.

Ну да, предначертали. Не спорю. но в том то и дело, что делать из ExcelDocument'a фасад в котором собраны всё что вы хотите делать с файлами Эксель в других частях кода — не корректно по смыслу.
Документ он не ищет сам себя в файловой системе, не выдаёт список документов в папке, не открывает диалоговых окон (для сохранения). Это то что реально делал у нас класс с таким названием (ExcelHelper).

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

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

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


Мне кажется, выразительнее и понятнее:


var document = ExcelDocument.Of(path);

а не:


var document = ExcelHelper.LoadDocument(path);

Или можно ещё:


var document = file.AsFile().AsExcelDocument();

Написанное полно описывает происходящее, и не привлечены дополнительные не классы, но понятия: говоря о загрузке документа из файла, мы ограничились известными словами.


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

Ну а кто будет диалог сохранения показывать? document.SaveMeWithDialog(context)? Это ж какой годкласс на сколько тысяч строк получится? Да и зависимости левые получаются. Зачем документу знать о графической системе даже просто транзитом?

И да — метода именно загрузки не было. Загрузка шла лениво, кешировано и абсолютно незаметно (логически) для другого кода. Там почти монаду сделали. (т.е. ExcelHelper.setWorkingDir(path) и дальше там уже пошло статистическое веселье, но, конечно можно было и сделать ExcelHelper.getDocument(...) в нескольких вариантах, если хотелось)
Ну а кто будет диалог сохранения показывать?

Диалог сохранения показывает метод-обработчик кнопки сохранить.


Это ж какой годкласс на сколько тысяч строк получится? Да и зависимости левые получаются. Зачем документу знать о графической системе даже просто транзитом?

На мой взгляд, показать диалоговое окно с выбором пути для сохранения, а потом вызвать document.SaveTo(path) или document.SaveTo(stream) — вполне достаточно. Если нужно обобщить для Unit-тестов, то document.SaveTo(storage). Да и говорим мы: "Сохрани документ". Опять же, посредники и помощники — избыточны.


Попытки играться с зависимостями, перенося их туда, куда не требуется, приводит к ложному впечатлению, будто всяческие SOLID соблюдены, а код понятен и расширяем. Само наличие слова Helper — прямая дорога в технологическую сингулярность, и ваши слова про "экселевскую мусорку" это, к сожалению, подтверждают.


Там почти монаду сделали. (т.е. ExcelHelper.setWorkingDir(path)

Боюсь, это не монада...

There are only two hard things in Computer Science: cache invalidation and naming things.

И основная проблема именно в этом
Существование предшествует сущности

Не очень понял эту часть. Мне кажется, что можно было бы просто сказать «Действие (глагол) предшествует сущности (существительному)» и смысл был бы тот же, нет? Или я чего-то не замечаю?

Это лёгкая шалость в сторону экзистенциализма, там есть идея с похожим узором.


"Глагол предшествует существительному" — примерно то же самое, вы всё верно указали.

Тут, мне кажется, есть конфликт интересов тех, кто пользуется кодом и тех, кто его пишет и поддерживает.
Как пользователю API мне удобно было бы написать Directory, поставить точку и найти в автодополнении метод Clean().

Но при разработке обычно у каждого класса/модуля есть ответственный владелец (человек или команда). Было бы неприятно зайти в свой идеально причёсанный класс Directory и обнаружить там наваленную гору кода от метода Clean и его подчинённых, который написан другой командой и другим стилем, с другими соглашениями.

С другой стороны, чем меньше класс, тем легче его поддерживать. Обвешивать общий класс методами, которые нужны только для какой-то частной задачи, так себе удовольствие. Применять наследование для наращивания функциональности — CleanableDirectory=class(Directory) — ещё худший выход. Передать класс в хелпер, который есть только в том проекте, где он нужен, намного элегантнее.
> Было бы неприятно зайти в свой идеально причёсанный класс Directory и обнаружить там наваленную гору кода от метода Clean и его подчинённых, который написан другой командой и другим стилем, с другими соглашениями.

Если это будет

    void Clean() {
        DirectoryCleaner.process(this);
    }


а DirectoryCleaner это то чудо от другой команды — то почему бы нет? Просто отделегировали задачу…

проблема-то в основном остаётся, как именно переложить действие — на глаголы или существительные :)

Эта статья как раз описывает проблему.
Это плохое решение, потому что порождает зависимость некоторого общего класса (Directory) от очень частного DirectoryCleaner, который в 95% случаев нафиг в проектах не нужен.
Так с этой точки зрения всё равно есть зависимость от функциональности: если мы очистку (которая в 95% случаев не нужна) предполагаем в виде Directory.Clean, то сама функциональность присутствует в Directory.
Зато вынести её в отдельный модуль (что в таких языках делается через класс) устраняет описанную вами проблему «чужой код с чужим стилем в моём вылизанном садике», так что прогресс тут есть.

Радикально решить — чтобы и волки сыты (код никак не привязан и не должен быть даже в той же сборке, если мы продолжаем думать на примере C#), и овцы целы (можно звать как Directory.Clean) — можно решить, как я понимаю, за счёт пастуха (какой-то редирект на стадии компиляции — возможно, тут достаточно extension method, а может, и нет, если хочется даже без дополнительного using).

Ну а если реализация всё равно подключается через какую-нибудь DLL, где этот Cleaner подгрузится по необходимости — то и потери тут не большие. Всё равно ведь они будут. Например, стандартная сборка GNU libc подключает локализацию даже при main() { return 0; } потому, что часть инициализации самой библиотеки может жаловаться в stderr на проблемы старта ;(
Но при разработке обычно у каждого класса/модуля есть ответственный владелец (человек или команда). Было бы неприятно зайти в свой идеально причёсанный класс Directory и обнаружить там наваленную гору кода от метода Clean и его подчинённых, который написан другой командой и другим стилем, с другими соглашениями.

Поэтому методы расширений, которые есть в C#, или же т.н. Uniform Function Call Syntax (в некоторых других, хороших языках) позволяют добиться нужного уровня декомпозиции (разумеется, там, где не подходит объектная), сохраняя синтаксис (и семантику, надеюсь) вызова метода у экземпляра.


Передать класс в хелпер, который есть только в том проекте, где он нужен, намного элегантнее.

Поддержка хелпера хуже, чем перенасыщенный поведением класс. Когда мы говорим о Clean в Directory, то отражаем реально существующее действие очистки папки в коде, а вот в случае с хелпером — нет.


Да и раз уж говорить про статические хелперы, то зачем писать:


DirectoryHelper.CleanDirectory(path);
FileSystemUtil.CleanDirectory(path);

если можно хотя бы:


Directory.Clean(path);
Clean.Directory(path);

Ведь читается приятнее, проще, а перенасытить поведением такие штуки довольно сложно: возможно, в самой предметной области не найдётся столько слов.

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

Это не совсем верно.


Во-первых, методы расширений ограничены одним типом (если это хорошие, аккуратные методы расширений, а не очередные мусорки).
Во-вторых, методы расширений семантически идентичны вызовам методов на объекте, тогда как хелперы — вызовам статических методов с передачей объекта как параметра.

семантически идентичны вызовам методов на объекте
Мысль не закончена. Какой из этого вывод?

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

А вот это
@"C:\Windows".UpgradeOS();
выглядит как какая-то пышь-пышь магия ))
Мысль не закончена. Какой из этого вывод?

Что методы расширений не являются "теми же самыми хелперами, просто с другим синтаксисом".


А вот это
@"C:\Windows".UpgradeOS();


выглядит как какая-то пышь-пышь магия ))

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


Например:


var windows = Windows.Of(path: @"C:\Windows");

windows.Upgrade();

Разве это хуже, чем:


WindowsUtils.UpgradeFromPath(@"C:\Windows");
Разве это хуже, чем
Разумеется, хуже. Хотя бы тем, что аллоцируется объект в куче, в котором нет пока никакой необходимости.

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


Более того, бюджет производительности приложения в 99% случаях позволяет выделять столько объектов, сколько нужно, чтобы читалось хорошо.

вывод?
Что методы расширений не являются «теми же самыми хелперами
Почему нет? В IL-коде всё то же самое, лишь синтаксис другой.
И что такого дают методы расширения? Вот у обычных методов и виртуальных есть определённая разница в возможностях использования, а расширения — синтаксический сахар (они даже позволяют себя вызывать именно как статический метод класса). Просто короткий формат записи, как string.Format("{0}", a) и $"{a}" — удобство, не более.
И что такого дают методы расширения?

var a = b.As<A>();
// Не то же самое, что:
var a = Represent.As<A>(b);

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


Как видите, я рассматриваю их не с точки зрения деталей реализации, а с точки зрения кода как текста.

Sign up to leave a comment.

Articles