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

Итак, кому же? В первую очередь, наверное, таким же как я — новичкам в области проектирования программных систем. Тем, кто не обладает колоссальным эмпирическим опытом и владеет шаблонами проектирования исключительно на основании общих рассуждений. Ещё более эффективным будет прочтение такой статьи тем, кто ни разу не слышал про SOLID, GRASP и прочие принципы проектирования. Ибо я искренне уповаю на то, что мне удастся показать, как из базовых теоретических суждений на основании законов логики выводятся все те непоколебимые постулаты, ранее казавшиеся a priori истинными.

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

Предисловие


Почему же стал на такой путь и пишу о столь фундаментальной, скорее всего всеми давно понятой, теме?

Несколько причин.

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

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

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

Кто же я такой? Меня зовут Джош, я из Харькова, мне 22, и я всё ещё Junior Software Developer. Наверное. Примерно год назад я уже публиковался на хабре, и на тот момент мои размышления на тему компонентно-ориентированного движка на C# были встречены не так плохо, как ожидал. Более того, публикация вырвалась из песочницы и какое-то время набирала просмотры. Но это так, знакомства ради, с которым я и без того затянул.

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

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

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

Что же, надеюсь, я не утомил читателя столь долгим введением, и, пожалуй, приступим.

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

Структура


Метафора 1. Иерархично всё. Вселенную можно рассматривать как набор уровней различной степени приближения: кварки, атомы, молекулы, вещества, клетки, ткани… Не секрет, что само человеческое мышление принимает иерархичную форму, когда модули нижнего уровня складывают модули верхнего уровня, а потому вопрос о том, иерархична ли Вселенная или, всё-таки, человек, я оставлю в качестве философского упражнения пытливому читателю, ибо подобные размышления не касаются основной линии сюжета.

Понятие 1.1. Задача — требование к функциональности приложения.
Понятие 1.2. Блок — код, сосредоточенный вокруг выполнения одной и только одной задачи.
Понятие 1.3. Зависимость — использование одним блоком кода другого.
Понятие 1.4. Степень приближения — количество уровней, на которые необходимо подняться от атомарного, чтобы достигнуть данного.
Понятие 1.5. Абстракция — блок, не имеющий определённой реализации на этапе компиляции.

Функция 1.1. Apr(x) — степень приближения x.
Функция 1.2. Qd(х) — количество зависимостей блока х.

Положение 1.1. С течением времени количество блоков, из которых состоит программная система, увеличивается.
Положение 1.2. Атомарным для программной системы является уровень базовых операторов и ключевых слов.
Положение 1.3. Чем выше степень приближения абстракции, т.е. чем более общую задачу она призвана решать, тем меньше вероятность того, что появятся изменения.
Положение 1.3.1. Зависимость от абстракции имеет меньшую вероятность привести к косвенным изменениям.
Положение 1.3.1.1. Абстракции понижают энтропию.
Положение 1.4. Избыточность порождает изменения.

Процессы


Понятие 2.1. Создание — увеличение количества блоков в приложении путём написания нового кода.
Понятие 2.2. Изменение — отображение изменения формулировки задачи на блоки.
Понятие 2.3. Косвенное изменение — отображение изменения блока на зависимые от оного.
Понятие 2.4. Корректность — количественная характеристика проверки. Показывает, насколько точно и полно работает блок относительно выдвинутых пред- и постусловий.
Понятие 2.5. Энтропия — количественная характеристика качества кода, показывающая, сколько дополнительного бюджета потребуется на внедрение нового функционала. Выражается через отношение между средним временем изменения и средним временем создания блоков.

Функция 2.1. Tc(х, y) — время создания блока х в рамках задачи y.
Функция 2.2. Tu(х, y) — время изменения блока х в рамках задачи y.
Функция 2.3. Qu(х, у) — кол-во изменений блока х в рамках задачи у.
Функция 2.4. Qm(х, у) — кол-во кос. изменений блока х в рамках задачи у.
Функция 2.5. Md(x) — отображение из множества косвенных изменений блока x в множество тех, что приведут к реальным.
Функция 2.6. Cor(x) — показывает степень корректности блока x, т.е. отношение между теоретическим результатом и фактическим. Можно формально определить как отношение количества элементов множества, формирующегося путём пересечения результатов работы ожидаемой функции с фактической, к количеству элементов множества результатов работы ожидаемой функции.
Функция 2.7. Ku(х) — коэффициент хрупкости блока х. Отношение между количеством Md(х) к количеству косвенных изменений x.

Положение 2.1. Новый код увеличивает энтропию.
Положение 2.2. Изменения увеличивают энтропию.
Положение 2.3. Косвенные изменения косвенно снижают корректность.
Положение 2.3.1. Косвенные изменения могут привести к не косвенным.
Положение 2.3.1.1. Косвенные изменения косвенно увеличивают энтропию.
Положение 2.3.2. Проверочные блоки снижают степень влияния косвенных изменений на корректность.

Организация


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

Понятие 3.1. Переиспользование — использование одного и того же блока для решения одной и той же задачи во всех местах приложения.
Понятие 3.2. Полиморфизм блоков — возможность подставлять блок реализации в абстракцию.
Понятие 3.3. Наследование — переиспользование дочерним блоком структуры родительского.
Понятие 3.4. Инкапсуляция — сокрытие внутренней структуры блока от блоков, его использующих.

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

SOLID


Напоследок я хочу взять на себя смелость теоретически обосновать применение наиболее популярных принципов — SOLID.

Single Responsibility Principle — принцип единой ответственности. Формально звучит так: программный модуль или класс должен иметь только ответственность только за одну функциональную часть, предоставляемую приложением. У него должна быть “только одна причина для изменения” (Роберт Мартин). В нашей теоретической модели это напрямую вытекает из определения зависимости: как уже было показано выше, состояние, когда логически один блок верхнего уровня содержит два блока нижнего уровня, выполняющих разные задачи, но зависящие друг от друга, имеет большую энтропию, чем состояние без циклической зависимости.

Open/Closed Principle — принцип открытости/закрытости. Кратко: изменение поведения сущности должно производиться не за счёт модификации её исходного кода, а за счёт расширения, под которым подразумеваются специфичные механизмы вроде наследования, полиморфизма и абстракций. В свете вышеизложенных построений можно сказать, что время внедрения новой функциональности составляет только время создания и никак не время изменения, что, таким образом значительно уменьшает энтропию.

Liskov Substitution Principle — принцип подстановки Барбары Лисков. Говорит о том, что должна существовать возможность заменить все объекты типа T на объекты типа S, где S — подтип T, без ущерба корректности и работоспособности программы. На формальном языке это можно выразить так: пусть функция f(x) справедлива для всех x типа T, тогда функция f(y) должна быть также справедливой для всех y типа S, где S — подтип T. Данный принцип является следствием стремления уменьшить коэффициент хрупкости приложения и, к сожалению, не даёт никаких конкретных рекомендаций, а лишь постулирует требование к программной системе.

Interface Segregation Principle — принцип разделения интерфейсов. Объявляет, что большое количество мелких интерфейсов лучше, чем один большой, т.к. клиенты, зависящие от интерфейсов, могут пользоваться только той их частью, которая им нужна.
Могу заметить, что такой принцип является продолжением SRP, прикладываемым на область абстракций. Аргументы и доказательства абсолютно те же.

Dependency Inversion Principle — принцип инверсии зависимостей. Для меня один из самых труднопонимаемых принципов, гласящий, что объекты высокого уровня не должны зависеть от объектов низкого уровня, и наоборот — оба уровня должны зависеть от абстракций. Логичность и истинность данного принципа напрямую следует из Положения 1.3.1.1: вероятность изменения абстракции ниже, чем вероятность изменения конкретного блока-реализатора.

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

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

Практика


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

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

public class DamageMediator : GameComponent
{
  public int Next()
  {
    var equipment = GetEquipment();
    var stats = GetStats();

    var weapon = equipment.Weapon;

    var damage = weapon.Damage;
    var isCrit = stats.CriticalChance.Next();

    var result = isCrit ? damage.Next() + damage.Next() : damage.Next();

    return result;
  }
}

Посчитаем количество зависимостей.

Блоки-функции: GetEquipment, GetStats, Equipment.Weapon, Weapon.Damage, Stats.CriticalChance, CriticalChance.Next, Damage.Next.
Блоки-типы: Equipment, Stats, Weapon, Damage, Chance.
Таким образом Qd = 12.

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

1. Избавимся от GetEquipment и GetStats, перенеся их в параметры.

public class DamageMediator : GameComponent
{
  public int Next(Equipment equipment, Stats stats)
  {
     var weapon = equipment.Weapon;

     var damage = weapon.Damage;
     var isCrit = stats.CriticalChance.Next();

     var result = isCrit ? damage.Next() + damage.Next() : damage.Next();

     return result;
  }
}

2. Заменим Equipment на Weapon.

public class DamageMediator : GameComponent
{
  public int Next(Weapon weapon, Stats stats)
  {
     var damage = weapon.Damage;
     var isCrit = stats.CriticalChance.Next();

     var result = isCrit ? damage.Next() + damage.Next() : damage.Next();

     return result;
  }
}

3. Заменим Weapon на Damage.

public class DamageMediator : GameComponent
{
  public int Next(Damage damage, Stats stats)
  {
     var isCrit = stats.CriticalChance.Next();

     var result = isCrit ? damage.Next() + damage.Next() : damage.Next();

     return result;
  }
}

4. Заменим Stats на Chance.

public class DamageMediator : GameComponent
{
  public int Next(Damage damage, Chance criticalChance)
  {
     var isCrit = criticalChance.Next();
     
     return isCrit ? damage.Next() + damage.Next() : damage.Next();
  }
}

Таким образом теперь состояние блока: Qd = 4.

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

