Сегодня мы предоставим вашему вниманию перевод поста Стива Клабника (Steve Klabnik), известного разработчика, приверженца Ruby, одного из победителей Ruby Hero Award этого года. Что это за награда? Она присуждается победителями прошлого года тем участникам сообщества, которые наиболее проявили себя: создали значимый обучающий контент, разработали плагины и гемы, участвовали в проектах с открытым кодом. Такая награда была создана для того, чтобы отметить наиболее проявивших себя людей и дать им признание, которое они заслуживают.
Пообщаться со Стивом можно будет на конференции в Киеве RubyC 5-6 ноября этого года.
Я часто говорю людям, что учил Ruby через Rails. Это один из худших способов, но к тому времени я уже выучил столько языков программирования, что это не мешало мне. Тем не менее, это дало мне слегка искаженное ощущение того, насколько тщательно проектировать классы, необходимые для Rails приложений. К счастью, я пристрастно просматриваю код, написанный другими, и заметил, что есть одна важная вещь, которая встречается в разработках у многих уважаемых мною людей.
Мне кажется, эти люди также считают эту вещь уникальной. Это не когда люди, не умеющие писать хороший код, стараются, но все равно получается плохо. Это как флаг, сигнал. Теперь, когда я вижу, как кто-то внедряет эту вещь, я сразу думаю: «он шарит». Возможно, я слишком сильно доверяю своему чувству, но эта продвинутая техника разработки предлагает множество взаимосвязанных преимуществ вашим Rails приложениям, легко применима и ускоряет тестирование на порядок или больше. К сожалению, для многих начинающих Rails разработчиков это неочевидно, но я хотел бы, чтобы вы писали код лучше и вот я здесь, чтобы, с вашего позволения, «раскрыть секрет» и поделиться этой мощной техникой с вами.
Она называется «Старый простой объект Ruby"
Да, именно так. Руби класс, который ничего не наследует. Это настолько просто, что спрятано на видном месте. Любимые создателями Rails, старые простые объекты Ruby Object, или «POROs» как некоторые любят их называть, являются скрытым оружием против сложности. Вот что я имею в виду. Рассмотрим эту «простую» модель:
Мы хотим показать указатель по первой букве для всех наших постов. Для этого мы создаём словарь и кладем в него наши посты. Предположим, нам не надо разбивать список на страницы, так что не обращайте внимание на запрос всех постов из базы. Важна идея: теперь мы можем показать все посты по названию:
Конечно. C одной стороны, код не плохой. Но он также и не хорош: мы беспокоимся о представлении внутри модели, предназначенной для бизнес-логики. Так что давайте исправим это используя паттерн Presenter (Представитель):
Теперь мы можем использовать DictionaryPresenter.new(Post.all).as_dictionary. У него множество преимуществ: мы вынесли логику представления из модели. Мы уже добавили новую возможность: любая коллекция теперь может быть отображена с указателем. Мы можем легко написать отдельные тесты для этого класса-представителя и они будут быстрыми.
Несмотря на мою любовь к паттерну «Представитель», этот пост не о нем. Этот принцип появляется также и в других местах, «эта концепция заслуживает собственного класса». Перед тем, как перейти к другому примеру, давайте расширим этот: если мы хотим отсортировать наши посты по названию, этот класс будет работать, но показать, скажем, пользователей не получится, потому что у пользователей нет названий (поля title). Более того, мы получим большое количество постов на «А», потому что названия довольно часто начинаются с артикля «а», и на самом деле нам нужна первая буква второго слова. Мы можем сделать 2 вида Представителей, но тогда мы утратим общность и концепция «индекса» снова будет иметь 2 Представителя в нашей системе. Как Вы правильно поняли: PORO спасет нас!
Давайте слегка изменим нашего Представителя, чтобы он принимал объект политики:
Теперь мы можем добавить политики и сделать их разными:
Бам!
Да, становится немного длинновато. Бывает :) Зато теперь вы можете видеть, что каждая концепция имеет одно представление в нашей системе. Представителю все равно как устроены вещи, и только политики диктуют как они устроены. На самом деле, мои названия немного отстойны, возможно, стоило бы лучше назвать «UsernamePolicy» или «TitlePolicy», вообще-то. Нам даже все равно какого они класса!
И так во всем. Сочетая гибкость Руби с моим любимым примером из «Эффективной работы с унаследованным кодом», мы можем превратить в объекты сложные вычисления. Вгляните на этот код:
Ой! Этот метод выводит время возврата (вычисления), но, как вы видите, это сложное вычисление. Мы бы могли понять это более просто, если бы мы использовали выделение метода (метод рефакторинга Method Extract) несколько раз чтобы разбить его, но тогда мы рискуем засорить наш класс Quote большим количеством кода нужным только для красивого вычисления времени. Также, пожалуйста, не смотрите на то, что модель реализует логику представления — это только пример громоздкого кода.
Так, теперь первый шаг этого рефакторинга, который Feather (автор «Working Effectively With Legacy Code») называет «Break Out Method Object». Вы можете открыть свой экземляр «Working Effectively With Legacy Code» на стр. 330 и почитать больше. Если ее у вас нет, купите :). Вообще-то, я отвлекся. Вот план действий:
1. Создайте новый класс вычислений.
2. Определите в нем метод для работы по-новому.
3. Скопируйте тело старого метода и замените ссылки на указатели объектами.
4. Создайте ему конструктор, который принимает аргументы для назначения переменных, использованных в шаге 3.
5. Делегируйте старый метод новому классу и методу.
Я слегка поменял оригинальный шаблон для Руби, поскольку мы не можем положиться на компилятор (Lean On The Compiler) и несколько шагов Feather занимается как раз этим. В любом случае давайте попробуем на этом коде. Шаг первый:
Второй:
Третий:
Я люблю давать обычные названия в начале, а потом изменять их на шаге 5 после того, как понятно что именно он делает. Скорее всего наш код сам подскажет нам хорошее имя.
Четвертый:
Пятый:
Готово! Мы должны запустить тесты и посмотреть как они пройдут. Даже если «запустить тесты» означает проверить вручную…
Так в чем преимущество? Ну, сейчас мы можем начать процесс рефакторинга, но мы в нашей собственной маленькой чистой комнате. Мы можем внести методы в наш класс TurnaroundCalcuator без засорения класса Quote, мы можем написать быстрые тесты только для Calculator и разбить идею вычислений в одном месте, где позже её можно будет легко изменить. Вот наш класс после нескольких рефакторингов:
Ух-ты! Этот код, который я написал 3 года назад не идеален, но его почти можно понять. И каждый кусок имеет смысл. Это после 2-х или 3-х волн рефакторинга, которые я, возможно, раскрою в отдельном посте, потому что то, что получилось сейчас несколько более иллюстративно, чем я думал. В любом случае, вы поняли идею. Это то что я имею в виду, когда говорю что нацелен на пяти-строчные методы в Руби; если ваш код понятен, Вам легко его править.
Идея извлечения чистых объектов Руби справедлива и в Rails. Посмотрите на этот маршрут:
А? LoggedInConstraint?
О. Да. Объект, описывающий политику маршрутизации. Замечательно. Также примеры валидации, бесстыдно украденные с omgbloglol:
Это не чистый Руби класс, но вы поняли идею.
Сейчас вы наверно думаете: «Стив, это не только для Rails, ты солгал!» Ну да, вообще-то, вы поймали меня: это не секрет для объектно-ориентированного Rails, это больше общий объектно-ориентированный подход. Но есть кое что специфическое для Rails, что как будто манит вас в ловушку никогда не разбивать классы. Возможно, lib/ кажеться таким хранилищем мусора. Возможно, что 15-ти минутные примеры только включают в себя модели ActiveRecord. Возможно, больше закрытых Rails приложений, (Внимание: лишь мое собственное мнение) чем открытых не-Rails, так что, у нас не так много хороших примеров, на которые можно опереться. (У меня есть такая догадка, поскольку Рельсы часто используются для разработки сайтов для компаний. Гемы? Точно? Мое веб приложение? Не так много. Все же, у меня нет статистики для подтверждения.)
В общем: извлечение объектов предметной области — это хорошо. Они делают ваши тесты быстрыми, код – коротким, и облегчают в дальнейшем внесение изменений. У меня есть еще что сказать об этом, особенно про «быстрые тесты», но я уже исчерпал возможную длину поста. До следующего раза!
Пообщаться со Стивом можно будет на конференции в Киеве RubyC 5-6 ноября этого года.
Я часто говорю людям, что учил Ruby через Rails. Это один из худших способов, но к тому времени я уже выучил столько языков программирования, что это не мешало мне. Тем не менее, это дало мне слегка искаженное ощущение того, насколько тщательно проектировать классы, необходимые для Rails приложений. К счастью, я пристрастно просматриваю код, написанный другими, и заметил, что есть одна важная вещь, которая встречается в разработках у многих уважаемых мною людей.
Мне кажется, эти люди также считают эту вещь уникальной. Это не когда люди, не умеющие писать хороший код, стараются, но все равно получается плохо. Это как флаг, сигнал. Теперь, когда я вижу, как кто-то внедряет эту вещь, я сразу думаю: «он шарит». Возможно, я слишком сильно доверяю своему чувству, но эта продвинутая техника разработки предлагает множество взаимосвязанных преимуществ вашим Rails приложениям, легко применима и ускоряет тестирование на порядок или больше. К сожалению, для многих начинающих Rails разработчиков это неочевидно, но я хотел бы, чтобы вы писали код лучше и вот я здесь, чтобы, с вашего позволения, «раскрыть секрет» и поделиться этой мощной техникой с вами.
Она называется «Старый простой объект Ruby"
Да, именно так. Руби класс, который ничего не наследует. Это настолько просто, что спрятано на видном месте. Любимые создателями Rails, старые простые объекты Ruby Object, или «POROs» как некоторые любят их называть, являются скрытым оружием против сложности. Вот что я имею в виду. Рассмотрим эту «простую» модель:
Copy Source | Copy HTML
- class Post < ActiveRecord::Base
- def self.as_dictionary
- dictionary = ('A'..'Z').inject({}) {|h, l| h[l] = []; h}
-
- Post.all.each do |p|
- dictionary[p.title[ 0]] << p
- end
-
- dictionary
- end
- end
Мы хотим показать указатель по первой букве для всех наших постов. Для этого мы создаём словарь и кладем в него наши посты. Предположим, нам не надо разбивать список на страницы, так что не обращайте внимание на запрос всех постов из базы. Важна идея: теперь мы можем показать все посты по названию:
Copy Source | Copy HTML
- - Post.as_dictionary do |letter, list|
- %p= letter
- %ul
- - list.each do |post|
- %li= link_to post
Конечно. C одной стороны, код не плохой. Но он также и не хорош: мы беспокоимся о представлении внутри модели, предназначенной для бизнес-логики. Так что давайте исправим это используя паттерн Presenter (Представитель):
Copy Source | Copy HTML
- class DictionaryPresenter
- def initialize(collection)
- @collection = collection
- end
-
- def as_dictionary
- dictionary = ('A'..'Z').inject({}) {|h, l| h[l] = []; h}
-
- @collection.each do |p|
- dictionary[p.title[ 0]] << p
- end
-
- dictionary
- end
- end
Теперь мы можем использовать DictionaryPresenter.new(Post.all).as_dictionary. У него множество преимуществ: мы вынесли логику представления из модели. Мы уже добавили новую возможность: любая коллекция теперь может быть отображена с указателем. Мы можем легко написать отдельные тесты для этого класса-представителя и они будут быстрыми.
Несмотря на мою любовь к паттерну «Представитель», этот пост не о нем. Этот принцип появляется также и в других местах, «эта концепция заслуживает собственного класса». Перед тем, как перейти к другому примеру, давайте расширим этот: если мы хотим отсортировать наши посты по названию, этот класс будет работать, но показать, скажем, пользователей не получится, потому что у пользователей нет названий (поля title). Более того, мы получим большое количество постов на «А», потому что названия довольно часто начинаются с артикля «а», и на самом деле нам нужна первая буква второго слова. Мы можем сделать 2 вида Представителей, но тогда мы утратим общность и концепция «индекса» снова будет иметь 2 Представителя в нашей системе. Как Вы правильно поняли: PORO спасет нас!
Давайте слегка изменим нашего Представителя, чтобы он принимал объект политики:
Copy Source | Copy HTML
- class DictionaryPresenter
- def initialize(policy, collection)
- @policy = policy
- @collection = collection
- end
-
- def as_dictionary
- dictionary = ('A'..'Z').inject({}) {|h, l| h[l] = []; h}
-
- @collection.each do |p|
- dictionary[@policy.category_for(p)] << p
- end
-
- dictionary
- end
- end
Теперь мы можем добавить политики и сделать их разными:
Copy Source | Copy HTML
- class UserCategorizationPolicy
- def self.category_for(user)
- user.username[ 0]
- end
- end
-
- class PostCategorizationPolicy
- def self.category_for(post)
- if post.starts_with?("A ")
- post.title.split[1][ 0]
- else
- post.title[ 0]
- end
- end
- end
-
Бам!
Copy Source | Copy HTML
- DictionaryPresenter.new(PostCategorizationPolicy, Post.all).as_dictionary
Да, становится немного длинновато. Бывает :) Зато теперь вы можете видеть, что каждая концепция имеет одно представление в нашей системе. Представителю все равно как устроены вещи, и только политики диктуют как они устроены. На самом деле, мои названия немного отстойны, возможно, стоило бы лучше назвать «UsernamePolicy» или «TitlePolicy», вообще-то. Нам даже все равно какого они класса!
И так во всем. Сочетая гибкость Руби с моим любимым примером из «Эффективной работы с унаследованным кодом», мы можем превратить в объекты сложные вычисления. Вгляните на этот код:
Copy Source | Copy HTML
- class Quote < ActiveRecord::Base
- #<snip>
- def pretty_turnaround
- return "" if turnaround.nil?
- if purchased_at
- offset = purchased_at
- days_from_today = ((Time.now - purchased_at.to_time) / 60 / 60 / 24).floor + 1
- else
- offset = Time.now
- days_from_today = turnaround + 1
- end
- time = offset + (turnaround * 60 * 60 * 24)
- if(time.strftime("%a") == "Sat")
- time += 2 * 60 * 60 * 24
- elsif(time.strftime("%a") == "Sun")
- time += 1 * 60 * 60 * 24
- end
-
- "#{time.strftime("%A %d %B")} (#{days_from_today} business days from today)"
- end
- end
Ой! Этот метод выводит время возврата (вычисления), но, как вы видите, это сложное вычисление. Мы бы могли понять это более просто, если бы мы использовали выделение метода (метод рефакторинга Method Extract) несколько раз чтобы разбить его, но тогда мы рискуем засорить наш класс Quote большим количеством кода нужным только для красивого вычисления времени. Также, пожалуйста, не смотрите на то, что модель реализует логику представления — это только пример громоздкого кода.
Так, теперь первый шаг этого рефакторинга, который Feather (автор «Working Effectively With Legacy Code») называет «Break Out Method Object». Вы можете открыть свой экземляр «Working Effectively With Legacy Code» на стр. 330 и почитать больше. Если ее у вас нет, купите :). Вообще-то, я отвлекся. Вот план действий:
1. Создайте новый класс вычислений.
2. Определите в нем метод для работы по-новому.
3. Скопируйте тело старого метода и замените ссылки на указатели объектами.
4. Создайте ему конструктор, который принимает аргументы для назначения переменных, использованных в шаге 3.
5. Делегируйте старый метод новому классу и методу.
Я слегка поменял оригинальный шаблон для Руби, поскольку мы не можем положиться на компилятор (Lean On The Compiler) и несколько шагов Feather занимается как раз этим. В любом случае давайте попробуем на этом коде. Шаг первый:
Copy Source | Copy HTML
- class Quote < ActiveRecord::Base
- def pretty_turnaround
- #snip
- end
-
- class TurnaroundCalculator
- end
- end
Второй:
Copy Source | Copy HTML
- class TurnaroundCalculator
- def calculate
- end
- end
Третий:
Copy Source | Copy HTML
- class TurnaroundCalculator
- def calculate
- return "" if @turnaround.nil?
- if @purchased_at
- offset = @purchased_at
- days_from_today = ((Time.now - purchased_at.to_time) / 60 / 60 / 24).floor + 1
- else
- offset = Time.now
- days_from_today = @turnaround + 1
- end
- time = offset + (@turnaround * 60 * 60 * 24)
- if(time.strftime("%a") == "Sat")
- time += 2 * 60 * 60 * 24
- elsif(time.strftime("%a") == "Sun")
- time += 1 * 60 * 60 * 24
- end
-
- "#{time.strftime("%A %d %B")} (#{days_from_today} business days from today)"
- end
- end
Я люблю давать обычные названия в начале, а потом изменять их на шаге 5 после того, как понятно что именно он делает. Скорее всего наш код сам подскажет нам хорошее имя.
Четвертый:
Copy Source | Copy HTML
- class TurnaroundCalculator
- def initialize(purchased_at, turnaround)
- @purchased_at = purchased_at
- @turnaround = turnaround
- end
-
- def calculate
- #snip
- end
- end
Пятый:
Copy Source | Copy HTML
- class Quote < ActiveRecord::Base
- def pretty_turnaround
- TurnaroundCalculator.new(purchased_at, turnaround).calculate
- end
- end
Готово! Мы должны запустить тесты и посмотреть как они пройдут. Даже если «запустить тесты» означает проверить вручную…
Так в чем преимущество? Ну, сейчас мы можем начать процесс рефакторинга, но мы в нашей собственной маленькой чистой комнате. Мы можем внести методы в наш класс TurnaroundCalcuator без засорения класса Quote, мы можем написать быстрые тесты только для Calculator и разбить идею вычислений в одном месте, где позже её можно будет легко изменить. Вот наш класс после нескольких рефакторингов:
Copy Source | Copy HTML
- class TurnaroundCalculator
- def calculate
- return "" if @turnaround.nil?
-
- "#{arrival_date} (#{days_from_today} business days from today)"
- end
-
- protected
-
- def arrival_date
- real_turnaround_time.strftime("%A %d %B")
- end
-
- def real_turnaround_time
- adjust_time_for_weekends(start_time + turnaround_in_seconds)
- end
-
- def adjust_time_for_weekends(time)
- if saturday?(time)
- time + 2 * 60 * 60 * 24
- elsif sunday?(time)
- time + 1 * 60 * 60 * 24
- else
- time
- end
- end
-
- def saturday?(time)
- time.strftime("%a") == "Sat"
- end
-
- def sunday?(time)
- time.strftime("%a") == "Sun"
- end
-
- def turnaround_in_seconds
- @turnaround * 60 * 60 * 24
- end
-
- def start_time
- @purchased_at or Time.now
- end
-
- def days_from_today
- if @purchased_at
- ((Time.now - @purchased_at.to_time) / 60 / 60 / 24).floor + 1
- else
- @turnaround + 1
- end
- end
- end
Ух-ты! Этот код, который я написал 3 года назад не идеален, но его почти можно понять. И каждый кусок имеет смысл. Это после 2-х или 3-х волн рефакторинга, которые я, возможно, раскрою в отдельном посте, потому что то, что получилось сейчас несколько более иллюстративно, чем я думал. В любом случае, вы поняли идею. Это то что я имею в виду, когда говорю что нацелен на пяти-строчные методы в Руби; если ваш код понятен, Вам легко его править.
Идея извлечения чистых объектов Руби справедлива и в Rails. Посмотрите на этот маршрут:
Copy Source | Copy HTML
- root :to => 'dashboard#index', :constraints => LoggedInConstraint
А? LoggedInConstraint?
Copy Source | Copy HTML
- class LoggedInConstraint
- def self.matches?(request)
- current_user
- end
- end
О. Да. Объект, описывающий политику маршрутизации. Замечательно. Также примеры валидации, бесстыдно украденные с omgbloglol:
Copy Source | Copy HTML
- def SomeClass < ActiveRecord::Base
- validate :category_id, :proper_category => true
- end
-
- class ProperCategoryValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless record.user.category_ids.include?(value)
- record.errors.add attribute, 'has bad category.'
- end
- end
- end
Это не чистый Руби класс, но вы поняли идею.
Сейчас вы наверно думаете: «Стив, это не только для Rails, ты солгал!» Ну да, вообще-то, вы поймали меня: это не секрет для объектно-ориентированного Rails, это больше общий объектно-ориентированный подход. Но есть кое что специфическое для Rails, что как будто манит вас в ловушку никогда не разбивать классы. Возможно, lib/ кажеться таким хранилищем мусора. Возможно, что 15-ти минутные примеры только включают в себя модели ActiveRecord. Возможно, больше закрытых Rails приложений, (Внимание: лишь мое собственное мнение) чем открытых не-Rails, так что, у нас не так много хороших примеров, на которые можно опереться. (У меня есть такая догадка, поскольку Рельсы часто используются для разработки сайтов для компаний. Гемы? Точно? Мое веб приложение? Не так много. Все же, у меня нет статистики для подтверждения.)
В общем: извлечение объектов предметной области — это хорошо. Они делают ваши тесты быстрыми, код – коротким, и облегчают в дальнейшем внесение изменений. У меня есть еще что сказать об этом, особенно про «быстрые тесты», но я уже исчерпал возможную длину поста. До следующего раза!