Архитектура для начинающих или почему не нужно вставлять флажок в человека-меча +33







Аннотация:

  1. Пример реализации нового функционала в классе через добавление «флажка».
  2. Последствия.
  3. Альтернативный подход и сравнение результатов.
  4. Как избежать ситуации: «Архитектурный оверкилл»?
  5. Момент, когда приходит время всё менять.

Перед тем как начать, пара замечаний:

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


Допустим, я — рядовой программист-программист на проекте. Проект представляет собой игру, в которой единственный Герой (aka Hero) идёт по идеально горизонтальной прямой слева направо. Этому замечательному путешествию мешают монстры. По команде пользователя Герой лихо рубит их мечом в капусту и в ус не дует. В проекте уже 100К строк, и «нужно больше строк фич!» Посмотрим же на нашего Героя:

class Hero {
    func strike() {
        // некий код 1
    }
    // ещё больше кода
}

Предположим также, что мой личный тимлид Василий тоже может в отпуск. Внезапно. Кто бы мог подумать? Далее ситуация развивается стандартно: прилетает Сова и требует срочно, вот прям вчера, сделать возможность выбирать перед началом игры: играть за Героя с дубиной или с мечом.

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

Замечание: if-чик в значении @!^%$#@^%&@!11$ его #$%@^%&!@!!! в &!^%#$^%!1 if-чик!

Мои мысли: “Ха! Два часа! Готово!”:

class Hero {
    enum WeaponTypes {
        case sword:
        case club:
    }
	
    var weaponType: WeaponTypes?

    func strike() {
        guard let weaponType = weaponType else {
            assertionFailure()
            return	
        }
        // Я крут: switch в Swift лучше if-ов - предупреждает о необработанных кейсах!
        switch (weaponType) {
            case .sword: // некий код обработки удара мечом
            case .club:  // некий код обработки удара дубиной
        }
    }
    // больше кода
}

Если Вы узнали в моём решении своё, то, увы: у меня для Вас две новости:

  1. Как бы хорошая: мы оба доставляем. Доставляем — от слова deliver value или от слова смешной (сквозь слёзы) код.
  2. И плохая: без Василия проекту капец.

Итак, что же случилось? Казалось бы, пока ничего. Но давайте посмотрим, что случится дальше (пока мы всеми силами удерживаем Василия в отпуске). А дальше будет вот что: отдел QA обратит внимание на вес нашего Героя. И нет, это не потому, что Герою пора присесть на диету, а вот почему:

var weight: Stone {
    return meatbagWeight + pantsWeight + helmetWeight + swordWeight
}

Забыл исправить расчёт веса. Ну подумаешь, ошибочка, с кем не бывает?! Тяп-ляп, трах-тибидох, готово:

var weight: Stone {
    // пропустим код guard let weaponType
    let weightWithoutWeapon = meatbagWeight + pantsWeight + helmetWeight
    switch (weaponType) {
        case .sword: return weightWithoutWeapon + swordWeight
        case .club:  return weightWithoutWeapon + clubWeight
    }
}

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

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

var spells: [Spells] {
    // пропустим код guard let weaponType и
    // код, чтобы подготовить let spellsWithoutWeapon: [Spells]
    switch (weaponType) {
        case .sword:
            // подготовка let swordSpells: [Spells]
            return spellsWithoutWeapon + swordSpells
	case .club:
            // подготовка let clubSpells: [Spells]
            return spellsWithoutWeapon + clubSpells
    }
}

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

Ему всего-то надо было добавить понятие “Уровень оружия” в формулы расчёта силы удара и веса Героя. А Петя пропустил один из четырех случаев. Но ничего! Я всё поправил:

func strike() {
    //guard let weaponType
    switch (weaponType) {
        case .sword: // некий код обработки удара мечом с учетом weaponGrade
        case .club:  // некий код обработки удара дубиной с учетом weaponGrade
    }
}

var weight: Stone {
    // guard let weaponType
    let weightWithoutWeapon = meatbagWeight + pantsWeight + helmetWeight
    switch (weaponType) {
        case .sword: return weightWithoutWeapon + swordWeight / grade
	case .club:  return weightWithoutWeapon + pow(clubWeight, 1 / grade)
    }
}

var spells: [Spells] {
    // А тут ничего не поменялось, ура! 
    // Правда, разросшийся метод увеличил объем класса. Искать внутри Героя код, имеющий отношение к задаче, стало чуть сложнее. Но это ничего!
}

Стоит ли говорить, что когда (внезапно!) понадобилось добавить лук / обновить формулы, опять были забытые кейсы, баги и вот это вот всё.

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

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

А с теми, кто всё же решил читать, продолжим.

