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


Сложно оценить героя, не поняв его "статы" и "абилки". Что он может и на что способен — вот следующий уровень сложности, на который нам придётся нырнуть. Мало с помощью точного имени отразить внутреннее святилище объекта, ещё следует убедиться, что это таки святилище, а не конюшни из геттеров.


Об этом — в статье.


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


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

Действия


Персонаж атакует, защищается, уворачивается, стреляет из лука, использует заклинания, взмахивает клинком. Имя отражает объект, но сам объект — в движении, в реакции, в действиях. В противном случае мы бы говорили о таблицах в Excel.


В C# действия — методы и функции. А для нас: глаголы, атомы словесного движения. Глаголы двигают время, из-за них объекты существуют и взаимодействуют. Где есть изменение — там должен быть глагол.


Сеттеры


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


Например, есть IPullRequest со свойством Status, которое может быть Approved, Declined или Merged. Можно писать pullRequest.Status = Status.Declined, но это то же самое, что говорить “Установи пул-реквесту отменённый статус”, — императивно. Куда сильнее — pullRequest.Decline() и, соответственно, pullRequest.Approve(), pullRequest.Merge().


Активный глагол предпочтительнее сеттера, но не все глаголы такие.


Пассивный залог


PerformPurchase, DoDelete, MakeCall.


Как в HeroManager важное существительное заслоняется бессмысленным Manager, так и в PerformMigrationPerform. Ведь живее — просто Migrate!


Активные глаголы освежают текст: не “нанёс удар”, а “ударил”; не “сделал замах”, а “замахнулся”; не “принял решение”, а “решил”. Так и в коде: PerformApplication > Apply; DoDelete > Delete; PerformPurchase > Purchase, Buy. (А вот DealDamage устоялось, хотя в редких случаях может иметься в виду Attack .)


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


Сильные глаголы


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


Так изменение здоровья игрока можно выразить через player.Health = X или player.SetHealth, но живописнее — player.RestoreHealth.


Или, например, Stack мы знаем не по Add/Remove, а по Push/Pop.


Сильные и активные глаголы насыщают объект поведением, если они не слишком конкретны.


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


Как и с ManualResetEvent, чем ближе мы подбираемся к техническим внутренностям .NET, которые сложны и хорошо бы их выразить просто, тем насыщеннее подробностями и излишествами получается API.


Бывает, нужно выполнить какую-то работу на другом потоке, но так, чтобы не хлопотать о его создании и остановке. В C# для этого есть ThreadPool. Только вот простое “выполнение работы“ тут — QueueUserWorkItem! Что за элемент работы (WorkItem) и какой он может быть, если не пользовательский (User), — неясно. Куда проще было бы — ThreadPool.Run или ThreadPool.Execute.


Другой пример. Помнить и знать, что есть атомарная инструкция compare-and-swap (CAS) — хорошо, но переносить её подчистую в код — не самое лучшее решение. Interlocked.CompareExchange(ref x, newX, oldX) во всём уступает записи Atomically.Change(ref x, from: oldX, to: newX) (с использованием именованных параметров).


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


Повторения


UsersRepository.AddUser, Benchmark.ExecuteBenchmark, AppInitializer.Initialize, UniversalMarshaller.Marshal, Logger.LogError.


Как и говорил в прошлой части, повторения размывают смысл, ужимают пространство.


Не UsersRepository.AddUser, а UsersRepository.Add; не Directory.CreateDirectory, а Directory.Create; не HttpWebResponse.GetResponseStream, а HttpWebResponse.Stream; не Logger.LogError, а Log.Error.


Мелкий сор


Check — многоликое слово. CheckHasLongName может как возвращать bool, так и бросать исключение в случае, если у пользователя слишком длинное имя. Лучше — bool HasLongName или void EnsureHasShortName. Мне даже встречался CheckRebootCounter, который… Где-то внутри перезагружал IIS!


Enumerate — из той же серии. В .NET есть метод Directory.EnumerateDirectories(path): зачем-то уточняется, что папки будут перечисляться, хотя проще ведь Directories.Of(path) или path.Directories().


Calc — так часто сокращают Calculate, хотя больше смахивает на залежи кальция.


