То, что начиналось как попытка укротить некоторые непослушные представления, превратилось в длинный и трудный уловок с помощью контроллеров, вспомогательных методов, дублирования и, наконец, путаницы.
Текущее положение вещей таково, что у нас есть модельный объект, который инкапсулирует всю логику, которая использовалась в представлениях и контроллерах. Мы извлекли методы и переименовали переменные, чтобы найти некоторую степень понимания. Код больше не пугает и не пугает, но похвастаться нечем.
module Stats class RunningTimeData include Rails.application.routes.url_helpers attr_reader :title, :dates, :key, :today def initialize(title, timestamps, key, now) @title = title @dates = timestamps.map(&:to_date) @key = key @today = now.to_date end def y_legend I18n.t('stats.running_time_legend.actions') end def y2_legend I18n.t('stats.running_time_legend.percentage') end def x_legend I18n.t('stats.running_time_legend.weeks') end def values datapoints_per_week_in_chart.join(",") end def links url_labels.join(",") end def values_2 cumulative_percentages.join(",") end def x_labels time_labels.join(",") end def y_max # add one to @max for people who have no actions completed yet. # OpenFlashChart cannot handle y_max=0 1 + datapoints_per_week_in_chart.max + datapoints_per_week_in_chart.max/10 end private def url_labels Array.new(total_weeks_in_chart) { |i| url(i, key) } << url(total_weeks_in_chart, "#{key}_end") end def url(index, id) options = { :controller => 'stats', :action => 'show_selected_actions_from_chart', :index => index, :id=> id, :only_path => true } url_for(options) end def time_labels labels = Array.new(total_weeks_in_chart) { |i| "#{i}-#{i+1}" } labels[0] = "< 1" labels[total_weeks_in_chart] = "> #{total_weeks_in_chart}" labels end def total_weeks_in_chart [52, total_weeks_of_data].min end def total_weeks_of_data weeks_since(dates.last) end def weeks_since(date) (today - date).to_i / 7 end def cumulative_percentages running_total = 0 percentages_per_week.map {|count| running_total += count} end def percentages_per_week datapoints_per_week_in_chart.map(&percentage) end def percentage Proc.new {|count| (count * 100.0 / dates.count)} end def datapoints_per_week_in_chart frequencies = Array.new(total_weeks_in_chart) {|i| datapoints_per_week[i].to_i } frequencies << datapoints_per_week.inject(:+) - frequencies.inject(:+) end def datapoints_per_week frequencies = Array.new(total_weeks_of_data + 1, 0) dates.each {|date| frequencies[weeks_since(date)] += 1 } frequencies end end end
Оттенки добра
Стив Фриман и Нат Прайс, авторы « Растущего объектно-ориентированного программного обеспечения под руководством тестов GOOS», рассказывают о четырех желательных характеристиках объектно-ориентированного кода:
- Слабо связанный
- Очень сплоченный
- Легко компонуется
- Контекстно-независимый
Stats::RunningTimeData
объекта Stats::RunningTimeData
для всех четырех учетных записей.
Слабо связанный
Этот код зависит от библиотеки I18n
.
I18n.t('stats.running_time_legend.percentage')
Хуже того, вы не можете создать его без Rails!
include Rails.application.routes.url_helpers
Представьте себе, что вы хотите использовать те же функции в приложении Sinatra или в автономном приложении электронной почты, которое регулярно отправляет отчеты. Он не будет работать в его текущей форме, потому что вам нужно будет тянуть Rails вместе.
Очень сплоченный
Код, конечно, не сплоченный.
У него есть несколько методов описания диаграммы: x_legend
, y_legend
, values
. Он также имеет несколько методов, которые не имеют ничего общего с диаграммами. Они больше о неделях: weeks_since(date)
, total_weeks_of_data
, total_weeks_of_data
.
Сплоченность часто связана с принципом единой ответственности (SRP). Этот объект делает слишком много разных вещей? Есть ли у него много разных причин для изменения?
Кажется, здесь есть как минимум две разные вещи: еженедельная статистика и конфигурация графика .
Легко составляется
Это большой неповоротливый объект. Вы можете использовать его или оставить его, но вы не собираетесь объединять его с другими объектами, чтобы сделать что-то полезное.
Независимый от контекста
Этот класс может использоваться только в контексте двух конкретных диаграмм, которые он был создан для обработки. Если вы хотите отправить по электронной почте простой текстовый отчет с теми же данными в дополнение к отображению диаграммы, вам просто не повезло.
Сделай это так
Методы, которые нам нужно извлечь из модели Stats::RunningTimeData
:
- количество точек в неделю
- проценты в неделю
- совокупный процент в неделю
Поскольку в неделю повторяется, кажется разумным сделать эту часть класса.
Но что, если вы хотите ежемесячную статистику тоже?
Да, это справедливый вопрос. Мы можем. Но сейчас мы не знаем и не знаем.
Давайте представим, что нам это не нужно. Позже, если мы увидим дублирование, возможно, будет очевидная абстракция. На данный момент легче иметь дело с тем, что на самом деле здесь, чем с каким-то туманным и гипотетическим будущим.
Представляем WeeklyHistogram
Мы сильно полагались на тесты Золотого Мастера. Теперь пришло время начать все сначала:
mkdir test/models/stats touch test/models/stats/weekly_histogram_test.rb
Одной из самых приятных вещей в создании полностью независимого от контекста, слабо связанного класса является то, что вам не нужно ждать загрузки всей платформы при выполнении тестов.
До сих пор нам приходилось ждать 3 секунды для каждого запуска теста, хотя мы выполняли один файл. Тест блокировки проходит через базу данных, контроллеры, представления — все.
3 секунды Этого достаточно, чтобы проверить твиттер.
В новых тестах все, что нам нужно, это date
из стандартной библиотеки, minitest
и (пока еще гипотетический) класс weekly_histogram
.
require 'date' require 'minitest/autorun' require './app/models/stats/weekly_histogram' class StatsWeeklyHistogramTest < Minitest::Test end
Это работает менее чем за 300 мс. Это чувствует себя почти мгновенно.
Реализация гистограммы
В духе программирования с помощью желаемого мышления вот API, который я хотел бы иметь:
histogram.counts histogram.percentages histogram.cumulative_percentages
Рефакторинг, который мы делали в предыдущих двух статьях, выявил некоторые интересные характеристики статистики:
- Существует ограничение : максимальное количество недель для включения в качестве независимой статистики.
- Если отсечка старше, чем самая старая точка данных, вы не заполняете пустые недели вплоть до отсечки.
- Если у вас данных на несколько недель больше, чем отсечка, то все переполнение будет объединено в одну корзину.
- Если у нас нет данных за данную неделю в середине нашего набора данных, он будет правильно посчитан как
0
.
Нам понадобятся два теста, чтобы покрыть это: тест на меньшее количество недель, чем пороговое значение, и один — на большее количество недель, чем пороговое значение. Тест должен включать неделю отсутствия данных, чтобы убедиться, что мы случайно не потеряли такое поведение.
Вот набор тестов, который я закончил, покрывая весь API нового объекта:
def test_fewer_weeks_than_cutoff today = Date.new(2014, 7, 1) cutoff = 10 # weeks dates = [ Date.new(2014, 6, 30), Date.new(2014, 6, 27), Date.new(2014, 6, 23), Date.new(2014, 6, 23), Date.new(2014, 6, 22), # no data for 3 weeks ago Date.new(2014, 6, 9), ] histogram = Stats::WeeklyHistogram.new(dates, cutoff, today) assert_equal [2, 3, 0, 1], histogram.counts end def test_more_weeks_than_cutoff today = Date.new(2014, 7, 1) cutoff = 3 # weeks dates = [ Date.new(2014, 6, 30), Date.new(2014, 6, 21), Date.new(2014, 6, 20), Date.new(2014, 6, 1), # June Date.new(2014, 5, 1), # May Date.new(2014, 4, 1), # April Date.new(2014, 3, 1), # March ] histogram = Stats::WeeklyHistogram.new(dates, cutoff, today) assert_equal [1, 2, 0, 4], histogram.counts end def test_percentages today = Date.new(2014, 7, 1) dates = [ Date.new(2014, 6, 30), Date.new(2014, 6, 19), Date.new(2014, 6, 18), Date.new(2014, 6, 7), ] histogram = Stats::WeeklyHistogram.new(dates, 4, today) assert_equal [25.0, 50.0, 0.0, 25.0], histogram.percentages end def test_cumulative_percentages today = Date.new(2014, 7, 1) dates = [ Date.new(2014, 6, 30), Date.new(2014, 6, 19), Date.new(2014, 6, 18), Date.new(2014, 6, 7), ] histogram = Stats::WeeklyHistogram.new(dates, 4, today) assert_equal [25.0, 75.0, 75.0, 100.0], histogram.cumulative_percentages end
Мы можем написать реализацию напрямую из класса Stats::RunningTimeData
чтобы тесты прошли. Пуристы могут захотеть заново реализовать все с нуля. Мне все равно, так или иначе, если результат читается и делает то, что ему нужно.
module Stats class WeeklyHistogram attr_reader :dates, :cutoff, :today def initialize(dates, cutoff, today=Date.today) @dates = dates @cutoff = cutoff @today = today end def counts frequencies = Array.new(length) {|i| datapoints_per_week[i].to_i } frequencies << datapoints_per_week.inject(:+) - frequencies.inject(:+) end def percentages counts.map(&percentage) end def cumulative_percentages running_total = 0 percentages.map {|count| running_total += count} end def length [cutoff, total_weeks_of_data].min end private def percentage Proc.new {|count| (count * 100.0 / dates.count)} end def weeks_since(date) (today - date).to_i / 7 end def total_weeks_of_data weeks_since(dates.last) end def datapoints_per_week frequencies = Array.new(total_weeks_of_data + 1, 0) dates.each {|date| frequencies[weeks_since(date)] += 1 } frequencies end end end
Это лучше?
Да, это точно.
Он слабо связан : все, что ему нужно, это сбор дат для работы.
Он очень сплоченный : все в классе заботятся об этих датах.
Его легко составить : код можно использовать для создания текстовых отчетов в автономной почтовой программе или для создания статистики для приложения Sinatra или приложения командной строки.
Это не зависит от контекста : не важно, откуда берутся эти даты. Контекст в текущем приложении — TODO. Незаконченные ТОДО, чтобы быть конкретными. Но это случайно. Теперь мы можем использовать это в совершенно разных контекстах: продажи кексов. Зомби атакует. Наблюдения за знаменитостями.
Стена на спине
Снова и снова мы принимаем решения. Это наша работа, и большую часть времени мы ошибаемся. Мы не можем знать будущее. Наши кодовые базы отражают наши догадки, наши предположения и, самое главное, наши ошибки.
Тесты Golden Master дают нам возможность изменить свое мнение, когда все первоначальные причины давно ушли. Когда мы больше не помним, почему мы сделали выбор, который мы сделали. Когда мы больше не уверены, как код делает то, что делает. Когда мы даже не уверены, что код делает больше.
Это выход из ситуации, когда нет очевидного пути вперед или когда движение вперед может быть на самом деле назад, округлым и запутанным.
Техника Golden Master — это не инструмент для дизайна, архитектуры или красоты. Золотой звучит так красиво, так благородно.
Это не так.
Это грязно, временно и совершенно практично.