Итак, обратимся к классикам:
Преждевременная оптимизация — корень всех зол!
А… э… это не то. Вот то:

В ООП-языках (например в Swift) есть три основных способа расширить возможности класса:

  1. Первый способ — “наивный”. Мы видели его только что. Добавление флажка. Добавление ответственности. Разбухание класса.
  2. Второй способ — наследование. Всем известный мощный механизм переиспользования кода. Можно было бы, например:
    • Отнаследовать новых ГерояСЛуком и ГерояСДубиной от Героя (который с мечом, но это сейчас не отражено в названии класса Hero). И затем в наследниках переопределить изменившиеся методы. Этот путь очень плох (просто поверьте мне).
    • Сделать базовый класс (или протокол) Герой, а все особенности связанные с конкретным типом оружия убрать в наследников:
      ГеройСМечом: Герой,
      ГеройСЛуком: Герой,
      ГеройСДубиной: Герой.
      Это лучше, но ведь сами имена этих классов смотрят на нас как-то недовольно, свирепо и в то же время грустно и с недоумением. Если на кого-то они так не смотрят, то постараюсь написать ещё статью, где помимо скучного маскулинного Героя будут они…
  3. Третий способ — сепарирование ответственности через инъекцию зависимости. Это может быть зависимость закрытая протоколом или замыкание (как бы закрытое сигнатурой), что угодно. Главное, чтобы реализации новых ответственностей ушли из основного класса.

Как это может выглядеть в нашем случае? Например, так (решение от Василия):

class Hero {
    let weapon: Weapon // зависимость класса Герой, т.е. Герой зависит от оружия

    init (_ weapon: Weapon) { // точка инъекции или внедрения зависимости
        self.weapon = weapon
    }
	
    func strike() {
        weapon.strike()
    }

    var weight: Stone {
        return meatbagWeight + pantsWeight + helmetWeight + weapon.weight
    }

    var spells: [Spells] {
        // подготовка как раньше
        return spellsWithoutWeapon + weapon.spells
    }
}

Что нужно, чтобы так было? Ниндзютсу — protocol:

protocol Weapon {
    func strike()
    var weight: Stone {get}
    var spells: [Spells] {get}
}

Пример реализации протокола:

class Sword: Weapon {
    func strike() {
        // то, что раньше валялось в Hero в switch внутри кейса .sword
    }

    var weight: Stone {
        // то, что раньше валялось в Hero в switch внутри кейса .sword
    }

    var spells: [Spells] {
        // то, что раньше валялось в Hero в switch внутри кейса .sword
    }
}

Аналогично Sword-у пишутся классы для: Club, Bow, Pike, etc. «Легко видеть» (с), что в новой архитектуре весь код, который относится к каждому конкретному типу оружия, сгруппирован в соответствующем классе, а не размазан по Герою вместе с остальными типами оружия. Это облегчает чтение и понимания Героя и любого конкретного оружия. Плюс, благодаря требованиям накладываемым протоколом гораздо проще отследить все методы, которые нужно реализовать при добавлении нового типа оружия или при добавлении новой фичи к оружию (например, у оружия может появиться метод расчёта цены).

Тут можно заметить, что инъекция зависимости усложнила создание объектов класса Hero. То, что раньше делалось как просто:

let lastHero = Hero()

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

class HeroFactory {
    static func makeSwordsman() -> Hero { // хотя, насчет static - это неточно
        let weapon = Sword(/* аргументы */)
        return Hero(weapon)
    }

    static func makeClubman() -> Hero {
        let weapon = Club(/* аргументы */)
        return Hero(weapon)
    }
}

Понятно, что Василию пришлось попотеть, чтобы раскидать кучу, которую навалили спроектировали Петя (и я).

Конечно, глядя на последнее решение, может возникнуть следующая мысль:
Ок, получилось, норм. Удобно читать и расширять, но ведь все эти фабрики / протоколы / зависимости — это куча оверхеда? Код, который ничего не даёт с точки зрения фич, а существует только для организации кода. «Код для организации кода», мгм. Неужели нужно городить этот огород всегда и везде?
Честный ответ на первый вопрос будет таким:
Да, это оверхэд к фичам, которые так любит бизнес.
А на вопрос “когда?” отвечает раздел:

Философия человека-меча или “когда же надо было править?”


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

Человек-меч — это даже звучит не так плохо.

А к чему привели первые «наивные» правки? К чему привело добавление if-чика?

Добавление if-чика привело к возникновению… мутанта! Мутанта, т.е. мутабельного объекта, который может мутировать между «человекомеч» и «человекодубина». При этом, если немного ошибиться в реализации мутанта, то возникает состояние «человекомечедубина». Не надо так! Не надо «человекодубины» вообще.