Proc — ещё одно причудливое сокращение от Process.


Base, Impl, Internal, Raw — слова-паразиты, указывающие на переусложнённость объектов.


Итого


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


Теперь, разобравшись с движением и “спецэффектами”, посмотрим на то, как описываются отношения между объектами.


Свойства


У персонажа есть здоровье и мана; в корзине покупок находятся предметы; солнечная система состоит из планет. Объекты не только самозабвенно действуют, но и соотносятся: иерархически (предок-наследник), композиционно (целое-часть), пространственно (хранилище-элемент) и т.д.


В C# свойства и отношения — методы (как правило начинающиеся с Get), геттеры (свойства с определённым телом get) и поля. Для нас же это: слова-дополнения, выражающие принадлежность одного объекта другому. Например, у игрока есть здоровье — Player.Health, что почти точно соответствует английскому “player’s health“.


Больше всего нынче путают методы-действия и методы-свойства.


Глагол вместо существительного


GetDiscount, CalculateDamage, FetchResult, ComputeFov, CreateMap.


Отовсюду слышно устоявшееся: методы должны начинаться с глаголов. Редко встретишь, чтобы кто-то засомневался: а точно ли это так? Ведь не может быть, чтобы между Player.Health и Player.Health() была существенная разница. Пусть записи синтаксически отличаются, подразумевают они одно и то же.


Положим, в IUsersRepository легко ожидается какой-нибудь GetUser(int id). Отчего для представления пользователя додумывать какое-то получение (Get)? Аккуратнее будет — User(int id)!


И действительно: не FetchResult(), а Result(); не GetResponse(), а Response(); не CalculateDamage(), а Damage().


В одном докладе по DDD дают пример “хорошего” кода: DiscountCalculator с методом CalculateDiscountBy(int customerId). Мало того, что на лицо симметричный повтор — DiscountCalculator.CalculateDiscount, так ещё и уточнили, что скидка вычисляется. А что ещё с ней, спрашивается, делать?


Сильнее было бы пойти от самой сущности — Discount с методом static decimal Of(Customer customer, Order order), чтобы вызывать Discount.Of(customer, order) — проще, чем _discountCalculator.CalculateDiscountBy(customerId), и соответствует единому языку.


Иногда же, опустив глагол, мы кое-что теряем, как, скажем, в CreateMap(): прямой замены на Map() может быть мало. Тогда лучшее решение — NewMap(): снова во главе объект, а не действие.


Использование пустопорожних глаголов свойственно устаревшей, императивной культуре, где алгоритм первичен и стоит впереди понятия. Там чаще встретишь “клинок, который закалили”, чем “закалённый клинок”. Но стиль из книг про Джеймса Бонда не подходит для описания пейзажа. Где нет движения, там глаголу не место.


Другое


Свойства и методы, выражающие отношения между объектами, — тоже объекты, поэтому сказанное выше во многом относится и к ним.


Например, повторения в свойствах: не Thread.CurrentThread, а Thread.Current; не Inventory.InventoryItems, а Inventory.Items, и т.д.


Итого


Простые, понятные слова не путают, и поэтому код, состоящий из них, также не путает. В писательском мастерстве не менее важно писать легко: избегать пассивных залогов, обилия наречий и прилагательных, повторений, для действий предпочитать глагол существительному. Общеизвестный пример: “Он кивнул своей головой, соглашаясь” вместо “Он кивнул” вызывает улыбку, и вспоминается QueueUserWorkItem.


