Часть 1 — Позолоченная роза

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

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

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

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

Моя попытка решения позолоченной розы сработала просто отлично, но абстракция модели была беспорядочной, а код правил был почти менее читабелен, чем оригинал. В беседе с испытуемым мне указали на паттерн стратегии.

Шаблон стратегии кодирует структуру для замены методов в одном и том же базовом классе объектов, что дает элегантный способ использования той же информации, структуры и базового кода, при этом индивидуализируя работу правил (таких как, например, #applies? и # update_quality и #update_age).

Часть 2. Ряд правил скидок

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

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

Это означает, что решение необходимо для:

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

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

Тыкая медведя

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

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

Разбудить медведя

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

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

Тогда определенно пора попробовать этот шаблон стратегии. В качестве прелюдии я извлек код скидки в отдельный класс; тест-драйв конечно. Затем я создал класс DiscountList для хранения каждой скидки и в то же время использовал шаблон стратегии, чтобы разделить каждое правило скидки на отдельный класс, который был внедрен в класс Discount во время создания экземпляра. Потом стало некрасиво.

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

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

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

Лучшие методы пробуждения медведя?

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

  • Есть ли лучший способ структурировать это решение, например, внедрить экземпляры DeliveryList в экземпляры Discount или DiscountLine во время создания, чтобы их не приходилось каждый раз передавать туда и обратно в качестве аргументов метода?
  • Есть ли лучший способ TDDing этого решения? Создание каждого класса было достаточно простым с использованием модульного тестирования, но этап, на котором я собирал скидки, Discount_list и заказ вместе, был не очень красивым. Мне пришлось во многом полагаться на тесты функций, чтобы пройти через это, до такой степени, что сначала мне удалось пропустить модульные тесты для TDD метода в классе Order.

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

Часть 3 - Не тот медведь

После публикации первых двух разделов я взял интервью у организации, проводившей тест. Простой запрос: «Сделайте скидку на общую сумму в 20 % в июле при продажах свыше 30 фунтов стерлингов» — взорвал мой подход. Чтобы реализовать эту новую скидку, мне нужно было как реализовать новую стратегию, так и добавить исключение к существующей, 10%-ную скидку «по умолчанию» на заказ выше 30 фунтов стерлингов, чтобы новая скидка действовала вместо этой. Это доказывает, что нет разделения интересов. Было бы легко найти больше скидок, которые взаимодействовали бы друг с другом различными подобными способами, каждый из которых приводил бы к большей сложности кода для компенсации отдельных шаблонов.

У меня осталось несколько неаппетитных вариантов для немедленного требования:

  1. Создайте новую стратегию как отдельный класс и измените затронутую стратегию, чтобы она не применялась в июле, пока новая
  2. Добавьте июльскую стратегию к существующей общей сумме скидок на 10 %, используя структуру «если-то-иначе», чтобы изменить то, что произошло. Имя, возвращаемое скидкой, также необходимо изменить (!)
  3. Добавьте какие-то правила выбора над стратегиями, подрывая смысл использования шаблона стратегии в первую очередь.

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

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

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

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

Обновление 20/5 — я обновил репозиторий требованием собеседования, используя второй подход. Отложить YAML до тех пор, пока у меня не появится больше времени, а также подумать о том, как работать со структурой if-then-else с рядом возможных условий.

Бонусное видео

Позолоченная роза открыта для огромного количества «решений». Следующее видео о рефакторинге Gilded Kata в Ruby от Sandi Metz демонстрирует решение, которое одновременно чрезвычайно элегантно и интересно смотреть:

Бонусное замечание по устаревшему рефакторингу

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

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