Надо:

  1. Человек + зависимость от меча (потребность в мече);
  2. Человек + зависимость от дубины (потребность в дубине).

Не всякая зависимость — зло! Это зависимость от алкоголя — зло, а от протокола — добро. Да даже зависимость от объекта лучше, чем “человекодубина”!

Когда произошло превращение в мутанта? Превращение в мутанта произошло в момент добавления флажка: монолитный код так и остался монолитным, но при изменении (мутации) флажка поведение одного и того же объекта стало существенно меняться.

Василий выделил бы здесь две стадии мутации:

  1. Добавление флага и самого первого «if» (или «switch», или другого механизма ветвления) по флагу. Ситуация угрожающая, но терпимая: Героя поливают радиоактивными отходами, но он превозмогает.
  2. Появление в классе более одного «if» по данному флагу, особенно в разных методах класса. Всё. Героя уже нет — перед нами мутант. Мутант, у которого постоянно что-то отваливается из-за багов.

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

Что именно сделал Василий, чтобы полечить мутанта?

С технической точки зрения — применил инъекцию зависимости закрытой протоколом.

С философской — разделил ответственность.

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

P.S. TL;DR;

  1. Видишь флажок?
  2. Будь мужиком, блин! Сотри его!
  3. Инъекция зависимости
  4. Profit !11

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

P.P.S. Серьезный опрос!

  • 9,5%Тема слишком простая, уже 100500 раз описано, такое никому не будет интересно20
  • 25,1%Я начинающий разработчик, мне понравилось53
  • 56,9%Я опытный разработчик, считаю, что для начинающих годно120
  • 1,4%Тема слишком сложная, в реальности такое никому не нужно3
  • 7,1%Изложение столь плохо, что бессмысленно оценивать тему15