Текст от кода отличается ещё тем, что в первом случае вам заплатят, если дом стоит, утопая в лучах заходящего солнца; во втором — если дом стоит; но стоит помнить: стоять должен дом, а не палки из хелперов.


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


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

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


  1. alex1t
    11.04.2019 12:44

    Всё это уже ведь написано в хорошей книге «Чистый код» (Clean Code) Роберта Мартина


    1. JoshuaLight Автор
      11.04.2019 14:42

      Книга отличная!


      Но этот цикл про несколько иную идею, хотя и пересекается с некоторыми разделами книги.


      1. alex1t
        11.04.2019 14:43

        Да, есть. Просто читаю статью и сразу вспоминаю книгу практически на каждом абзаце. Кто не читал книгу может полезно будет :)


  1. dopusteam
    11.04.2019 13:29

    Почему IUsersRepository.User(int Id) вместо IUsersRepository.Get(int Id) по аналогии с add?


    1. JoshuaLight Автор
      11.04.2019 14:34

      Потому что Get — это глагол, а никакого значимого действия мы указывать не хотим, только то, что вот пользователь с идентификатором ID.


      Но и User — не предел. Я улучшу это имя, развив ряд соображений из этой статьи, в статье следующей.


      1. dopusteam
        11.04.2019 16:16

        Мне кажется немного притянуто получается.

        Почему же

        а никакого значимого действия мы указывать не хотим
        ?

        Я хочу именно что указать, что мне нужно получить пользователя, разве нет?

        Просто в Вашем случае интерфейс получается примерно такой
        Add()
        Update()
        User()

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

        Но посмотрим, что будет в следующей статье


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

          Я хочу именно что указать, что мне нужно получить пользователя, разве нет?

          В том то и дело, что вот это "получить" — лишнее, ненужное. Получить пользователя и пользователь подразумевают одно и то же (пользователя), только первое зачем-то что-то уточняет.


          1. dopusteam
            11.04.2019 19:54
            +3

            Но что в итоге? IUsersRepository.User vs IUsersRepository.Get


            Дублирование vs практически предложение со смыслом


            1. JoshuaLight Автор
              12.04.2019 17:27

              IUsersRepository.One!


      1. mayorovp
        12.04.2019 15:18

        А почему вы решили не указывать никакого значимого действия, когда оно есть?

        Загрузка объекта из хранилища — тоже действие, притом дорогое (по сравнению с работой с объектами в памяти).


        1. JoshuaLight Автор
          12.04.2019 17:36
          -1

          Загрузка объекта из хранилища — тоже действие, притом дорогое (по сравнению с работой с объектами в памяти).

          Загрузка — это Load, а не Get.


          Кстати, даже тут можно уйти от Load. По контексту хранилища обычно понятно, будет это дорогостоящая операция или нет. Например, database.User(id) — мы видим, что обращение User происходит на объекте базы, поэтому понимаем: операция дорогостоящая.


          Кроме этого, если контракт (интерфейс) обобщает способ получения и, вообще, конкретные детали, то уточнение стоимости получения — нарушение инкапсуляции.


  1. Ketovdk
    11.04.2019 14:25

    Сильнее было бы пойти от самой сущности — Discount с методом static decimal Of(Customer customer, Order order), чтобы вызывать Discount.Of(customer, order) — проще, чем _discountCalculator.CalculateDiscountBy(customerId)

    А как этот статический класс инджектить в IoC — container? DDD часто используется в вебе, где без этого никуда (да и не только в вебе без этого никуда)


    1. JoshuaLight Автор
      11.04.2019 14:44

      Если нужно инжектить, то пожалуйста:


      public interface IDiscount
      {
          decimal Of(Customer customer, Order order);
      }

      Вызов тогда:


      var discount = _discount.Of(customer, order);
      // Но никак не:
      var discount = _discountCalculator.CalculateDiscountBy(customerId, order);

      Суть осталась та же, изменилась только форма.


      1. ksbes
        11.04.2019 15:49

        Ну а если нам нужно ещё и устанавливать/сбрасывать скидку + плюс вычислять не только скидку, но и, скажем, НДС со скидкой?

        Т.е. в достаточно сложном коде появляются односмысленные (но принципиально разные) действия (вычисление/установка и т.д.) над разнотипными объектами. А если сверху это всё и иерархией с дженериками накрыть — так вообще уже через несколько часов сам автор перестанет понимать .of() чего он делает.

        А так у нас есть CalculatDiscount, CalculateNDS (о, ужас, я знаю — просто не помню как это по английски — тоже проблема, кстати), CalculateTotalSum и т.д. И уже код читается почти как английский текст и от точек в глазах не рябит.


        1. JoshuaLight Автор
          11.04.2019 16:13

          Я бы не хотел отходить от темы, размышляя "а что если". Боюсь, тогда мы тут надолго застрянем.


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


          Кстати, если взять CalculateDiscount(), CalculateNDS() и CalculateTotalSum(), то проще — Discount(), NDS() и TotalSum(). Что оно Calculate, и так понятно.


          1. ksbes
            11.04.2019 16:26

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

            К тому же английский язык бывает коварен. TotalSum() может и понятен, а вот просто Sum() — уже нет. Это глагол (меняющий внутреннее состояние объекта) или существительное? Тоже самое: Add это совсем не то же самое что Push (в первом случае непонятно что в конец).

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


            1. JoshuaLight Автор
              11.04.2019 16:57

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

              Да, существуют ситуации, когда уже есть какие-то переменные или поля с такими же именами. Но и их можно обойти, всё зависит от конкретного случая, главное понимать, зачем глагол, а зачем существительное. В случае с Calculate глагол уж точно ни к чему.


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

              В C# для методов существует такое же правило. Интересно, что если метод, то Get (или какой-нибудь Calculate) пишется, а вот если свойство (синтаксически это метод без параметров и без скобочек) — то нет. Как если бы от физического воплощения (свойство или метод) одного и того же понятия мы бы по разному говорили о нём между собой.


              Думается мне, критерий всегда один: говорят "скидка", значит Discount, и т.д., тут в комментарии ниже описывал.


              Код тогда строится не по действиям, императивно, а описательно: не "скидка считается считателем скидок по формуле", а "скидка — это...". Разумеется, там где действие, там остаётся царствие глагола.


              1. Simplifier
                12.04.2019 11:45

                Интересно, что если метод, то Get (или какой-нибудь Calculate) пишется, а вот если свойство (синтаксически это метод без параметров и без скобочек) — то нет. Как если бы от физического воплощения (свойство или метод) одного и того же понятия мы бы по разному говорили о нём между собой

                Ну да, это потому что в случае свойства действие (гет или сет) уже передается синтаксисом и дублировать его словесно не надо. order.Discount = 5 — свойство слева от присваивания, поэтому сет.
                К тому же у метода множество различных возможных вариантов действия, а у свойства только два, поэтому нет неоднозначности, в отличие от метода


                1. JoshuaLight Автор
                  12.04.2019 11:47

                  "гет" — это не действие.


                  Действие это или нет, различается не синтаксически и формально (метод или свойство), а семантически: скидка это, пользователь из базы, запись, добавление.


                  1. mayorovp
                    12.04.2019 15:19

                    Но в хорошей программе синтаксис отражает семантику.


      1. Ketovdk
        11.04.2019 16:07

        Ну, название интерфейса IDiscount очевидно не хорошее, с точки зрения естественного языка, так как не передает совершаемого им действия (ну он не Я скидка, а Я считаю скидку), так-что в конечном итоге его бы пришлось назвать IDiscountCalculator или ICalcDiscount и вернуться ко второму варианту, потому-что _discountCalculator.Of(customer) опять же не правильно с точки зрения языка. Потому-что если перевести, то получается, что он должен возвращать инстанс какого-то калькулятора для конкретного пользователя, а не считать скидку


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

          Ну, название интерфейса IDiscount очевидно не хорошее, с точки зрения естественного языка, так как не передает совершаемого им действия

          Совершаемое действие не имеет значения. Имеет значения результирующее понятие.


          1. qw1
            11.04.2019 22:50

            Потом окажется, что Discount это не числовое значение, а сущность с несколькими полями. Захочешь назвать её «Discount», а это имя уже занято сервисом-калькулятором.


            1. JoshuaLight Автор
              12.04.2019 11:36

              Думаю, просто следует проявить изобретательность.


              public interface IDiscount
              {
                  (decimal Value, decimal AdditionalValue) Of(Customer customer, Order order);
              }

              Всегда можно поиграться и найти то, что подходит. Если на каждое улучшение находится "если", это не значит, что нужно вообще ничего не улучшать и писать повсюду: _discountCalculator.CalculateDiscount (ещё утверждается, что это DDD).


              1. qw1
                12.04.2019 20:00

                Нафиг-нафиг. Кортежи, безымянные классы — всё это тяжко рефакторить.

                Попробуйте поискать FindUsages такого AdditionalValue — найдутся обращения к Item2 всех кортежей.


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

                  Попробуйте поискать FindUsages такого AdditionalValue — найдутся обращения к Item2 всех кортежей.

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


                  Изначальный посыл: использовать понятия из предметной области, избегать повторений. В предметной области вычислителей скидок нет, а _discountCalculator.CalculateDiscount содержит слова discount и calculator по два раза. Поэтому нужно думать.


                  Кстати, есть ещё вариант:


                  public interface IDiscount
                  {
                      void Apply(Customer customer, Order order);
                  }

                  Но и тут можно придраться к тому, менять Order нельзя. Как и к тому, что скидка с несколькими полями — вещь сомнительная. Разумеется, Apply можно переделать на Apply(ref price, Customer customer, Order order), но и тут можно найти минусы.


                  Без контекста все эти конкретные размышления парируются каким-нибудь "а что если", при этом изначальный посыл от этого не меняется.


                  1. qw1
                    13.04.2019 00:54

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

                    Как и к тому, что скидка с несколькими полями — вещь сомнительная
                    Вовсе нет, потому что типичная скидка в сложных системах — алгебраический тип-сумма, может быть либо процентной, либо абсолютной. Также неплохо бы хранить основание для скидки (постоянному клиенту, или по акции №55).


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

                      После модификации Order этим методом теряется информация, сколько заказ стоил до скидки

                      Зависит от реализации.


                      Тут и вырисовывается DiscountManager как сервис, и Discount как сущности, с которыми он работает.

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


                      Кстати, вот ещё в голову пришёл вариант:


                      public interface IDiscounts
                      {
                          IDiscount One(Customer for, Order and);
                      }
                      
                      public interface IDiscount
                      {
                          public int Value { get; }
                          public DiscountType Type { get; }
                          public DateTime WillBeAvailableTo { get; } 
                      }
                      
                      // Вызываем так:
                      Order order = ...;
                      Customer customer = ...;
                      IDiscounts discounts = ...; // Инжектим и т.д.
                      
                      var price = Price(of: order); // Как-то считается цена.
                      var discount = discounts.One(for: customer, and: order);
                      
                      discount.Apply(ref price); // `Apply` или внутрь `IDiscount`, или методом расширений.

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


                      1. qw1
                        13.04.2019 11:10

                        Кстати, вот ещё в голову пришёл вариант:
                        Ну, замечательно. Вы изобрели DiscountService/DiscountManager под именем Discounts.

                        Если вы собираетесь отстаивать своё наименование, то представьте, что ваш коллега Вася из соседнего отдела напилил кучу интерфейсов: IDiscount, IOrder, IOrders, IShop. Что из них репозиторий, что из них сущность, что сервис — непонятно.

                        Можно экономить на названиях, но через 3 года сам же откроешь свой код, и будешь вспоминать, почему IOrders — это репозиторий, а IDiscount — сервис с методом
                        void Apply(Customer customer, Order order);

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

                        У вас получается, что скидка должна знать про каждый канал продаж, появление нового канала потребует внесения изменений в код Discount. Это нормально, когда пилишь сам себе простой проект, в котором сам всё контролируешь. Но плохо для промышленной разработки.


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

                          Ну, замечательно. Вы изобрели DiscountService/DiscountManager под именем Discounts.

                          Имя — это всё.


                          Что из них репозиторий, что из них сущность, что сервис — непонятно.

                          Это избыточные детали. Приложение-то по работе с заказами, а не с сервисами заказов.


                          Можно экономить на названиях

                          Но ведь это не экономия на названиях, а использование слов из предметной области.


                          Можно экономить на названиях, но через 3 года сам же откроешь свой код, и будешь вспоминать, почему IOrders — это репозиторий, а IDiscount — сервис с методом
                          void Apply(Customer customer, Order order);

                          Единственное, что можно подумать: IDiscount — это скидка, а IOrders — заказы.


                          Такой подход убивает абстракцию.

                          Боюсь, DiscountManager — это не абстракция...


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

                          Это новое "если".


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

                          Если я верно вас понял, вы предлагаете переиспользовать "лишь информацию о скидке". А если в каждом из каналов скидки выражаются разными данными? А если где-то выдаётся только процентная скидка? Если она не всегда ограничена по времени?


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


                          1. qw1
                            13.04.2019 11:53

                            А если в каждом из каналов скидки выражаются разными данными? А если где-то выдаётся только процентная скидка? Если она не всегда ограничена по времени?
                            Придётся данные скидки привести к общему знаменателю, под максимальный случай. Ведь, как это часто бывает, захотят видеть агрегированную статистику, а агрегировать можно однородные данные.


    1. alex1t
      11.04.2019 14:46

      Я тоже где-то читал про читаемость кода и там ещё дальше развивали эту идею, делая кучу extension-методов для базовых типов, чтобы можно было написать что-то вроде Thread.Sleep(5.Minutes())


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

        Как раз об этом и будет заключительная статья!


      1. qw1
        11.04.2019 22:55

        5.Minutes()
        Как финтифлюшка в HelloWorld забавно смотрится, но не масштабируется для большого проекта.

        Потому как в DispatcherTimer(5.Minutes()) Minutes должен вернуть TimeSpan, а в контексте, когда нужны тики (FileTime) — Minutes должен вернуть тоже целое число, но не в миллисекундах, а в других единицах, сотнях наносекунд. И что с этим Minutes делать, когда единственное применение намертво прибито к типу int?


        1. alex1t
          11.04.2019 23:04

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


        1. JoshuaLight Автор
          12.04.2019 11:25

          Как финтифлюшка в HelloWorld забавно смотрится, но не масштабируется для большого проекта.

          Хм. Сперва постулируется: "не масштабируется для большого проекта", а затем как подтверждение — довольно частный пример, который может касаться лишь одной из подсистем этого самого большого проекта.


          Кроме этого, надо понимать, 5.Minutes() работает там, где работает, а там, где не работает, ищется другое.


          Положим, описываю я дебаффы в игре:


          Debuff(CriticalChanceDecrease, power: 10, duration: 5.Minutes());

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


          1. qw1
            12.04.2019 20:02

            Не масштабируется в том смысле, что этот подход нельзя перенести на остальные подсистемы, потому что int.Minutes() уже занято какой-то одной.

            Не получается единообразия.


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

              что этот подход нельзя перенести на остальные подсистемы

              Зачем это может быть нужно? Каждая подсистема имеет свой ограниченный контекст. Жертвовать читаемостью кода из-за того, что он не масштабируется на разработку операционных систем для ракетостроения, — не самое лучшее решение.


              1. qw1
                13.04.2019 01:00

                Рано или поздно захотят интегрировать подсистемы, и придётся в одном модуле оперировать сущностями из разных подсистем. И тут экономия в детализации наименований выйдет боком.


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

                  Мне кажется, такие рассуждения о том, что будет или чего не будет, как правило, не заканчиваются успехом. А код читается сейчас.


  1. eugene_bx
    11.04.2019 18:34

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


    1. AlekseyArh
      12.04.2019 11:27

      Подтверждаю. Я один из тех программистов, который считает что язык программирования и литературный язык нужно разделять на корню. Простой пример, который доставлял бы мне дискомфорт в программировании если бы английский язык был бы для меня родной. Door->open() vs Open->door().


      1. JoshuaLight Автор
        12.04.2019 11:31

        Простой пример, который доставлял бы мне дискомфорт в программировании если бы английский язык был бы для меня родной. Door->open() vs Open->door().

        Да, в ООП предполагается такое незначительное нарушение грамматики, но из него никак не следует, что нужно идти в диаметрально противоположное направление и писать хелперы.


    1. JoshuaLight Автор
      12.04.2019 11:29

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


      Разве что, случается (ведь я сам не носитель): некоторые слова кажутся вычурными и неестественными, потому что редко их встречаешь или до этого не знал.


  1. Wyrd
    11.04.2019 22:37
    +3

    QueueWorkItem vs Execute — имхо, первое намного лучше: оно даёт понимание того, что ThreadPool исполнит задачу «когда-нибудь», не обязательно сейчас. Execute такого понимания не даёт и это достаточно больная проблема, имхо


    1. JoshuaLight Автор
      12.04.2019 11:27

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


      Никогда бы не подумал что-то вызвать на тредпуле, не понимая, что это в действительности значит: что там есть потоки, они управляются, что не нужно их надолго задерживать. Тип ThreadPool сообщает вполне достаточно.


      Но аргумент понятен. Как вам: ThreadPool.Queue(action)? Зачем WorkItem? Что это значит?


      1. Wyrd
        12.04.2019 12:10

        WorkItem это объект доменной модели если можно так выразится. Например, в приложениях под IIS WorkItem не просто становится в очередь и выполняется когда-то, но и также не даёт IIS pool уйти в recycle до тех пор пока он не выполниться до конца. Да это не очевидно, но в MSDN про это пишут.


        1. Wyrd
          12.04.2019 12:11

          • выполнится


      1. Wyrd
        12.04.2019 20:47

        Вообще, в целом я с Вами согласен (особенно надеюсь на статью код-как-английский-язык),


        но! ИМХО Вы слишком категоричны, рассмотрите варианты типа threadPool.QueueWorkItem(() => {… }) — вполне возможно что внутри скобочек не
        будет слова action => неискушённому взгляду не будет понятно что там ставят в очередь…


        1. JoshuaLight Автор
          13.04.2019 00:02

          Вообще, в целом я с Вами согласен (особенно надеюсь на статью код-как-английский-язык)

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


          но! ИМХО Вы слишком категоричны

          Того требует форма. Если бы на каждый пример я дописывал "вообще, бывают вот такие исключения, а вот в этих ситуациях всё-таки лучше так, а ещё случается...". Категоричность как раз была целью, поскольку её различаемость выше, чем у средних рассуждений (не в ущерб оным).


          рассмотрите варианты типа threadPool.QueueWorkItem(() => {… }) — вполне возможно что внутри скобочек не
          будет слова action => неискушённому взгляду не будет понятно что там ставят в очередь…

          И всё же, давайте представим сценарий. Разработчик, имеющий представление о потоках и о том, как они управляются в пуле (а это почти обязательное требование, чтобы им пользоваться), смотрит на запись ThreadPool.Run( () => { ... } ) и такой думает: "Не, бред какой-то. Что тут происходит?". Потом встречает ThreadPool.QueueUserWorkItem, и тотчас восклицает: "А, так это ж UserWorkItem! Теперь ясно!".


          Боюсь, разработчики просто привыкают к QueueUserWorkItem, и для них это просто alias выражения "Вызвать метод на тредпуле".


          1. qw1
            13.04.2019 01:02

            Боюсь, разработчики просто привыкают к QueueUserWorkItem, и для них это просто alias выражения «Вызвать метод на тредпуле»
            Просто обманывать не надо. Если после завершения метода Run фактически выполнение не началось — это наименование врёт.


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

              Просто обманывать не надо

              Вы используете слово выполнить без контекста, тогда как в действительности это будет выполнить на тредпуле. Выполнение чего-то на тредпуле имеет другую семантику, чем просто выполнение чего-то. Почему бы тогда не писать ThreadPool.FindFreeThreadAndExecuteUserWorkItem(action)? А то QueueUserWorkItem не сообщает о том, что выполнение действия будет совершено только тогда, когда в тредпуле есть свободный поток.


              Если после завершения метода Run фактически выполнение не началось — это наименование врёт.

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


              1. qw1
                13.04.2019 11:18

                Почему бы тогда не писать ThreadPool.FindFreeThreadAndExecuteUserWorkItem(action)?
                Потому что это неверно. Метод не ищет свободный поток. Он добавляет в очередь, и на этом его ответственность заканчивается. Если в имя метода добавить «Find», может показаться, что от наличия свободного потока зависит результат (если такого нет, метод вернёт false, например).


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

                  Потому что это неверно. Метод не ищет свободный поток.

                  Это потому что вы знаете, как устроен ThreadPool внутри. А метод, меж тем, не должен сообщать, что он делает внутри.


                  Мне, как клиенту ThreadPool.QueueUserWorkItem совершенно безразлично, будет там внутри очередь или Scheduler. У меня есть действие, и я хочу выполнить его на потоке из пула. Поэтому: "Пул, подыщи-ка мне поток, и выполни эту задачу" — вот вам и FindFreeThreadAndExecuteUserWorkItem получился.


                  Если в имя метода добавить «Find», может показаться, что от наличия свободного потока зависит результат (если такого нет, метод вернёт false, например).

                  Если прочитать только Find, но там есть ещё AndExecute.


                  В общем, на мой взгляд, всё это несущественные споры. Основная претензия была не Queue (хотя и к нему есть), а к UserWorkItem.


                  Как вам ThreadPool.Queue?


                  1. qw1
                    13.04.2019 12:35

                    Если прочитать только Find, но там есть ещё AndExecute.
                    «And» можно понять так, что действия выполняются последовательно. Если Find не успешен, Execute не выполняется.

                    Как вам ThreadPool.Queue?
                    Для данного класса — подходит, потому что ThreadPool с другими сущностями не работает. Но Microsoft можно понять: есть классы (например, Directory), которые работают с разными сущностями. Поэтому не просто Directory.Enumerate, а EnumerateFiles и EnumerateDirectories, и вообще, глагол+существительное как стандарт.

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


                    1. JoshuaLight Автор
                      13.04.2019 14:12

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

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


                      Поэтому не просто Directory.Enumerate, а EnumerateFiles и EnumerateDirectories, и вообще, глагол+существительное как стандарт.

                      Зачем так, если можно хотя бы Directories.Of(path) и Files.Of(path)? Глагол не нужен совершенно. При этом лучше вообще использовать методы расширений, чтобы передать "субъектный" оттенок: path.AsDirectory().Files и path.AsDirectory().Directories. Тогда и расширяемость выше.


                  1. netch80
                    13.04.2019 21:27

                    Тогда уж лучше ThreadPool.Push().
                    IMHO, вызывает только желательные коннотации и в достаточном количестве.


                    1. qw1
                      14.04.2019 01:10

                      У push есть ассоциация со стеком, а он LIFO. Queue же FIFO.


                      1. netch80
                        14.04.2019 08:35

                        > У push есть ассоциация со стеком, а он LIFO.

                        Не согласен. Ассоциация только у тех, для кого английский не родной и кто работал только со стеком.
                        В C++, например, у deque есть push_front, push_back, pop_front, pop_back, а у queue только push (в конец) и pop (из начала). То есть уже понимание не такое, как вам кажется.
                        У vector и многих прочих — только push_back.
                        Тот, кто их знает, уже не считает, что push всегда положить туда, откуда его же возьмёт pop.
                        А у нейтива вообще понимание, что push может быть какой угодно и где угодно, и понимать его надо по смыслу области применения.


  1. Yoooriii
    12.04.2019 02:13
    +1

    Вставлю свои 2 цента. Меня забавляет префикс Custom, например: CustomView, CustomController, CustomModel. Берем стандартный класс, и не особо думая добавляем Custom. Следующий наследник — это Custom Custom и так далее. Я встречал проекты, где чуть ли не половина классов называлась Custom. Реже встречается префикс My (MyClass, MyView). Этими обычно начинающие балуются, пока до Custom не дойдут (это уже считай уровень мидл). А вот у сеньеров с фантазией полный порядок. Например: BreadCrumps или BouncyCastle.


    1. KaiOvas
      12.04.2019 17:29

      Более того — начинающие разработчики часто, начитавшись каких-либо обучающих статей, лепят в свой код префиксы «Boo, Baz, Foo, Bar» которыми любят авторы статей именовать незначительные для повествования классы, методы и т.п. конструкции и в итоге потом жутко обижаются на то что их код заворачивается на код ревью с комментариями — переписать с вменяемыми названиями. Вот никогда не понимал, почему вставлять эти паразитные названия в демонстрационный код статьи? Ну если уж пишите статью то придумайте нормальные названия классов, интерфейсов, методов и потрудитесь не делать «Interface IBaz1» или «class FooBar». Это просто неуважение к читателю.


      1. mayorovp
        12.04.2019 18:27

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