Во-первых, уменьшая количество зависимостей, мы временно избавились от двух, казалось бы, непримечательных методов: GetEquipment и GetStats. Тем лучше для текущего примера — можно будет более детально рассмотреть возникающие проблемы. Оказывается, данные методы получали экземпляры экипировки и характеристик персонажа, используя систему компонентов: сам DamageMediator является GameComponent и по соглашению имеет доступ к ссылке на родительский GameComponentContainer (прошу меня простить, что приходится это выслушивать, но в моей первой статье есть разъяснения), соответственно, первоначальная версия кода предполагала, что компоненты экипировки и характеристик будут также находиться в контейнере.

Во-вторых, на самом деле просчёт урона не ограничивается броском кости на критический удар. Что, если теперь появилось (на самом деле, было) условие: на окончательное значение будет влиять одна из базовых характеристик, например, сила или ловкость, причём это влияние будет определяться особенностью оружия?

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


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

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

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

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

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

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

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

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

public class DamageMediator : GameComponent
{
  // Возвращаем первоначальное API.
  public int Next()
  {
    // Вернули два старых метода.
    var equipment = GetEquipment();
    var stats = GetStats();

    return Next(equipment.Weapon.Damage, stats.CriticalChance);
  }

  private static int Next(Damage damage, Chance criticalChance)
  {
     var isCrit = criticalChance.Next();
     
     return isCrit ? damage.Next() + damage.Next() : damage.Next();
  }
}

Что же, в конце концов, поменялось? Стало быть, энтропия, потому как количество зависимостей уменьшилось: если ранее существовала циклическая зависимость между блоком просчёта урона и блоком получения компонентов, сейчас осталась лишь зависимость от блока получения компонентов к блоку расчёта урона. Кроме того, уменьшилась вероятность изменения блока просчёта урона, т.к. его количество зависимостей, как помните, составляет Qd = 4.

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

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

public class DamageMediator : GameComponent
{
  public int Next()
  {
    var equipment = GetEquipment();
    var stats = GetStats();

    return DamageUtil.Next(equipment.Weapon.Damage, stats.CriticalChance);
  }
}

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

Итоги


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

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

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

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

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

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

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

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

