Поскольку Ruby — объектно-ориентированный язык, мы склонны моделировать мир как набор объектов. Мы говорим, что два целых числа (x и y) — это точка, а у линии их два.

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

Что происходит, когда вам нужна ячейка? Ну, Point владеет данными. Таким образом, вы добавляете методы своей ячейки в Point. Со временем вы закончите с классом Point, который представляет собой мусорный ящик со слабо связанными методами.

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

Первое правило веб-разработки заключается в том, что мы не говорим о классе User.

В основе любого закаленного в боях рабочего приложения Rails лежит гигантское чудовище класса под названием User.

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

class User attr_accessor :username, :password, :email, :address def authenticate!(password) ... end end

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

class User ... attr_accessor :payment_processor_token, :subscription_plan_id, ...etc def charge_cc ... end end

Здорово! Теперь мы зарабатываем реальные деньги! Генеральный директор решил, что они хотят иметь возможность экспортировать визитные карточки с контактной информацией пользователей, чтобы отдел продаж мог легко импортировать их в SalesForce. Время запуска vim:

class User ... def export_vcard ... end end

Что мы наделали?

Мы начали с класса User, единственной целью которого была обработка аутентификации. Добавив к нему дополнительные методы и атрибуты, мы превратили его в гибрид Франкенштейна пользователя/подписчика/контакта.

Зачем нам это делать? Разве мы не уважаем себя как разработчиков? Наши родители недостаточно любили нас? Или мы настроили себя на неудачу, полагая, что эти данные действительно являются вещью, объектом?

Почему мы говорим, что комбинация имени пользователя, пароля и адреса электронной почты является пользователем? Почему не подписчик? Или Контакт? Или SessionHolder?

Правда о данных заключается в том, что это данные

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

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

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

Вот карикатура на то, как мы могли бы построить нашу пользовательскую систему в Эликсире:

my_user = %{username: "foo", password: ..., phone: ..., payment_token: ...} my_user = Authentication.authenticate(my_user) my_user = Subscription.charge(my_user) my_user = Contact.export_vcard(my_user)

Поскольку данные отделены от кода, ни один модуль не имеет привилегированной интерпретации этих данных.

Возвращаем его к Руби

Поскольку этот подход так хорошо работает в Elixir, почему бы не применить его в Ruby? Ничто не мешает нам превратить User в простую оболочку для его данных и вытащить всю бизнес-логику в модули.

class User attr_accessor :username, :password, :email, :address end module Authentication def self.authenticate!(user) .. end end module Subscription def self.charge(user) .. end end module Contact def self.export_vcard(user) .. end end

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

Ну и что?

Это кажется тривиальным изменением? В чем смысл?

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

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

Другие подходы

Существуют и другие подходы к предотвращению проблемы мусорного ящика. Традиционно в объектно-ориентированном программировании вы пытаетесь сделать это с помощью наследования и тщательного рефакторинга. В 2012 году Санди Мец опубликовала Practical Object Oriented Design in Ruby, которая убедила многих людей начать создавать объекты с помощью внедрения зависимостей. Еще недавно популярность функционального программирования заставила рубистов экспериментировать с неизменяемыми «объектами данных».

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

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

Первоначально опубликовано на blog.honeybadger.io.