Комментарии (24):

  1. fju
    /#21474206 / +2

    Если такие статьи помогут избежать появления в продакшене кучи плохого кода, то я только за. Для полноты картины не хватает тестов на код с if-чиками и на написанный по SOLID ;) Сразу станет понятно какие еще проблемы появятся.

  2. tikhonov666
    /#21474334

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

    • gerr_Truda
      /#21474806

      для того что-бы опытные скидывали её не опытным?

    • pavelsc
      /#21476408

      Опытный в значении больше стаж разве что, а не уровень экспертизы. Есть и по 10 лет работают, а уровень Джуна по коду

  3. Almet
    /#21475632

    Как раз на подобных примерах легче понять суть ООП, так что пиши еще!

  4. varton86
    /#21476470

    На эту же тему и тоже про рыцарей:

    habr.com/ru/company/redmadrobot/blog/352088

    • artemvkepke
      /#21476834

      Спасибо, прочитал, раньше не попадалось)) Видимо это судьба: статьи об инверсии зависимости должны быть написаны каждым поколением снова и снова))
      Кстати, рекомендую статью Евгения всем, кому нравится более спокойный стиль изложения. Также мне показалось интересным: взгляд под другим углом, другие акценты, более обширные и приближенные к UIViewController примеры.

      • frobeniusfg
        /#21481888

        вроде, DI обычно относят к инверсии контроля

        • artemvkepke
          /#21484270

          Спасибо, да, у меня в комментарии ошибка по невнимательности. Прямо из начала упомянутой выше статьи Евгения в блоге RedMadRobot:
              «DI (внедрение зависимости, англ. Dependency injection)»
              «IoC (Инверсия управления, англ. Inversion of Control)»
          Евгений как раз хорошо написал о различиях этих терминов и о том, почему они зачастую упоминаются вместе. У меня в статье больше про DI, конечно. IoC хоть и присутствует в «решении Василия», но явным образом об этом не сказано.

  5. iStaZzzz
    /#21476514

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

  6. SbWereWolf
    /#21476688

    Спасибо за юмор, поучительная история.
    ЗЫ
    Не сразу понял что protocol это interface.

    • artemvkepke
      /#21476790

      На примере Swift, поэтому и протоколы)

  7. Shatun
    /#21477176 / +2

    Хочу побыть адвокатом дьявола.
    Варинат 1. У вас была готовая игра, очень простая кототрую заказчик хотел выкинуть на рынок первым-но вдруг для добавления оружия вам понадобилось все переписать, полная регрессия-фича не легла на текущую архитектуру проекта, не повезло. Потом заказчик выпустил эту игру на рынок и навсегда забыл про нее. Да, вы сделали в полтора раза меньше игр за то же время, но зато у вас правильная архитектура!

    Второй вариант-у вас правда нужно расширение функционала и вы решили что наследование-неправильно, лучше делать через DI. Вы создали класс героя, у которого есть шапка, броник, меч. После этого вы создали шапку, броник и меч как асбтракцию, через DI передали герою их. Да вы потратили много времени, но у вас для добавления нового героя нужно всего лишь создать новый броник, меч, шапку, самого героя, скиллы и передать это все хозяйство герою. Все очень расширяемо, но вот нюанс-вам нужны просто уникальные герои-ведь человек в начале игры выбирает между варваром с мечом и силой 10 и магом с посохом и интеллектом 25.
    При наследовании вы бы переопределили вес, тип оружия и характеристики за 5 минут — наследуешся от героя, перепоределил аттаку, ловкость написал 15-и вор создан. С ДИ же пришлось создать нож с ловкостью 10 и шапку с ловкостью 5, нож с атакой 20, ну повезло-броник подошел от варвара.
    А потом вам говорят -разбойник должен прыгать выше, оказывается что проблема в его весе суммарном-30 а надо 20. Ты пытаешься уменьшить вес всех предметов чтобы сумма сошлась, но оказывается что бронник от варвара весит 25, но прыгать он может потому что силы больше. Надо срочно релизить — ты добавляешь шапку с весом -10, кидаешь ее вору, ура, вор начинает высоко прыгать.
    Потом игра опять расширяется и нужен еще один герой-ассасин-такой же, но с большей ловкостью и без броника. Ты создаешь героя с базовой ловкостью 20, инжектишь от вора кинжал, шапку, бронника нет, запускаешь игру и видишь как герой с весом -5 улетает в космос-ведь у этой шапки вес -10 для компенсации броника.

    • mayorovp
      /#21479048

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

    • maxim_0_o
      /#21481046

      В описании второго варианта, на мой взгляд, есть пара изъянов:
      1 (субъективный). Вы так говорите, как будто создание отдельных реализаций каждого из интерфейсов сильно дольше чем if-ы/наследование. Но с учетом мощи современных IDE, я не думаю, что разница значительна.
      2. Если вору надо прыгать выше — надо реализовывать эту особенность в воре, зачем трогать одежду? Решение с невесомой шапкой изначально было обречено на провал

      • Shatun
        /#21481570

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

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

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

        Нууу, тут Остапа немножко понесло)
        Но вообще «хорошо быть богатым и красивым»-но когда у вас баг на проде или нужно срочно релизить подставляется костыль. Идея в том что такой же костыль в варианте с наследованием не возник бы-там вес был бы прописан у персонажа. Ситуация конечно гипотетическая, но тоже самое можно сказать и рассказ из статьи.
        Идея что DI -не серебряная пуля, а ifы и наследование никак не однозначное зло, абстракции нужно выбирать с умом. И это правда сложная задача! Обычно для правильного создания всех абстракций нужно понимание бизнес логики и чего хочет бизнес в итоге.
        В ситуации с персонажем нужно понимать что это за игра-в простой ходилки а-ля марио if вполне возможно что проще было бы поддерживать, в игре где есть выбор из 3 заданныхх персонажей вам скорее всего лучше применить наследование, а если у вас существует инвентарь, предметы то проще сразу создавать отдельные предметы и например через DI давать их стартовому персонажу.

        • maxim_0_o
          /#21481736

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

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

          Если хватает времени — то надо добавить такое свойство, раз появилась такая необходимость. Если это требует больших изменений, а времени нет — то вес персонажа можно контролировать в самом методе получения веса, для этого не надо волшебных невесомых шапок.
          Идея что DI -не серебряная пуля, а ifы и наследование никак не однозначное зло, абстракции нужно выбирать с умом. И это правда сложная задача! Обычно для правильного создания всех абстракций нужно понимание бизнес логики и чего хочет бизнес в итоге.

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

  8. JediPhilosopher
    /#21477314 / +4

    Встречал такое хорошее утверждение, которое иллюстрируется этой статьей:


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


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


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

  9. dmitryb-dev
    /#21478206 / +2

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

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

    Так вот. Я люблю ифчики. Ифчики просты. Их легко рефакторить. Цель написанного кода понять намного проще. Наговнокодил кучу ифчиков? Тимлид пришел, сделал фейспалм, подсказал, как отрефакторить. В общем, я с вашим подходом полностью согласен, просто хочу продемонстрировать еще и другую крайность. И по моим наблюдениям, лучший код получается в цикле «взять самое топорное решение» — «отрефакторить».

    • porn
      /#21483646

      идеалисты, которые начинают наперед продумывать все по максимуму
      Приведите, пожалуйста, примеры из open source.

  10. Ommand
    /#21481328 / +1

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

  11. igormu
    /#21493328

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