Всем спасибо за внимание и до скорых встреч!
Поделиться с друзьями
-->

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


  1. Marsikus
    06.01.2017 00:09
    +1

    Меня зовут Джош, я из Харькова

    Вас действительно так зовут, или вы сами себя так называете? Для Харькова немного необычно.


    1. JoshuaLight
      06.01.2017 01:29
      +1

      Забавно, но да!


  1. bobermaniac
    06.01.2017 00:37
    +2

    А вот эти Apr(x) и Qd(x) как-нибудь вычисляются, или их наличие обусловлено исключительно флером научности?


    1. JoshuaLight
      06.01.2017 01:30

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

      Как считаете, если удалю, никто не расстроится?)


      1. lair
        06.01.2017 01:42

        А как вы можете вычислить Apr(x)?


        1. JoshuaLight
          06.01.2017 02:11

          public class Test
          {
            public void ThirdLevel()
            {
              SecondLevel();
            }
          
            public void SecondLevel()
            {
              FirstLevel();
            }
          
            public void FirstLevel()
            {
              Console.WriteLine("1");
            }
          }
          

          Из Положения 1.2. и определения Степени приближения следует, что Apr(ThirdLevel) = 3.

          Зачем это нужно? В работе не было явно указано и я не проводил прямых расчётов, но, скажем, данная величина показывает, на каком уровне находится блок, а следовательно, она является модификатором вероятности того, что данный блок изменится. Это утверждение напрямую следует из определения (хотя не такого точного, как я уже вижу, за что сразу спасибо), т.к. изменение на любом из уровней приведёт к косвенному изменению уровня ThirdLevel (если в контексте примера) и т.д.


          1. lair
            06.01.2017 02:17

            Из Положения 1.2. и определения Степени приближения следует, что Apr(ThirdLevel) = 3.

            … это если считать, что "уровень" — это глубина вызова, а Console.WriteLine — атомарная операция.


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


            Попробуйте заменить Console.WriteLine на _writer.WriteLine, где _writer — поле типа TextWriter, и посчитать вашу метрику снова.


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

            А вот и нет. Если SecondLevel — это публично определенный API, то вероятность изменения ThirdLevel складывается из вероятности изменения требований к ThirdLevel и вероятности изменения API SecondLevel, и вам совершенно не важно, сколько уровней внутри SecondLevel (собственно, вы этого и не знаете). Да здравствует инкапсуляция.


            1. JoshuaLight
              06.01.2017 03:08

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


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

              А вот и нет. Если SecondLevel — это публично определенный API, то вероятность изменения ThirdLevel складывается из вероятности изменения требований к ThirdLevel и вероятности изменения API SecondLevel, и вам совершенно не важно, сколько уровней внутри SecondLevel (собственно, вы этого и не знаете). Да здравствует инкапсуляция.


              И ещё раз прошу прощения, но что такое «публично определённый API»? Если я верно догадываюсь, это метод, для которого определены входные и выходные данные, скажем, метод складывания двух чисел с параметрами x и y должен возвращать x + y, и совершенно неважно, как именно внутри он считает.
              Здесь я буду вынужден парировать ваш аргумент тем, что, независимо от этого, любое изменение (если таковое будет) внутри метода складывания двух чисел изменит косвенно все методы, его использующие, если не гарантировать, что даже после изменений для x и y метод всё ещё возвращает x + y.
              Подобную гарантию может дать только тестирование заявленного методом API. Если тестирования нет, значит всякое изменение внутреннего устройства на любом из N уровней, которые его разделяют от атомарного (или от данного) приведёт к косвенным изменениям. Напомню, что косвенные изменения — это не настоящие изменения, но могут стать таковыми.
              Говоря более простым языком, мы не можем гарантировать, что после того, как я поправлю одну-две строчки кода в каком-то методе, не отвалится ни одно из M мест по проекту.
              Здесь могу добавить, что, если речь идёт о библиотеке или нативной платформе, то вероятность того, что там что-то изменится — крайне низка.
              Возможно, я слегка не так вас понял, за что заранее прошу прощения.

              К слову, каш аргумент также касается и абстракций. Если SecondLevel — это абстракция, то вероятность считается также (у абстракций вообще нет уровней), т.е. вероятность изменения ThirdLevel равна вероятности изменения требований (спасибо за слово, запамятовал его как раз) и вероятность изменения абстракции (у которой есть только API).


              1. lair
                06.01.2017 03:20

                И ещё раз прошу прощения, но что такое «публично определённый API»?

                Эмм, вы не знаете, что такое публичный API/контракт, но рассуждаете о LSP?


                Если упрощать, то контракт — это то поведение, которое пользователь (программист) ожидает от кода.


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

                Ну так метод, который говорит, что складывает x и y, и должен складывать x и y. Если вы будете это нарушать, работать с системой будет невозможно.


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

                … говорят, еще есть формальное доказательство корректности программ. Еще говорят, что есть рантайм-верификация пре- и пост-условий контрактов. И так далее. Но да, еще есть юнит-тестирование, которое ровно для этого и придумано.


                После этого каждый публичный API имеет для вас "степень приближения" 0, что, в общем-то, сводит метрику к бессмысленной.


                у абстракций вообще нет уровней

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


                1. JoshuaLight
                  06.01.2017 04:14

                  После этого каждый публичный API имеет для вас «степень приближения» 0, что, в общем-то, сводит метрику к бессмысленной.


                  Почему же? Если вы мне предоставляете API, которое гарантировано выполняет то, что обещает, и при этом все его изменения скрыты и их влияние нивелировано — спасибо, это отличное API. Буду пользоваться дальше и расскажу друзьям.)))

                  Тут идея в том, что сама степень приближений осталась той же: вероятность изменения N-го уровня всё ещё базируется на сумме вероятностей предшествующих ему, однако путём, как вы сказали, объявления публичного API, т.е. наложения контракта, а также написания Unit-теста, мы сводим вероятность того, что изменения некоторых уровней (для которых, следовательно, были произведены упомянутые операции) произойдут, к нулю. Можно заключить, что таким образом действия, которые сводят такую вероятность к нулю, т.е., скажем, написание Unit-тестов, логически обоснованы.

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

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

                  Согласен, ошибка в определении. Спасибо, поправлю.


                  1. lair
                    06.01.2017 04:21

                    Почему же?

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


                    Тут идея в том, что сама степень приближений осталась той же: вероятность изменения N-го уровня всё ещё базируется на сумме вероятностей предшествующих ему

                    Мне интересно, вы МакКоннела читали? Про сокрытие сложности?


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

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


                    Мне становится интересно, а как вы вообще видите применение LSP в системе, где код не удовлетворяет своим контрактам? Или использование абстрактных зависимостей?


                    1. JoshuaLight
                      06.01.2017 05:40

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

                      Я вас прекрасно понял. Вы правы — для корректной оценки понадобится знать о том, сколько действительно существует уровней между x и y, что в принципе невозможно (да и не нужно), особенно в тех случаях, когда речь идёт о методе какой-нибудь библиотеки, в который упираешься, а на самом деле там сокрыто N слоев.
                      Но над чем я сейчас размышляю, так это над тем, что независимо от того, знаю я о количестве слоёв или нет, вероятность изменения моего кода всё равно будет равна сумме вероятностей изменения каждого из слоев. Попробую пояснить на том же примере:
                      public class Test
                      {
                        public void ThirdLevel()
                        {
                          SecondLevel();
                        }
                      
                        public void SecondLevel()
                        {
                          FirstLevel();
                        }
                      
                        public void FirstLevel()
                        {
                          Console.WriteLine("1");
                        }
                      }
                      


                      Предположим, что Console.WriteLine — это API не от Microsoft, а метод библиотеки, над которой постоянно ведётся работа. Также предположим, что данный метод имеет 40 слоёв. Скажем, первым слоем идёт какой-нибудь нативный вызов, затем он оборачивается в некую абстракцию с одним контрактом, потом ещё во что-то, и так далее. Даже не смотря на то, что я не могу посмотреть и узнать о том, что там 40 слоев, не означает ли это, что тем не менее, после выхода новой версии библиотеки, существует немаленькая вероятность того, что мой код сломается, т.к. изменения, происходящие на 40 (а если 400?) слоях (даже, если я о них не знаю) самой разной степени общности, рано или поздно приведут к нарушению корректности?

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

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

                      Однако, если всё же допустить возможность отсутствия Unit-тестов и вернуться к примеру: я так понимаю, что вы утверждаете, что потенциальное несоответствие между контрактом и фактической работой метода Console.WriteLine — это проблема самого метода и API, которую должны решать его разработчики, верно? Тут я вас прекрасно понимаю, всё верно.

                      А если предположить, что Console.WriteLine — это метод из моего проекта, который наворачивает базовый .NET API в уйму различных абстракций самого разного сорта, каждая из которых обещает выполнять все наложенные контракты, но ввиду постоянных изменений, не подкреплённых Unit-тестами, периодически что-то где-то отваливается (сейчас уже больше веду речь о реальной практике, а не вымышленных примерах)?

                      Мне интересно, вы МакКоннела читали? Про сокрытие сложности?

                      Как видно, нет. Раз вы спросили и это было упомянуто в беседе — значит, стало быть, надо. Добавлю в очередь.


                      1. lair
                        06.01.2017 12:02

                        вероятность изменения моего кода всё равно будет равна сумме вероятностей изменения каждого из слоев

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


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

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


                        (и, заметим, весь этот наш разговор мило и незаметно нарушает SRP)


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


                        (начался этот разговор, напомню, с того, что ваше Apr(x) — неизмеримо)


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

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


                        А если предположить, что Console.WriteLine — это метод из моего проекта, который наворачивает базовый .NET API в уйму различных абстракций самого разного сорта, каждая из которых обещает выполнять все наложенные контракты, но ввиду постоянных изменений, не подкреплённых Unit-тестами, периодически что-то где-то отваливается?

                        … то у вас плохой процесс разработки. О качестве вашего кода по этому описанию сказать нельзя вообще ничего.


                        1. JoshuaLight
                          06.01.2017 18:14

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


                          Согласен. Но известность или неизвестность никак не влияет на то, что происходит в действительности. Хотите верьте, хотите нет, но почти всё, что вы видите состоит из пустого пространства. Ваше знание или незнание этого факта *никак* не сделает реальность иной. Таким образом знание или незнание метрики степени приближения определённого метода не повлияет на вероятность изменения.

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

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

                          Но, скажем, как бы я, всё же, пользовался данной метрикой в реальном проекте? Заранее предположим, что это очень плохой проект. Я вижу метод, который пользуется некоторым A для совершения некоторой операции. Метод A определён в этом же проекте и, глядя на его код, я нахожу, что он пользуется B для совершения операции ещё более низкого уровня (низкой степени приближения). Таким образом я опускаюсь на N уровней, когда ниже уже некуда: остались только List, да прочие библиотечные классы, вероятность изменения которых, как мы уже заключили из первого абзаца, посчитать невозможно.

                          Что я могу заключить об этом коде? Пока ничего. Но я уже точно знаю, что вероятность изменения метода, с которого был начат анализ, будет складываться из вероятностей изменения низлежащих методов (A, B и т.д.).

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

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

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

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

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


                          1. lair
                            06.01.2017 18:27

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

                            И что вам дало это знание?


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

                            Опять-таки, что вам даст эта вероятность?


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

                            Каким же образом?


                            Еще раз, non-actionable metrics бесполезны.


                            1. JoshuaLight
                              07.01.2017 17:36

                              И что вам дало это знание?

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


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


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


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


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


                              1. lair
                                07.01.2017 19:53

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

                                "вероятность изменения метода"… количественно выражает "вероятность того, что он сломается в конце концов."


                                Очень полезная метрика. Очень.


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


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


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


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


                                Ну и как вы будете считать вашу метрику теперь?


                                Вот и получается, что предлагаемая вами метрика (а) невычислима и (б) не имеет конкретных критериев и реакций. Ну и зачем она такая нужна?


                                Мало того, что это вообще полезное теоретическое знание, ибо без него той полноты картины, какой требуется мною изначально

                                Требуется зачем?


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

                                Эмм. Вы уверены, что они именно следуют, однозначным и очевидным способом? Продемонстрируйте.


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

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


                                1. JoshuaLight
                                  11.01.2017 01:08
                                  +1

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


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


  1. lair
    06.01.2017 00:45
    +2

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

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


    Сначала посмотрим на сигнатуру класса: DamageMediator : GameComponent. Зачем нужно это наследование? Какую пользу оно приносит?


    Теперь посмотрим на сигнатуру метода: public int Next(). Эм. Что делает этот метод? Почему он возвращает число? Какая у него семантика?


    Наконец, посмотрим внутрь:


    var equipment = GetEquipment();
    var stats = GetStats();

    Кажется, методы GetEquipment и GetStats — унаследованные от базового класса. Того самого, который GameComponent. У компонента игры есть шмот и статы? Выглядит странно. Или это методы, которые смотрят в какой-то контекст, где есть какой-то "текущий игрок", у которого есть шмот и статы? Но тогда это не очевидно из названия.


    var weapon = equipment.Weapon;
    var damage = weapon.Damage;

    … а если никакого оружия сейчас нет? А если, наоборот, есть два оружия? Почему, GetEquipment — метод, но Weapon — свойство?


    var isCrit = stats.CriticalChance.Next();

    … и снова этот магический Next, который на этот раз, судя по всему, возвращает bool.


    (я даже не спрашиваю, почему криты зависят от статов напрямую, а не через оружие)


    Избавимся от GetEquipment и GetStats, перенеся их в параметры.

    … количество зависимостей в этот момент измениться не должно.


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


    1. MonkAlex
      06.01.2017 01:06

      Да ладно, предыдущие статьи по Unity которые недавно мелькали на хабре нам сообщают, что надо экономить прям на foreach и свойствах. А вы про DDD.

      Другое дело, что код стал только сложнее в результате рефакторинга, что очень подозрительно.


      1. lair
        06.01.2017 01:41

        Да статья-то, вроде бы, и не о Unity.


        1. MonkAlex
          06.01.2017 07:49

          Да, немного промахнулся, GameComponent оказывается из XNA.


      1. JoshuaLight
        06.01.2017 01:56

        Другое дело, что код стал только сложнее в результате рефакторинга, что очень подозрительно.


        Можете, пожалуйста, разъяснить, что именно подразумеваете под «сложнее» и как оцениваете?


        1. MonkAlex
          06.01.2017 07:54
          +1

          Оцениваю легко в данном случае.

          Был один класс с кодом, в котором вычислялась так или иначе логика урона.
          После рефакторинга, их стало два. Более того, никто не гарантирует, что над входными параметрами статического метода DamageUtil.Next не «подшаманит» внешний слой, который логику опять разделит.

          Если у юнита урон х2 — это изменение пойдет в *Util или в компоненту конкретную? Не очевидно. Непонятно. Раньше в код расчета урона хотя бы одна точка входа была, теперь их две.


    1. JoshuaLight
      06.01.2017 01:54

      Спасибо большое за подробный комментарий! Сейчас постараюсь дать ответ по различным пунктам.

      1.

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

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

      2.
      Сначала посмотрим на сигнатуру класса: DamageMediator: GameComponent. Зачем нужно это наследование? Какую пользу оно приносит?


      Здесь уже точно подмечен главный недочёт примера: не объяснено более детально, что именно значит «быть GameComponent». На самом деле, это не «компонент игры», а «игровой компонент», что семантически означает «компонент, находящийся в контексте игры». Связано это с тем, что существует сущность Component, которую оный расширяет. Собственно, такие детали в рамках рассматриваемого примера я решил опустить.

      3.
      Теперь посмотрим на сигнатуру метода: public int Next(). Эм. Что делает этот метод? Почему он возвращает число? Какая у него семантика?


      Здесь вы также ловко указали на мой недочёт. На самом деле, я руководствовался простой логикой: методы класса Random имеют наименование Next, что означает, что каждый раз будет генерироваться отличное от предшествующего число. Вот, собственно, и я воспользовался той же нехитрой концепцией, полагая, что это не вызовет вопросов. Увы.

      4.
      Кажется, методы GetEquipment и GetStats — унаследованные от базового класса. Того самого, который GameComponent. У компонента игры есть шмот и статы? Выглядит странно. Или это методы, которые смотрят в какой-то контекст, где есть какой-то «текущий игрок», у которого есть шмот и статы? Но тогда это не очевидно из названия.


      Здесь я тоже попал в ловушку упрощения примера: эти методы на самом деле являются чисто декларативными абстракциями и в реальном коде представляют нечто вроде
      var equipment = Container.Get<Equipment>();
      

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

      5.
      … количество зависимостей в этот момент измениться не должно.

      Использование метода GetEquipment или GetStats я называю зависимостью (полагая, что это верно), т.к. любое изменение данных методов косвенно изменит использующий. Например, изменение типа выходного параметра может привести к ошибке компиляции или реальному изменению метода Next.
      Вынося их в параметры, я уничтожаю две зависимости, но, как потом показываю, не решаю проблему того, что данные сущности надо откуда-то доставать.
      Впоследствии я пытаюсь показать, как запутанный двузадачный метод распутывается.

      6.
      wtf-метрику кода
      прошу прощения за вопрос, но что это за понятие?

      7. Я полностью согласен с вашим комментарием по поводу дизайна, однако хочу отметить, что
      если он хорошо сделан
      — неопределённое понятие с точки зрения логики. Более формально и точно определить подобные неопределённые человеческие абстракции вроде «красивый», «хороший» и было моей основной целью, по крайней мере, так мне это виделось.

      Ещё раз спасибо за комментарий, я многое почерпнул из него. Буду рад вашему ответу. Особенно интересует, что вы думаете по поводу непосредственно формализации: имеет ли право на жизнь подобная идея? Можно ли пытаться количественно оценивать код, полностью опуская своё человеческое начало или же это невозможно? Когда я спросил то же самое у Майкла Фезерса, указывая ему, что его «красивый код» — не будет таким же, как мой «красивый код», он сказал, что так не выйдет, и что из всех наиболее формальных систем можно назвать принципы SOLID, где даже вводятся некоторые характеристики вроде связности.


      1. lair
        06.01.2017 02:07

        полностью разделяю компонентную систему и бизнес-логику.

        И где же ваша бизнес-логика оказалась?


        На самом деле, это не «компонент игры», а «игровой компонент», что семантически означает «компонент, находящийся в контексте игры».

        … неявный контекст — зло. Большое. Оно, кстати, и приносит вам кучу проблем.


        в реальном коде представляют нечто вроде var equipment = Container.Get<Equipment>();

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


        Использование метода GetEquipment или GetStats я называю зависимостью (полагая, что это верно), т.к. любое изменение данных методов косвенно изменит использующий. [...] Вынося их в параметры, я уничтожаю две зависимости, но, как потом показываю, не решаю проблему того, что данные сущности надо откуда-то доставать.

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


        прошу прощения за вопрос, но что это за понятие?

        WTF-метрика — это количество восклицаний "what the f***?!", произнесенных за время чтения кода.


        Более формально и точно определить подобные неопределённые человеческие абстракции вроде «красивый», «хороший» и было моей основной целью, по крайней мере, так мне это виделось.

        Формальных определений этим понятиям нет, и это к счастью.


        Особенно интересует, что вы думаете по поводу непосредственно формализации: имеет ли право на жизнь подобная идея?

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


        Можно ли пытаться количественно оценивать код, полностью опуская своё человеческое начало или же это невозможно?

        Это не нужно. Именно потому, кстати, что читают код люди.


        1. JoshuaLight
          06.01.2017 02:45

          И где же ваша бизнес-логика оказалась?

          Первоначально класс DamageMediator занимался просчётом урона, а также отвечал за контейнеры и компоненты. После изменений (к слову, до написания статьи и проведённого в ней анализа, я искренне верил, что он в относительном порядке) код, ответственный за просчёт урона (простая математическая функция от нескольких переменных) был полностью вынесен за пределы DamageMediator. Это можно называть разделением бизнес-логики и компонентной системы?

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

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

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

          Тут, пожалуй, не соглашусь, и парирую ваш аргумент тем, что, технически, когда я вынес вызов двух методов из метода Next, а результат работы этих методов вынес в параметры, количество зависимостей внутри этого метода уменьшилось, потому что теперь он не зависит от той магии, которая происходила в этих методах. Как мне кажется, это важный момент, который нельзя упускать. То, что сам метод до сих пор зависит от типов Equipment и Stats — это осталось. Другое дело, что такое изменение не имеет смысла, если другой метод (уже не переделанный Next) не вызовет GetEquipment и GetStats сам и не воспользуется переделанным Next для генерации результата (что и получилось на примере). Существует принципиальное различие между вариантом, когда метод Next зависит от GetEquipment и GetStats, и когда не зависит.

          Формальных определений этим понятиям нет, и это к счастью.

          Тут я имел ввиду не «красивый» вообще, а «красивый код», т.е. что именно мы подразумеваем под «красивый код»? Насколько «красивый код» отличается от «выгодного бизнесу кода» (исходя из количественной оценки определённого набора параметров). Скажем, бизнесу было бы очень выгодно, если бы добавление новой функциональности (как я уже это заметил в статье) заняло не 20 минут, 10 из которых было потрачено на то, чтобы подстроить текущую негибкую архитектуру, а ровно 10. Было бы весьма удобно иметь возможность доказать, почему один вариант кода будет хуже, а другой — лучше.

          К слову, с точки зрения тех рассуждений, которые я привёл, вами упомянутая WTF-метрика определяется через более базовые понятия.

          Далее я бы хотел ещё раз прокомментировать domain driven design. Замечу, что вы оценили качество моего примера и его нелепой архитектуры, опираясь на свой опыт и понимание различных концепций, которые, в совокупности, можно определить как набор правил. Например, новая кора в мозге распознаёт образ семантически неочевидного наследования — и сразу реагирует. И таких образов уйма, причём с явной избыточностью (подробнее можно почитать в книге Рэя Курцвейла «Эволюция разума»).

          Что пытаюсь сделать я? Всё же понять, на чём основаны данные правила? Как далеко и глубоко простирается их влияние?
          Зачем это нужно? Для того, чтобы докопаться до самой сути, ведь, как я уже показал, даже SOLID-принципы основываются на некоторых соображениях более низкого уровня, а значит, их можно из оных вывести. А значит, из оных можно вывести не только SOLID, но и что-то другое. Это также значит, что можно оценивать различные варианты и делать это точно, а не просто потому, что это мнение кого-то, кто только и делает, что апеллирует на свой безграничный опыт и количество прочитанных книг (например).

          Это не нужно. Именно потому, кстати, что читают код люди.

          Здесь, возможно, я несколько смутно выразился, и вы меня не поняли. Идея не в том, чтобы писать неочевидный и непонятный код, а затем аргументировать это тем, что, якобы «теория так сказала». Нет, напротив, исходя из той же теории (громко сказано, очень) энтропия «неочевидного» и «непонятного» кода крайне высока.
          Идея в том, чтобы иметь возможность оценивать код куда более глубоко и точно (то, о чём писал в предыдущем абзаце), чем обычное «Хм, этот код вполне неплох».


          1. lair
            06.01.2017 03:13
            +1

            был полностью вынесен за пределы DamageMediator

            Вынесен куда?


            Это можно называть разделением бизнес-логики и компонентной системы?

            Это зависит от того, что у вас бизнес-логика. И в моем понимании вашего домена, бизнес-логика обсчета дамага — это далеко не просто "математическая функция от нескольких переменных". Так что что-то вы разделили, но вот что с чем?


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

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


            Тут я имел ввиду не «красивый» вообще, а «красивый код», т.е. что именно мы подразумеваем под «красивый код»?

            Я не знаю, что вы понимаете под "красивый код". Я стараюсь этим словосочетанием в работе пользоваться по минимуму, потому что красота — субъективное понятие.


            Насколько «красивый код» отличается от «выгодного бизнесу кода» (исходя из количественной оценки определённого набора параметров).

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


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

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


            К слову, с точки зрения тех рассуждений, которые я привёл, вами упомянутая WTF-метрика определяется через более базовые понятия.

            Это какие же?


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

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


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

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


            Идея в том, чтобы иметь возможность оценивать код куда более глубоко и точно (то, о чём писал в предыдущем абзаце), чем обычное «Хм, этот код вполне неплох».

            Это прекрасная идея, но построить ее на количественных показателях не выйдет.


            1. JoshuaLight
              06.01.2017 05:03

              Вынесен куда?


              В DamageUtil.Next.

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


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

              Я не знаю, что вы понимаете под «красивый код». Я стараюсь этим словосочетанием в работе пользоваться по минимуму, потому что красота — субъективное понятие.

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

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

              Здесь я с вами согласен, добавить особо нечего.

              Это какие же?

              Ну, скажем, мой код из примера вызвал у вас WTF-реакцию. Его нелепость следует из того, что блоки, ответственные за выполнение задач, переплетены между собой или, как вы уже заметили, компоненты и контейнеры лежат вместе с бизнес-логикой.
              Единственное, о чём я совершенно не сказал — человеческий фактор, а именно такие понятия как «понятность» кода, которую сходу сложно определить, но которая означала бы, что метод asdgqw явно хуже, чем subtractHealth.

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

              Возможно, существенно сложнее.

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

              Возможно, насчёт «притянули» вы правы, но хочу заметить, что я хотел показать, что SRP — это всего лишь название для определённой комбинации понятий и положений, подобно тому, как закон коммутативности — это всего лишь наименование того, что a + b = b + a.
              Если намеченной цели добиться с первого раза не удалось — не беда. Ваши комментарии и критика чрезвычайно полезны, так что не останавливайтесь. Если есть ещё положения или понятия, в убедительности которых вы сомневаетесь, пишите, хотя очень не хотелось бы отнимать ваше время.


              1. lair
                06.01.2017 12:09

                В DamageUtil.Next.

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


                Я показал это (вероятно, неубедительно), а затем намеренно убрал магию, перенеся сущности в параметры.

                … и теперь код точно так же зависит от того, какие параметры ему передадут. Любое изменение в вызывающем коде, меняющее эти параметры, точно так же отразится на обсуждаемом коде.


                Ну, скажем, мой код из примера вызвал у вас WTF-реакцию. Его нелепость следует из того, что блоки, ответственные за выполнение задач, переплетены между собой или, как вы уже заметили, компоненты и контейнеры лежат вместе с бизнес-логикой.

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


                Возможно, существенно сложнее.

                О нет, это реальность, данная нам в ощущениях. Каждый раз, когда где-то пишется style guide, мы наблюдаем эту реальность в полный рост.


                SRP — это всего лишь название для определённой комбинации понятий и положений

                Это, извините, совершенно бессмысленное утверждение, потому что любое правило — это определенная комбинация понятий и положений. И… что?


                1. JoshuaLight
                  06.01.2017 18:34

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

                  Где, по вашему, должна в таком случае находится бизнес-логика? Повторяю, что конкретно в данном случае и для конкретно данной бизнес-логики просчёт урона — это математическая функция от нескольких переменных (значения урона, шанса критического удара).
                  То, что функция должна быть статической — как по мне однозначно. Я использовал наименование Util, т.к. по негласному соглашению стараюсь придерживаться правила, что в *Util лежат исключительно статические методы, которые что-то считают (в преобладающем большинстве случаев). Также в таких классах как правило нет состояния, чтобы ничего не усложнять.


                  … и теперь код точно так же зависит от того, какие параметры ему передадут. Любое изменение в вызывающем коде, меняющее эти параметры, точно так же отразится на обсуждаемом коде.

                  Функция складывания двух чисел тоже зависит от того, какие числа в неё передадут. Сейчас приведу пример.


                  Допустим для простоты, что "функция складывания" — что-то намного более сложное.


                  public static int Add(int? x, int? y)
                  {
                    if (x.HasValue && y.HasValue)
                      return x.Value + y.Value;
                  
                    if (!x.HasValue && y.HasValue)
                      return y.Value;
                  
                    if (x.HasValue && !y.HasValue)
                      return x.Value;
                  
                    return 0;
                  }

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


                  Думаю, очевидно, что текущая вариация метода Add делится на два блока: тот, который обрабатывает структуру Nullable, и тот, который суммирует два числа.
                  Количество зависимостей в методе складывается из int + Nullable<T> + Nullable<T>.HasValue + Nullable<T>.Value + (x + y) (как вы помните, это типа очень сложная операция). Итого Qd = 5.


                  Проведу рефакторинг, аналогичный тому, что есть в статье:


                  public static int Add(int? x, int? y)
                  {
                    if (x.HasValue && y.HasValue)
                      return Add(x, y);
                  
                    if (!x.HasValue && y.HasValue)
                      return Add(0, y);
                  
                    if (x.HasValue && !y.HasValue)
                      return Add(x, 0);
                  
                    return 0;
                  }
                  
                  public static int Add(int x, int y)
                  {
                    return x + y;
                  }

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


                  1. lair
                    06.01.2017 18:53

                    Где, по вашему, должна в таком случае находится бизнес-логика?

                    В объекте, к которому она семантически принадлежит.


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

                    … если бы я моделировал игровую боевку — скажем, для D&D или GURPS — то я бы шел одним из двух путей. В первом я бы разбил расчет урона на две части — созданный урон и понесенный урон (простите за кривые термины). Соответственно, созданный урон вычислялся бы тем, что его наносит (оружием/заклинанием/внешней силой), а дальше это значение передавалось бы объекту действия и тот бы уже вычислял понесенный урон. Потому что в реальности на урон может влиять: сила, тхака, критикалы, конкретные особенности оружия, окружающая среда, спасы, расы, буфы и еще миллион вещей. Собственно, эта сложность и порождает второй потенциальный путь: создать класс DamageCalculationRule, который бы описывал — существующую в реальности в PH — сущность "правила расчета урона", и передавал бы все нужные параметры ему. Более того, возможно, на каждый расчет таких правил было бы больше одного, и их вычисления применялись бы каскадом.


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

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


                    Тяжело не заметить, что количество зависимостей первого Add осталось таким же

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


                    Так что единственным оправданием подобного рефакторинга (в его правильном варианте, конечно) может быть только появление потребности к переиспользованию метода Add(int, int), причем не гипотетической "когда-то в будущем", а реальной, прямо сейчас.


                    1. JoshuaLight
                      07.01.2017 17:59

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

                      Спасибо, этот момент я понял, учту на будущее.


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


                      В примере класс Damage представляет из себя сущность, основанную на DnD-правилах броска костей, т.к. бросок кости с N граней M раз даёт нормальное распределение, что, в принципе, неплохо для баланса.
                      Сам класс Damage зависит от класса Dice, как и, например, CriticalChance.


                      Идея DamageMediator была в том, что он являлся посредником между сложной структурой контейнера Player и различными компонентами вроде Stats, Weapon, Equipment (например, кольца, влияющие на урон, тоже там хранились бы).


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


                      Более того, как вы уже поняли, компонентная система позволяет модулю битвы вообще не заботится о том, кто наносит урон и кому. Главное, чтобы соответствующие компоненты были.


                      У меня, соответственно, вопрос: чем плохо хранить функцию просчёта урона от нескольких переменных где-то в статическом классе, а затем пользоваться ей либо в модуле битвы, либо в компоненте-посреднике?


                      Приведу пример:


                      public static class DamageCalculationService
                      {
                        public static int Calculate(Damage damage, Chance criticalChance)
                        {
                          // такой же код.
                        }
                      
                        public static int Calculate(Weapon weapon, Chance criticalChance)
                        {
                          // здесь, скажем, учитываются свойства оружия (кинжал это или нет).
                        }
                      }

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


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


                      1. lair
                        07.01.2017 19:56

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

                        … вообще-то, для этого достаточно обычного старого доброго ООП, компоненты для этого вообще не нужны.


                        У меня, соответственно, вопрос: чем плохо хранить функцию просчёта урона от нескольких переменных где-то в статическом классе, а затем пользоваться ей либо в модуле битвы, либо в компоненте-посреднике?

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


                        Но даже если есть необходимость ее переиспользования, совершенно непонятно, зачем ее куда-то выносить… но для этого давайте сначала определимся — вы в рамках объектно-ориентированной парадигмы мыслите, или в рамках функциональной?


                        Я нахожу этот пример корректным

                        Этот пример мог бы быть корректным в рамках функционального программирования. Вы этой парадигмой пользуетесь?


                        1. JoshuaLight
                          10.01.2017 17:01

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

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


                          Это всё, между тем, описано в "Совершенном коде", о котором вы так часто вспоминаете, а именно — Часть 2. Глава 7. Раздел 1. Разумные причины для выделения методов (с. 160).


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


                          Этот пример мог бы быть корректным в рамках функционального программирования. Вы этой парадигмой пользуетесь?

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


                          1. lair
                            10.01.2017 17:11

                            Можете, пожалуйста, обосновать утверждение "нет никакого смысла"?

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


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


                            Ежели это не так, я предлагаю разобраться в этом моменте, по меньшей мере, мне любопытно, чем именно данный пример мог бы отличаться?

                            В "чистом" ООП нет ни статических классов, ни статических функций — потому что все операции совершаются над внутренним состоянием объектов. В идеале.


                            В вашем конкретном случае должне были бы возникнуть методы Damage.Calculate(CriticalChance) и Weapon.CalculateDamage(CriticalChance).


                            1. JoshuaLight
                              11.01.2017 00:36

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

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


                              Итак. Есть ряд аргументов, свидетельствующих в пользу второго варианта (я про методы Add) против первого:


                              1. Повысилась переиспользуемость как таковая. Возможность что-то переиспользовать уже побуждает другого программиста к переиспользованию. В противном случае ему бы пришлось самому искать и выносить работу в отдельный метод. Иными словами — проще перейти от состояния с 0 переиспользований к состоянию с 1.
                                Это необходимо учитывать отдельно от факта того, что метод вообще в принципе можно переиспользовать, потому что он актуален даже при переходе от состояния с N переиспользований к N+1.
                                Простым языком выражаясь: метод, который можно переиспользовать, получает очки в карму всякий раз, как кто-то его переиспользует, кроме того он получает дополнительное очко за первое переиспользование, потому что он выглядел соблазнительно и побуждал к переиспользованию.
                              2. Самый же главный аргумент заключается в том, что две ранее связанные между собой ответственности были разделены, тем самым снизилась вероятность изменения первоначального Add. Теперь его версия отвечает исключительно за то, чтобы подготовить корректные данные, в то время как работа по сложению чисел перешла к новой версии Add. И хотя пример со сложением чересчур упрощён, даже в нём ясно прослеживается преимущество разделения: проще тестировать, строго очерченные границы функциональности, ранее размытые, и, скажем, проще разобраться.

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


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


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


                              В "чистом" ООП нет ни статических классов, ни статических функций — потому что все операции совершаются над внутренним состоянием объектов. В идеале.
                              В вашем конкретном случае должны были бы возникнуть методы Damage.Calculate(CriticalChance) и Weapon.CalculateDamage(CriticalChance).

                              Здесь я вас прекрасно понял. Однако не вижу никаких глубоких различий между моим и вашим вариантами: что там, что там по смыслу происходит одно и то же — функция от нескольких переменных. Только вариант со статическим методом является stateless разве что, а второй, по всей видимости, больше похож на правду.


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


                              1. lair
                                11.01.2017 00:53

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

                                Это не важно. Они случились.


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

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


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

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


                                И хотя пример со сложением чересчур упрощён, даже в нём ясно прослеживается преимущество разделения: проще тестировать,

                                Проще? Протестируйте первый (проксирующий) метод в отрыве от второго. Можете? Нет. Значит, чтобы протестировать первый метод, вам придется написать все сценарии от второго, плюс сценарии от первого. Иными словами, вам придется повторить все сценарии тестирования второго метода дважды. Так что тестировать проще не стало.


                                Мне кажется, что сложность в общем понизилась

                                Да?


                                Вот смотрите, у вас был первый вариант, который имел (грубо) следующий контракт:


                                int x, y;
                                Add(null, null).Should().Be(0);
                                Add(x, null).Should().Be(x);
                                Add(null, y).Should().Be(y);
                                Add(x, y).Should().Be(x + y);

                                Ваш второй вариант этому контракту удовлетворяет (иначе какой это, нафиг, рефакторинг).


                                Теперь давайте скажем, что у нас поступило новое требование, и
                                "сложение" двух чисел всегда должно округлять до десятка. Т.е., Add(2, 7).Should().Be(10). Конечно же, мы правим метод Add(int, int), потому что логика "сложения" — она в нем.


                                Прогоняем тесты на контракт… и второй и третий тесты падают:


                                Add(2, null).Should().Be(2); // FAIL: 0 != 2
                                Add(null, 7).Should().Be(7); // FAIL: 10 != 7

                                Ээээ, что?! Приятного дебага.


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

                                То, что вы их не видите, означает, что вы не понимаете разницы между объектно-ориентированной и функциональной парадигмой.


                                В частности, метод объекта может опираться на любые внутренние детали реализации Weapon и Damage в то время как функция над двумя объектами — только на их публичный контракт. Соответственно, разные возможности по инкапсуляции.


                                если опустить формальности наименований и используемых парадигм.

                                Вот понимаете, для вас наименование — это формальность. А для меня это первое, на что я смотрю — потому что именно по наименованиям я читаю код.


                                1. JoshuaLight
                                  11.01.2017 23:21

                                  Конечно же, мы правим метод Add(int, int), потому что логика "сложения" — она в нем.

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


                                  1. lair
                                    11.01.2017 23:24

                                    Какой пример — такая и конкретика. Внесение округления в математические операции — это очень частое бизнес-требование. А куда в приведенном вами примере нужно вносить такое изменение?


                                    1. JoshuaLight
                                      12.01.2017 00:00

                                      Какой пример — такая и конкретика.

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


                                      А куда в приведенном вами примере нужно вносить такое изменение?

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


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


                                      1. lair
                                        12.01.2017 00:23

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

                                        А как вы этот уровень посчитали, напомните?


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

                                        Который вызывается откуда? Можете привести новый код метода Add(int?, int?)?


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

                                        … я, собственно, все пытаюсь вам продемонстрировать, что ни того, ни другого вы не достигли.


                                        1. JoshuaLight
                                          12.01.2017 00:27

                                          … я, собственно, все пытаюсь вам продемонстрировать, что ни того, ни другого вы не достигли.

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


                                          1. lair
                                            12.01.2017 00:47

                                            Во-первых, если вы не достигли упрощения, это еще не значит, что вы усложнили. Вы просто не сделали проще.


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


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


                                            На самом деле, "дробление на мелкие составляющие" — это, грубо говоря, вот так (сделаем вид, что класса Uri не существует):


                                            //было
                                            var url = "http://habrahabr.ru/post";
                                            var host = url.Substring(url.IndexOf("://" + 3)).Split('/', 2)[0];
                                            var ip = Dns.GetHostEntry(host).AddressList[0].MapToIPv4().ToString();
                                            
                                            //стало
                                            var url = "http://habrahabr.ru/post";
                                            var host = GetHostNameFromUrl(url);
                                            var ip = GetIPv4ByHost(host);

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


                                            1. JoshuaLight
                                              12.01.2017 02:25

                                              из x + y вы делаете Add (из трех мелких — один крупный, если грубо).

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


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

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


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


                                              Ещё вопрос: что такое "когнитивная дистанция"?


                                              И ещё: к какой категории вы относите случаи, когда вместо комментирования кода, он выносится в метод с декларативным названием? Это описано, кажется, у Мартина Фаулера в книге про рефакторинг.


                                              1. lair
                                                12.01.2017 02:34

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

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


                                                верно ли сказать, что, когда мы выносим "как" в "что", мы не только улучшаем читаемость исходного метода, но и создаём независимый от места использования кусок функциональности, что, в целом, лучше.

                                                Нет.


                                                Ещё вопрос: что такое "когнитивная дистанция"?

                                                Умозрительная метрика, позволяющая грубо оценить, насколько сложно переключаться между разными семантическими пластами в коде. Грубо говоря, "достать из строки символы 5-8" — это один семантический пласт, а "достать из uri имя хоста" — это другой, "отправить емейл на адрес" — третий, а "отправить уведомление пользователю" — четвертый. Переключение между пластами стоит усилий — поэтому для чтения выгодно, когда весь код подряд оперирует терминами из одного семантического пласта.


                                                И ещё: к какой категории вы относите случаи, когда вместо комментирования кода, он выносится в метод с декларативным названием?

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


                                                1. JoshuaLight
                                                  12.01.2017 12:03

                                                  Нет.

                                                  Почему?


                                                  1. lair
                                                    12.01.2017 12:04

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


                                                    1. JoshuaLight
                                                      12.01.2017 12:11

                                                      Потому что не всякое вынесение

                                                      Не всякое вынесение "как" в "что"?


                                                      Хорошо, а если детализировать.
                                                      Положим, есть метод подсчёта… например… Топ N пользователей по определённому критерию. Допустим также для примера, что он реализован так:


                                                      Инициализировать результирующий список с N элементами
                                                      
                                                      Пройтись по ВСЕМ пользователям
                                                        Посчитать баллы критерия по текущему пользователю
                                                      
                                                        Если баллы больше, чем первый элемент результирующего списка
                                                          Сместить все элементы списка вправо
                                                          Записать новый элемент вместо первого
                                                      
                                                      Конец цикла
                                                      
                                                      Вернуть результирующий список

                                                      Видно, что кусок "Сместить все элементы списка вправо" может лежать, по меньшей мере, в отдельном методе, а по большей — как метод расширения (если .NET) для массива.


                                                      Это упрощение или нет?


                                                      1. lair
                                                        12.01.2017 12:23

                                                        Не всякое вынесение "как" в "что"?

                                                        Да. И вообще (и тем более) не всякий introduce method refactoring имеет благоприятные последствия.


                                                        Это упрощение или нет?

                                                        Скорее всего да. Но зависит от того, как этот кусок выглядел изначально.


                                                        (И, что вероятнее, существенно большее упрощение будет, если выделить сущность "ограниченная сортированная куча", и пользоваться ей.)


                                                        1. JoshuaLight
                                                          12.01.2017 15:19

                                                          Что ж, можно заключить, что мы пришли к согласию.


                                                          Спасибо за пояснения и комментарии, я много узнал и многое понял.


                              1. lair
                                11.01.2017 01:28

                                Отдельно рекомендую подумать над вот таким "рефакторингом":


                                public static int Add(int? x, int? y)
                                {
                                  if (!(x.HasValue && y.HasValue))
                                  {
                                    if (x.HasValue)
                                      return x.Value;      
                                    if (y.HasValue)
                                      return y.Value;      
                                    return 0;
                                  }
                                
                                  //весь остальной многокод про сложение
                                }

                                PS


                                match (x, y) with
                                | (None, None) -> 0
                                | (Some(x), None) -> x
                                | (None, Some(y)) -> y
                                | (x, y) ->
                                  //весь остальной многокод про сложение


                                1. lair
                                  11.01.2017 01:35

                                  … ну или если не любить вложенные скобки, то:


                                  public static int Add(int? x, int? y)
                                  {
                                    if (!x.HasValue && !y.HasValue)
                                      return 0;
                                  
                                    if (!y.HasValue)
                                        return x.Value;      
                                  
                                    if (!x.HasValue)
                                        return y.Value;      
                                  
                                    //весь остальной многокод про сложение
                                  }

                                  Но здесь очень много отрицаний, а отрицания затрудняют чтение.


                          1. ApeCoder
                            10.01.2017 19:30

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


                            К тому же у вас уже создан DamageMediator, единственная обязанность которого — расчитывать урон. Соответственно название надо изменить на DamageCalculator и названием метода на Calculate. Введение еще одного уровня абстракции не упрощает дело, а только усложняет. Представьте себе, что для вычисления понадобится еще один параметр — придется менять DamageMediator и DamageUtil.


                            1. JoshuaLight
                              11.01.2017 00:47

                              Спасибо за комментарий!


                              С вашими утверждениями я вполне согласен, как раз читаю сейчас Макконнелла и нахожу примерно то же самое. В частности вариант с DamageCalculator и Calculate — действительно очень хорошая абстракция.


                              Представьте себе, что для вычисления понадобится еще один параметр — придется менять DamageMediator и DamageUtil.

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


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


                              1. ApeCoder
                                11.01.2017 10:47

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

                                Для этого в оба элемента надо будет добавить этот новый компонент. Но зачем это делать — непонятно. Получается, что они будут тесно свзаны (High coupling).


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

                                Рекомендую прочесть серию постов Naming is a process и попробовать применить то, что там написано к примеру расчета урона, обратив внимание на то, чтобы код хорошо читался


                                1. JoshuaLight
                                  11.01.2017 11:57

                                  Для этого в оба элемента надо будет добавить этот новый компонент. Но зачем это делать — непонятно. Получается, что они будут тесно свзаны (High coupling).

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


                                  По идее, всегда будут, по меньшей мере, расширяться два места:
                                  1) место, откуда достаются и формируются данные для просчёта;
                                  2) место, которое считает значение на основании данных.


                                  Если у вас есть какие-то быстрые соображения по этому поводу — пожалуйста, буду рад выслушать.


                                  Рекомендую прочесть серию постов Naming is a process и попробовать применить то, что там написано к примеру расчета урона, обратив внимание на то, чтобы код хорошо читался

                                  Спасибо, обязательно прочитаю!


                                  1. lair
                                    11.01.2017 12:05
                                    +1

                                    По идее, всегда будут, по меньшей мере, расширяться два места:
                                    1) место, откуда достаются и формируются данные для просчёта;
                                    2) место, которое считает значение на основании данных.

                                    Если оба этих "места" находятся в одном классе, а изменение логики не преполагает добавления новых данных (например: раньше считали попадание от STR, начали считать попадание от STR и DEX — но DEX и раньше была у PC, просто не учитывалась) — понадобится только изменить метод.


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


                                    1. JoshuaLight
                                      12.01.2017 00:53

                                      Количество "если" нивелирует смысл всех вопросов и рассуждений.


                                      "Если вы пишите код так, чтобы он легко расширялся, он будет легко расширяться".


                                      Ответьте, пожалуйста, на вопрос, что делать, если вдруг для просчёта урона надо учитывать эффект от кольца… надетого на персонаже члена группы?


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


                                      1. lair
                                        12.01.2017 00:57

                                        Проще всего для этого использовать rule-based-механизм, где правила имеют доступ ко всему контексту хода. Ну и тогда там будет банальное party.SelectMany(p => p.ActiveEquipment).OfType<IRing>().


                                        Но в принципе, можно и от оружия попрыгать: this.Character.Party — далее аналогично.


                                        1. JoshuaLight
                                          12.01.2017 02:03

                                          Не понял.


                                          Где в вашем примере просчёт урона затем происходит и какие данные он использует?


                                          1. lair
                                            12.01.2017 02:08

                                            В зависимости от выбранной модели (я уже описывал их в одном из ранних комментариев), либо у нас есть IDamageCalculationRule.Calculate(weapon, target), который опирается на все возможные знание об игровом поле, либо же IDamageDealer.CalculateDamage(target) (кстати, первому ничто не мешает внутри вызывать второе, но это уже отдельный вариант).


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


                                            1. JoshuaLight
                                              12.01.2017 02:18

                                              Насколько я понимаю, IDamageDealer — это интерфейс, который в вашем примере может реализовывать оружие, являясь (что правда), наносителем урона.


                                              Не до конца разобрался в том, что конкретно делает абстракция CalculateDamage и как она сделает так, чтобы на оружие влияли предметы, характеристики, что угодно?


                                              Или вы предлагаете сделать так, чтобы урон вычислялся в различных сущностях отдельно, а затем аккумулировался по некоторым правилам, скажем, аддитивно или мультипликативно (которые, соответственно, в ином месте лежат)?


                                              1. lair
                                                12.01.2017 02:28

                                                Не до конца разобрался в том, что конкретно делает абстракция CalculateDamage и как она сделает так, чтобы на оружие влияли предметы, характеристики, что угодно?

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


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


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


                  1. VolCh
                    07.01.2017 08:09

                    По DDD stateless методы бизнес-логики, явно к сущностям и т. п не относящиеся — это Domain Service. Статическими их делать или обычными — компромисс между гибкостью и скоростью. Нормальное название DamageCalcService или DamageCalculator.


      1. lair
        06.01.2017 02:11
        +1

        Можно ли пытаться количественно оценивать код, полностью опуская своё человеческое начало или же это невозможно?

        Собственно, ваш код — иллюстрация того, почему это бессмысленно. С точки зрения количественной метрики нет разницы между q = A.Next() и attackDamage = currentPlayer.Attack(selectedTarget) — но вот с точки зрения программиста, читающего код, разница фундаментальная.


      1. michael_vostrikov
        06.01.2017 18:13

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

        Для генерации псевдослучайных чисел это название оправдано, потому что следующее число зависит от предыдущего по формуле.


        1. JoshuaLight
          10.01.2017 17:02

          Спасибо за пояснение! Я ошибался.)))


  1. Chipsetone
    06.01.2017 01:56
    +1

    Напомнило диплом и прочие институтские "особенности" — многовато теоретической теории и обоснования всем известных фактов. Некоторые термины и положения, приведенные в начале вообще неизвестно зачем даны. Разделить бы эту статью на несколько маленьких статеек поконкретнее и примеров побольше в каждую. Объявление терминов и положений засунуть в Lazy)


    1. JoshuaLight
      06.01.2017 01:59

      Спасибо за комментарий!

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

      Думаю, если приступлю писать вторую часть, то обязательно разъясню все моменты и реализую больше непосредственно расчётов и формул (если получится). Или даже, как вы посоветовали, разобью всё на маленькие части.


      1. HaJIuBauKa
        07.01.2017 17:59

        Тем не менее, Ваша манера излагать, импонирует. Я думаю не только мне. Пишите сударь, больше и глубже!


        1. JoshuaLight
          07.01.2017 18:01

          Что же, спасибо на добром слове!


          Не могу не заметить, что полезность комментариев и обсуждений, там происходящих, давным-давно перевалила ценность самой статьи, за что могу только поблагодарить всех отписавшихся, ибо для меня и для дальнейшей работы в эту сторону это будет как никогда кстати!


  1. sentyaev
    06.01.2017 02:02
    -1

    Финальным штрихом будет вынесение статической функции просчёта урона во вспомогательный класс

    DamageUtil

    Я когда делаю codereview все классы оканчивающиеся на Utils и Helpers считаю дефектами.


    1. ApeCoder
      06.01.2017 12:23

      Тут наминг просто адский и ничего непонятно. Почему Mediator, если он вычисляет урон, а не Calculator что такое CriticalChance? и почему Next вычисляет урон и его возвращает? Почему именно Next?


      1. JoshuaLight
        10.01.2017 17:02

        Спасибо за комментарий. Ниже я уже дал пояснение касаемо общих архитектурных соображений примера.


  1. IIvana
    06.01.2017 04:03
    -2

    Да, под веществами после Виттгенштейнов с Курцвелями и не такое придумать можно…


  1. Vlad_fox
    06.01.2017 13:56

    смог осилить только первый том данного ноучного труда, и сразу полез в коментарии — удостовериться, один ли я настолько тупой, что не понимаю нах… на зачем вот это вот все писать???
    зачем увеличивать и без того зашкаливающую энтропию информационного пространства???

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


    1. velvetcat
      06.01.2017 22:40

      Зря Вы так, очень интересный подход. Автору бы на практике освоить применение SOLID (особенно S, как мне показалось, а еще это наследование везде… бррр), а так же взять реальный код в качестве примера (синтетика обычно выглядит уныло) — и получилось бы совсем круто.


      1. JoshuaLight
        10.01.2017 17:03

        Спасибо за комментарий!


        Можете, пожалуйста, объяснить, где была допущена не точность в понимании Single Responsibility? Буду премного благодарен.


  1. worldmind
    06.01.2017 20:58

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


  1. worldmind
    06.01.2017 21:04

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


  1. velvetcat
    06.01.2017 22:54

    > Dependency Inversion Principle — принцип инверсии зависимостей. Для меня один из самых труднопонимаемых принципов

    А что в нем непонятного? Как Вы его понимаете?

    > Логичность и истинность данного принципа напрямую следует из Положения 1.3.1.1: вероятность изменения абстракции ниже, чем вероятность изменения конкретного блока-реализатора.

    Из этого тезиса следует, что достаточно добавить интерфейс для существующей конкретной реализации и зависеть от него. Но этого недостаточно.

    Главное в этом принципе — более общее, находящееся выше по стеку абстракций, не должно зависеть от менее общего. Собственно, это есть в определении :).


    1. JoshuaLight
      07.01.2017 18:19

      А что в нем непонятного? Как Вы его понимаете?

      Насколько я понимаю, основная суть принципа инверсии зависимостей заключается в том, чтобы их, собственно, инвертировать.


      Если класс A использует класс B, то вероятность изменения класса A составляется из: вероятности изменения требований к классу A, вероятности изменения требований к классу B, вероятности изменения тех методов, что используются классом A.


      Если инвертировать зависимость от A к B, внедрив между ними абстракцию IC, то получится, что теперь вероятность изменения A составляет: вероятность изменения требований к классу A, вероятность изменения IC. Идея в том, что B теперь тоже зависит от IC, т.к. реализует этот интерфейс. Оба класса зависят от абстракции и от налагаемых ею ограничений. Наверное, я упустил разъяснить в статье этот момент, однако очевидно, что теперь, в случае изменения B, которое сломает корректность реализуемой абстракции, разгребать это придётся классу B.


      Есть ощущение, что я где-то допустил неточность, поэтому поправьте, пожалуйста.


      1. velvetcat
        07.01.2017 18:54

        Собственно, все так. Но вся соль в том, чтобы правильно выбрать эту новую абстракцию, а это уже чисто практический аспект.


  1. ApremierA
    07.01.2017 02:53

    Определенные метрики у кода существуют, например, ciclomatic complexity для метода, класса; количество зависимостей; количество copy paste; в конце концов, покрытие кода тестами. Если достаточно исследовать вопрос существующих метрик (для многих языков есть инструменты автоматического анализа), то этого будет достаточно для выявления разницы "хороший/плохой", "красивый/некрасивый" код.


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


    1. JoshuaLight
      07.01.2017 18:27

      Спасибо за комментарий!


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


      Возможно, как раз именно в них и нет необходимости. Необходимость есть скорее в том, чтобы объяснить, как и из чего складывается "хорошая" архитектура: от каких величин зависит, как её можно измерить, в конце концов на чём зиждется её "мироздание". Существует уйма аббревиатур вроде SOLID, KISS, GRASP, YAGNI, DRY (больше не знаю), которые, кажется, нужны только для того, чтобы в резюме занимать место наряду с джаваскрипт-фреймворками (да простит мне комьюнити шутку). Безусловно, суть таких принципов ясна и уловима, однако мне, возможно очень субъективно, кажется и хочется верить, что всё можно изучить и понять гораздо глубже: нащупать почву, на которой стоит этот исполинский бастион.


      1. lair
        07.01.2017 20:02
        +1

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

        … а почва эта описана, например, у МакКоннела, которого вы не читали, и сводится к простому тезису: одна из основных (если не основная) задач при проектировании/написании кода — это управление сложностью (ее минимизация). А принципы, на которые вы ссылаетесь — это проверенные опытом способы решения этой задачи.


        1. JoshuaLight
          10.01.2017 17:07
          -1

          … а почва эта описана, например, у МакКоннела, которого вы не читали, и сводится к простому тезису: одна из основных (если не основная) задач при проектировании/написании кода — это управление сложностью (ее минимизация). А принципы, на которые вы ссылаетесь — это проверенные опытом способы решения этой задачи.

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


          Насчёт "минимизации сложности" и "Главного технического императива разработки ПО" могу сказать лишь одно: видавшую виды Бритву Оккама можно было и попроще выразить.)))


          На самом деле, действительно, ничего крайне "нового" и "удивительного" в том, чтобы перенести древний принцип в область разработки ПО. Мне вполне нравится, и я даже поддерживаю то, как красной нитью он тянется сквозь повествование в книге. Признаюсь, на таком уровне это вполне ясно, докладно и очевидно! Но, возможно, во мне бушует некоторый романтический дух, говорящий, что этого мало. Тут уж сказать точно не могу.


          1. lair
            10.01.2017 17:14

            Насчёт "минимизации сложности" и "Главного технического императива разработки ПО" могу сказать лишь одно: видавшую виды Бритву Оккама можно было и попроще выразить

            Кажется, прочитали, но еще не поняли. Управление сложностью сложнее (простите) бритвы Оккама ("Не следует множить сущее без необходимости").


            1. JoshuaLight
              10.01.2017 18:10
              -1

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


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


              1. lair
                10.01.2017 18:13

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

                … скажем, правило "именование должно быть семантически корректным" не имеет никакого отношения к бритве Оккама. То есть вообще никакого. Или введение абстракций.


                1. ApeCoder
                  10.01.2017 19:38

                  не имеет никакого отношения к бритве Оккама. То есть вообще никакого.

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


                  1. lair
                    10.01.2017 21:16
                    +1

                    Это уже демагогия. Сущность есть, а поименовали ее "медиатор", "калькулятор", "утилита" или "сервис" — бритве все равно. Бритве не все равно, есть ли эта сущность, но это тема другого разговора.


                    1. ApeCoder
                      11.01.2017 10:51

                      Мне кажется в данном случае наименование есть отражение мышления — если человек придумывает новые термины при существовании старых, он не до конца понимает что это одно и то же и придумывает новую сущность.


                      1. lair
                        11.01.2017 11:35
                        +1

                        Именование, конечно, отражение мышления — но когда человек называет метод, рассчитывающий нанесенный вред, Next, ни о каком "старом термине" (в пространстве программы) речи не идет.


                        1. ApeCoder
                          11.01.2017 16:25

                          Да, я именно о том, что человек выдумывает термины специально для программы, когда можно этого не делть, а просто взять их из предметной области. Типа ubiquitous language.


                          1. lair
                            11.01.2017 16:33

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


                          1. VolCh
                            11.01.2017 16:43

                            Типа ubiquitous language.

                            Одна из самых сложных стадий проекта. Ещё выделить контексты.


                1. JoshuaLight
                  11.01.2017 00:58
                  -1

                  … скажем, правило "именование должно быть семантически корректным" не имеет никакого отношения к бритве Оккама. То есть вообще никакого. Или введение абстракций.

                  Здесь вы в праве как согласиться, так и нет. Я не зря выделил слово "форма", и как по мне "принцип минимизации сложности", т.е. Главный Технический Императив — это форма бритвы Оккама в разработке ПО. То, что данный императив далее выливается в различные конкретные принципы — иной разговор.


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


                  1. lair
                    11.01.2017 01:02
                    -1

                    Простите, но это демагогия. Если вы не видите разницы между "не множить сложное", "не умножать сущности" и "минимизировать сложность" — я не вижу смысла ее с вами обсуждать.


                    Я понимаю, что очень хочется свести чужую практику к простой фразе, но поверьте, все совсем не так тривиально.


                    1. JoshuaLight
                      11.01.2017 03:51
                      -1

                      Вы трактуете Бритву Оккама "буквально", используя формулировку русскоязычной Википедии, совершенно не беря во внимание следующие факторы:
                      1) это перевод;
                      2) принцип имеет корни в 4 веке до нашей эры (Аристотель: "We may assume the superiority ceteris paribus [other things being equal] of the demonstration which derives from fewer postulates or hypotheses.");
                      3) существует ряд трактовок, основанных на понятии простоты. ("Prior to the 20th century, it was a commonly held belief that nature itself was simple and that simpler hypotheses about nature were thus more likely to be true.").


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


                      Интересно, использовали ли бы вы фразу "умножать сущности", если бы прочитали англоязычный вариант перевода: "Among competing hypotheses, the one with the fewest assumptions should be selected.". Я уже не говорю о том, что версия на латыни буквально переводится как "закон экономии".


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


                      Я понимаю, что очень хочется свести чужую практику к простой фразе, но поверьте, все совсем не так тривиально.

                      Забавно, но именно это и сделал Макконнелл — свёл чужую практику к простой фразе — "управление сложностью". Интересно, почему вы так избирательны в уровнях детализации, используемых в беседе?


                      Кроме того, в данной фразе вы:
                      1) выдвинули предположение о моих намерениях в виде утверждения с заранее определённой истинностью ("очень хочется свести чужую практику к простой фразе");
                      2) охарактеризовали мои предыдущие утверждения независимо от их сути ("всё совсем не так тривиально").


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


                      1. Подмена тезиса (про свод чужой практики к простой фразе).
                      2. Концентрация на частностях (слово "сущности" в одном из вариантов перевода Бритвы Оккама).
                      3. Апелляция к очевидности ("Если вы не видите разницы между "не множить сложное", "не умножать сущности" и "минимизировать сложность" — я не вижу смысла ее с вами обсуждать.").

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


              1. VolCh
                11.01.2017 10:38

                ибо полагается, что истина в простоте.

                Скорее полагается, что если простая гипотеза в качестве рабочей даёт те же практические результаты, что и сложная, то нет никаких объективных причин использовать сложную. Наука не оперирует истинами, а только фактами и теориями/гипотезами их обїясняющими.


  1. semenyakinVS
    07.01.2017 03:48

    Я искренне убеждён, что код — не искусство, это строгие, поддающиеся анализу, структуры, и мне не видится эффективным ориентироваться на рефлексивные ощущения «красоты»


    Жёсткая заявка. Я склонен не согласиться. Архитектура, имхо — это то, благодаря чему программирование будет оставаться искусством всегда. Это нечто на стыке философии и инженерии. При этом как в философии, так и в инженерной деятельности важно умение смотреть на задачу неформально, всегда под новым углом в поиске новых решений. Больше того — даже если есть совершенное решение в какой-то области (например, самолёты и машины уже практически обрели идеальное воплощение) всегда будут возникать новые области (там, электромобили, у которых другие принципы построения могут быть или даже какие-нибудь ракеты для гражданских лиц), формализация которых может потребовать новых подходов к проектированию в принципе. И в этом смысле базовая метафора статьи: «Метафора 1. Иерархично всё» несколько упрощённо рассматривает устройство мира и, как следствие, упрощает особенности проектирования дизайна кода.

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

    Философские разглагольствования
    Архитектура кода рассматривалась в дипломе как инструмент систематизации знаний о реальном мире. Парадигмы (ООП, ФП, и т.д.) диктуют логику превращения понятий из предметной области в программные сущности, а задача задаёт степень конкретизации предметных областей, отображаемых в код в рамках программой модели. Рефакторинг рассматривался как эволюция знаний: анализ, поиск закономерностей с учётом новых фактов, поиск понятий, которые следует уточнить или пересмотреть с учётом новых знаний — и, в конце, интеграция знаний в существующую систему… Так вот, к чему это всё… Не может быть никаких формальных правил, которые в 100% или даже в 80% позволят сформулировать оптимальный способ отображения модели реального мира с (важно!) нужной степенью конкретизации. Сам по себе процесс принятия решения того, какие именно понятия отображать в сущности и концепции программной модели — процесс весьма нетривиальный и близок, опять-таки, к философской деятельности.


    1. JoshuaLight
      07.01.2017 18:45

      Жёсткая заявка. Я склонен не согласиться. Архитектура, имхо — это то, благодаря чему программирование будет оставаться искусством всегда.

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


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


      Во-первых, хочу сказать, списывая у Шопенгауэра, что искусство — это всегда и в первую очередь познанное непосредственно и интуитивно, т.е. in concreto.
      Во-вторых, источник признания искусства всегда покоится в самом человеке.


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


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


  1. Deosis
    07.01.2017 09:04

    Статья интересная, но приведённый пример ужасен. Рефакторинг ради рефакторинга.
    Принципы SOLID стоит рассматривать в контексте изменений требований:
    S. Если за одно требование отвечает несколько сущностей (методов, классов, модулей), то при изменении требований придётся проверять их все. Если одна сущность имеет несколько ответственностей, то при изменении надо проверить, что изменение не затронет другую ответственность.
    O. Если в одном месте понадобится изменить поведение компонента, то стоит создать наследника с нужным поведением и передать его.
    L. Гарантирует, что все остальные части будут взаимодействовать с изменённым компонентом также.
    I. Меньше знаешь — крепче спишь. Меньше вероятность, что одни изменения повлекут другие.
    D. Благодаря ему мы можем передать наследника из второго принципа.
    Подобные принципы локализуют изменения и уменьшают их количество.
    В примере автора любое изменение затронет оба метода: например, множитель критического урона определяемый типом оружия.
    Пример из комментариев тоже неудачен, его приводят, когда описывают монады, и там он разобран лучше.


    1. JoshuaLight
      10.01.2017 18:03

      Спасибо за комментарий и разъяснения по поводу SOLID.


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

      На самом деле нет. По крайней мере это верно для множителя критического урона. Т.к. за эту область сейчас ответственнен метод DamageUtil.Next, то он и изменится. Метод DamageMediator.Next изменится лишь косвенно.


  1. Loki3000
    09.01.2017 15:05

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


    1. JoshuaLight
      10.01.2017 18:00

      Спасибо за комментарий!


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


      У меня, например, было так: у оружия существует набор свойств (это всё основано на ДнД отчасти). Скажем, "тяжёлое" (Heavy) означает, что в расчёте урона участвует некоторый модификатор. Есть ещё "искуссное" (Finesse), что отвечает, если мне память не изменяет, за то, что на результат начинает влиять одна из характеристик (наибольшая): ловкость или сила, которые хранятся в компоненте Stats.


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


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


      Возьмём в пример игру World of Warcraft с её рейтингом устойчивости (на момент патча 3.3.5а информация актуальна). Рейтинг устойчивости персонажа снижает наносимый по нему урон (это мы пока можем посчитать), шанс критического удара (уже не можем) и модификатор критического удара (фактор увеличения урона при крите). Последние два свойства, увы, на данный момент совершенно невозможно посчитать ни в какой из реализаций.


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


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


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


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