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


И вместе с этим мы видим повсеместную эпидемию менеджеров, хелперов, сервисов, контроллеров, селекторов, адаптеров, геттеров, сеттеров и другой нечисти: всё это мёртвый код. Он сковывает и загромождает.


Бороться предлагаю вот как: нужно представлять программы как текст на естественном языке и оценивать их соответственно. Как это и что получается — в статье.


Оглавление цикла


  1. Объекты
  2. Действия и свойства
  3. Код как текст

Пролог


Мой опыт скромный (около четырёх лет), но чем больше работаю, тем сильнее понимаю: если программа нечитаемая, толку от неё нет. Давно уже известно и избито напоминать — код не только решает какую-то задачу сейчас, но ещё и потом: поддерживается, расширяется, правится. При этом он всегда: читается.


Ведь это текст.


Эстетика кода как текста — ключевая тема цикла. Эстетика тут — стёклышко, через которое мы смотрим на вещи и говорим, да, это хорошо, да, это красиво.


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


Нам повезло, программы почти полностью состоят из слов.


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


Но различать классы от структур, поля от свойств и методы от функций я не буду: персонаж как часть повествования не зависит от технических деталей (что его можно представить или ссылочным, или значимым типом). Существенно другое: что это персонаж и что назвали его Hero (или Character), а не HeroData или HeroUtils.


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


Объекты


В C# (и не только) объекты — экземпляры классов, которые размещаются в куче, живут там некоторое время, а затем сборщик мусора их удаляет. Ещё это могут быть созданные структуры на стеке или ассоциативные массивы, или что-нибудь ещё. Для нас же они: имена классов, существительные.


Имена в коде, как и имена вообще, могут запутывать. Да и редко встретишь некрасивое название, но красивый объект. Особенно, если это Manager.


Менеджер вместо объекта


UserService, AccountManager, DamageUtils, MathHelper, GraphicsManager, GameManager, VectorUtil.


Тут главенствует не точность и осязаемость, а нечто смутное, уходящее куда-то в туман. Для таких имён многое позволительно.


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


Или, случается, нужно работать с фейсбуком. Почему бы не складывать весь код в одно место: FacebookManager или FacebookService? Звучит соблазнительно просто, но столь размытое намерение порождает столь же размытое решение. При этом мы знаем: в фейсбуке есть пользователи, друзья, сообщения, группы, музыка, интересы и т.д. Слов хватает!


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


И ведь не GitUtils, а IRepository, ICommit, IBranch; не ExcelHelper, а ExcelDocument, ExcelSheet; не GoogleDocsService, а GoogleDocs.


Всякая предметная область наполнена объектами. “Предметы обозначились огромными пустотами”, “Сердце бешено колотилось”, “Дом стоял” — объекты действуют, чувствуются, их легко представить; они где-то тут, осязаемые и плотные.


Вместе с этим подчас видишь такое: в репозитории Microsoft/calculatorCalculatorManager с методами: SetPrimaryDisplay, MaxDigitsReached, SetParentDisplayText, OnHistoryItemAdded


(Ещё, помню, как-то увидел UtilsManager...)


Бывает и так: хочется расширить тип List<> новым поведением, и рождаются ListUtils или ListHelper. В таком случае лучше и точнее использовать только методы расширения — ListExtensions: они — часть понятия, а не свалка из процедур.


Одно из немногих исключений — OfficeManager как должность.


В остальном же… Программы не должны компилироваться, если в них есть такие слова.


Действие вместо объекта


IProcessor, ILoader, ISelector, IFilter, IProvider, ISetter, ICreator, IOpener, IHandler; IEnableable, IInitializable, IUpdatable, ICloneable, IDrawable, ILoadable, IOpenable, ISettable, IConvertible.


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


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


Хороший сюжет рассказывает о персонажах и их развитии, а не о том, как создатель создаёт, строитель строит а рисователь рисует. Действие не может в полной мере представлять объект. ListSorter это не SortedList.


