Код живой и мёртвый. Часть первая. Объекты

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


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


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


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


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

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

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

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

            0

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

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

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

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

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


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

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

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

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

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


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

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


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

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


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

                      0

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

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

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

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


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

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


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

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

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

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

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

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

                      +4

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


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

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

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


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

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

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


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

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


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

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


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

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

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


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

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

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

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


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

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


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

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

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


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

                                0

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


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


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

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

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


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

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


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

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

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

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

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


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


                                      var document = ExcelDocument.Of(path);

                                      а не:


                                      var document = ExcelHelper.LoadDocument(path);

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


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

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


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

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

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

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


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

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


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


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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

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


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

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


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


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

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


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

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

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

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


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

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

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

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

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


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


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

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


                                              Например:


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

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


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

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


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

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

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

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

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

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


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

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

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

                                          Самое читаемое