Возьмём, к примеру, DirectoryCleaner — объект, занимающийся очисткой папок в файловой системе. Элегантно ли? Но мы никогда не говорим: “Попроси очистителя папок почистить D:/Test”, всегда: “Почисти D:/Test”, поэтому Directory с методом Clean смотрится естественнее и ближе.


Интереснее более живой случай: FileSystemWatcher из .NET — наблюдатель за файловой системой, сообщающий об изменениях. Но зачем целый наблюдатель, если изменения сами могут сообщить о том, что они случились? Более того, они должны быть неразрывно связаны с файлом или папкой, поэтому их также следовало бы поместить в Directory или File (свойством Changes с возможностью вызвать file.Changes.OnNext(action)).


Такие отглагольные имена как будто оправдывает шаблон проектирования Strategy, предписывающий “инкапсулировать семейство алгоритмов”. Но если вместо “семейства алгоритмов” найти объект подлинный, существующий в повествовании, мы увидим, что стратегия — всего лишь обобщение.


Чтобы объяснить эти и многие другие ошибки, обратимся к философии.


Существование предшествует сущности


MethodInfo, ItemData, AttackOutput, CreationStrategy, StringBuilder, SomethingWrapper, LogBehaviour.


Такие имена объединяет одно: их бытие основано на частностях.


Бывает, решить задачу быстро что-то мешает: чего-то нет или есть, но не то. Тогда думаешь: "Мне бы сейчас помогла штука, которая умеет делать X" — так мыслится существование. Затем для "делания" X пишется XImpl — так появляется сущность.


Поэтому вместо IArrayItem чаще встречается IIndexedItem или IItemWithIndex, или, скажем, в Reflection API вместо метода (Method) мы видим только информацию о нём (MethodInfo).


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


Например, захотели менять значение типа string без создания промежуточных экземпляров — получилось прямолинейное решение в виде StringBuilder, тогда как, на мой взгляд, уместнее — MutableString.


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


Если хочется описать способ взятия лока, то необязательно уточнять, что это ILockBehaviour или ILockStrategy, куда проще — ILock (с методом Acquire, возвращающим IDisposable) или ICriticalSectionEnter).


Сюда же — всяческие Data, Info, Output, Input, Args, Params (реже State) — объекты, напрочь лишённые поведения, потому что рассматривались однобоко.


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


Причудливая таксономия


CalculatorImpl, AbstractHero, ConcreteThing, CharacterBase.


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


Ведь разве бывает человек (Human) — наследник базового человека (HumanBase)? А как это, когда Item наследует AbstractItem?


Бывает, хотят показать, что есть не Character, а некое "сырое" подобие — CharacterRaw.


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


Повторения


Со вложенными типами бывает такое: RepositoryItem — в Repository, WindowState — в Window, HeroBuilder — в Hero.


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


Избыточные детали


Для синхронизации потоков нередко используется ManualResetEvent с таким API:


public class ManualResetEvent
{
    // Все методы — часть `EventWaitHandle`.
    void Set();
    void Reset();
    bool WaitOne();
}

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


В таких случаях проще использовать далёкие от программирования (но близкие к повседневности) метафоры:


public class ThreadGate
{
    void Open();
    void Close();
    bool WaitForOpen();
}

Тут уж точно ничего не перепутаешь!


Иногда доходит до смешного: уточняют, что предметы — не просто Items, а обязательно ItemsList или ItemsDictionary!


Впрочем, если ItemsList не смешно, то AbstractInterceptorDrivenBeanDefinitionDecorator из Spring — вполне. Слова в этом имени — лоскуты, из которых сшито исполинское чудище. Хотя… Если это чудище, то что тогда — HasThisTypePatternTriedToSneakInSomeGenericOrParameterizedTypePatternMatchingStuffAnywhereVisitor? Надеюсь, legacy.


Кроме имён классов и интерфейсов, часто встречаешь избыточность и в переменных или полях.


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


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


Итого


Признаюсь, с таким началом найти объективную правду будет непросто. Кто-то, например, скажет: писать Service или не писать — вопрос спорный, несущественный, вкусовщина, да и какая вообще разница, если работает?


Но и из одноразовых стаканчиков можно пить!


Я убеждён: путь к содержанию лежит через форму, и если на мысль не смотрят, её как бы и нет. В тексте программы всё работает так же: стиль, атмосфера и ритм помогают выразиться не путано, а понятно и ёмко.


Имя объекта — не только его лицо, но и бытие, самость. Оно определяет, будет он бесплотным или насыщенным, абстрактным или настоящим, сухим или оживлённым. Меняется имя — меняется содержание.


В следующей статье поговорим об этом самом содержании и о том, какое оно бывает.

Комментарии (55)


  1. JustDont
    10.04.2019 14:03

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

    Действительно, хорошие имена — это всего лишь один, но зато огромный шаг к поддерживаемости кода.


  1. MonkAlex
    10.04.2019 14:31

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

    ПС: есть на эту тему «шутка» о всего двух проблемах программирования — именовании и инвалидации кеша =)


    1. ksbes
      10.04.2019 14:44

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


    1. JoshuaLight Автор
      10.04.2019 17:39

      А иногда что писать — знаешь, а как описать — нет. Это что же, долго перебирать варианты как описать, чтобы решить как назвать, чтобы наконец то написать?

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


  1. groaner
    10.04.2019 15:58

    Если вдруг кому-нибудь интересно — Егор Бугаенко много пишет на эту тему. Например, https://www.yegor256.com/2015/03/09/objects-end-with-er.html


    1. MonkAlex
      10.04.2019 16:16

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


      1. JoshuaLight Автор
        10.04.2019 20:01

        Разница между 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();

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


        1. rjhdby
          11.04.2019 11:26

          `Sort` — это не то, что должен уметь делать `List`, это то, что некто посторонний может сделать с `List`. Именно сортировщий сортирует набор элементов, а не сами они выстраиваются по росту.


  1. maslyaev
    10.04.2019 16:23
    -1

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


    1. ksbes
      10.04.2019 16:45

      Респонсобилити — понятие расплывчатое и контекстнозависимое. В контексте небольших программ или библиотек с десятком классов — это вполне сингл респонобилити, если объект все действия делает только в рамках себя самого и не лезет в «чужие» предметные области


      1. maslyaev
        10.04.2019 17:16
        -1

        Расплывчатое понимание догмы и контекстнозависимое её применение — это Вы какую-то бездуховность сейчас проповедуете. Прямо фашизм какой-то.


    1. JoshuaLight Автор
      10.04.2019 20:17

      Буква «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. maslyaev
        10.04.2019 22:08
        +1

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

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

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


        1. JoshuaLight Автор
          10.04.2019 22:25
          -1

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


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

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


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

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


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


      1. An70ni
        11.04.2019 16:36

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


  1. megahertz
    10.04.2019 16:48

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

    Примеры с DirectoryCleaner/Directory.Clean и file.Changes.OnNext могут работать в некоторых случаях, но в других могут порождать GodObject с кучей отвественностей и зависимостей. Хотя конечно, все от случая зависит. Я заметил, что такие упрощения имен допустимо делать только в зрелом коде, который уже не так часто меняется, при условии что изменение API не создаст проблем. Но если изначально писать код в таком стиле, то это создаст больше проблем чем принесет пользы.


    1. JoshuaLight Автор
      10.04.2019 17:49

      В примере orders вместо orderRepository есть и плюсы и минусы

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


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

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


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

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


      1. mayorovp
        10.04.2019 19:13

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

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


        1. JoshuaLight Автор
          10.04.2019 19:35

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

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


  1. mayorovp
    10.04.2019 17:21

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


    Так, большинство 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 или нет, а ведь это важная информация.


    1. JoshuaLight Автор
      10.04.2019 18:24

      Так, большинство 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. Всё ещё понятнее, чем стандартный аналог.


      1. mayorovp
        10.04.2019 19:11
        +1

        Проблемы есть, ведь как понятия 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. Всё ещё понятнее, чем стандартный аналог.

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


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


        1. JoshuaLight Автор
          10.04.2019 19:52

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

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


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

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


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

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


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


          1. mayorovp
            10.04.2019 20:18

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

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


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

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


          1. mayorovp
            10.04.2019 20:53

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


            1. JoshuaLight Автор
              10.04.2019 21:30

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


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


              1. mayorovp
                10.04.2019 22:05
                +1

                Это не наглядный пример. Любой, кто изучал синхронизацию потоков, знает что такое ManualResetEvent, но не знает что такое Gate.


  1. aikixd
    10.04.2019 18:11

    Сколько раз встречаю это мнение, что данные и исполняемый код объекта в ООП различаются. Это не так. Объекты все свое носят с собой. Данные и код. Когда вы получаете объект в руки, даже если это конкретный тип, вы не можете знать какой код отработает в методе, так же как не можете знать какие в нем данные. В этом, в общем, вся суть ООП.


  1. pankraty
    11.04.2019 11:22

    И ведь не GitUtils, а IRepository, ICommit, IBranch; не ExcelHelper, а ExcelDocument, ExcelSheet; не GoogleDocsService, а GoogleDocs.


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

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


    1. JoshuaLight Автор
      11.04.2019 12:08

      «Система контроля версий», а не «Коммиты»

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


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


    1. ksbes
      11.04.2019 12:09

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


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


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


      1. JoshuaLight Автор
        11.04.2019 12:18

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

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


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

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


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


        1. ksbes
          11.04.2019 12:44

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

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

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


          1. JoshuaLight Автор
            11.04.2019 12:49

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

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


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


            var document = ExcelDocument.Of(path);

            а не:


            var document = ExcelHelper.LoadDocument(path);

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


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

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


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


            1. ksbes
              11.04.2019 13:04

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

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


              1. JoshuaLight Автор
                11.04.2019 15:36

                Ну а кто будет диалог сохранения показывать?

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


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

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


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


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

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


  1. eefadeev
    11.04.2019 11:56

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

    И основная проблема именно в этом


  1. questor
    11.04.2019 14:40

    Существование предшествует сущности

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


    1. JoshuaLight Автор
      11.04.2019 15:21

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


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


  1. qw1
    11.04.2019 17:46
    +1

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

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

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


    1. netch80
      13.04.2019 21:13

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

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

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


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

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

      Эта статья как раз описывает проблему.


      1. qw1
        14.04.2019 01:09

        Это плохое решение, потому что порождает зависимость некоторого общего класса (Directory) от очень частного DirectoryCleaner, который в 95% случаев нафиг в проектах не нужен.


        1. netch80
          14.04.2019 08:28

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

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

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


  1. JoshuaLight Автор
    11.04.2019 18:01

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

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


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

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


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


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

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


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

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


    1. qw1
      11.04.2019 19:06

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


      1. JoshuaLight Автор
        12.04.2019 11:42
        -1

        Это тот же самый хелпер, просто с другим синтаксисом.

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


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


        1. qw1
          12.04.2019 19:49

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

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

          А вот это
          @"C:\Windows".UpgradeOS();
          выглядит как какая-то пышь-пышь магия ))


          1. JoshuaLight Автор
            12.04.2019 23:39

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

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


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


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

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


            Например:


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

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


            WindowsUtils.UpgradeFromPath(@"C:\Windows");


            1. qw1
              13.04.2019 00:46

              Разве это хуже, чем
              Разумеется, хуже. Хотя бы тем, что аллоцируется объект в куче, в котором нет пока никакой необходимости.


              1. JoshuaLight Автор
                13.04.2019 10:01

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


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


            1. qw1
              13.04.2019 00:48

              вывод?
              Что методы расширений не являются «теми же самыми хелперами
              Почему нет? В IL-коде всё то же самое, лишь синтаксис другой.


              1. JoshuaLight Автор
                13.04.2019 10:06

                Но ведь читаем мы С#.


                1. qw1
                  13.04.2019 10:55

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


                  1. JoshuaLight Автор
                    13.04.2019 11:03

                    И что такого дают методы расширения?

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

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


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


  1. netch80
    13.04.2019 21:15

    Execution in the Kingdom of Nouns ещё тут не вспоминали?