17 октомври 8 минути четене

модела

7 Модели за рефакториране на мазнини ActiveRecord модели

Саша
Резвина

Дял

свързани етикети

Когато екипите използват Code Climate, за да подобрят качеството на своите Rails приложения, те се научават да се отказват от навика да позволяват на моделите да дебелеят. „Дебелите модели“ причиняват проблеми с поддръжката в големи приложения. Само постепенно по-добри от затрупването на контролери с логика на домейна, те обикновено представляват провал в прилагането на Единия принцип на отговорност (SRP). „Всичко, свързано с това, което прави потребителят“, не е единствена отговорност.

В началото SRP е по-лесен за прилагане. Класовете ActiveRecord се справят с постоянството, асоциациите и не много други. Но малко по малко, те растат. Обектите, които по своята същност са отговорни за постоянството, стават фактически собственик и на цялата бизнес логика. И година или две по-късно имате потребителски клас с над 500 реда код и стотици методи в неговия публичен интерфейс. Възниква ад за обратно обаждане.

Докато добавяте повече присъща сложност (прочетете: функции!) Към вашето приложение, целта е да го разпространите в координиран набор от малки, капсулирани обекти (и на по-високо ниво модули), точно както бихте могли да разнесете тестото за торта в дъно на тиган. Дебелите модели са като големите натрупвания, които получавате, когато за първи път изсипете тестото. Рефактор, за да ги разбие и да разпредели логиката равномерно. Повторете този процес и накрая ще получите набор от прости обекти с добре дефинирани интерфейси, работещи заедно в истинска симфония.

Може би си мислите:

„Но Rails наистина затруднява правилното извършване на ООП правилно!“

И аз вярвах в това. Но след известно проучване и практика разбрах, че Rails (рамката) не възпрепятства толкова много OOP. Това са „конвенциите“ на Rails, които не се мащабират, или по-точно липсата на конвенции за управление на сложността извън това, което моделът на Active Record може елегантно да обработи. За щастие можем да приложим OO-базирани принципи и най-добри практики там, където липсва Rails.

Не извличайте миксини от мазнини

Нека махнем това от пътя. Обезсърчавам изваждането на набори от методи от голям клас ActiveRecord в „проблеми“ или модули, които след това се смесват само в един модел. Веднъж чух някой да казва:

„Всяко приложение с директория за приложения/притеснения е свързано.“

И аз съм съгласен. Предпочитайте композицията пред наследяването. Използването на миксини като този е подобно на „почистване“ на разхвърляна стая, като изхвърлите бъркотията в шест отделни чекмеджета за боклуци и ги затворите. Разбира се, изглежда по-чист на повърхността, но чекмеджетата всъщност затрудняват идентифицирането и прилагането на разлаганията и извличанията, необходими за изясняване на модела на домейна.

Сега на рефакторинга!

1. Извличане на обекти на стойност

Обектите на стойност са прости обекти, чието равенство зависи по-скоро от тяхната стойност, отколкото от идентичността. Те обикновено са неизменни. Дата, URI и Pathname са примери от стандартната библиотека на Ruby, но приложението ви може (и почти сигурно трябва) да дефинира и специфични за домейна обекти на стойност. Извличането им от ActiveRecords е плод с ниско висящо рефакториране.

В Rails обектите на стойност са чудесни, когато имате атрибут или малка група атрибути, които имат логика, свързана с тях. Всичко, което е повече от основните текстови полета и броячи, са кандидати за извличане на обект на стойност.

Например приложение за текстови съобщения, по което съм работил, е имало обект на стойност PhoneNumber. Приложението за електронна търговия се нуждае от клас пари. Code Climate има стойностен обект, наречен Рейтинг, който представлява проста A - F оценка, която всеки клас или модул получава. Можех (и първоначално го направих) да използвам екземпляр на Ruby String, но Рейтингът ми позволява да комбинирам поведение с данните:

След това всеки ConstantSnapshot излага екземпляр от Рейтинг в публичния си интерфейс:

Освен отслабване на класа ConstantSnapshot, това има редица предимства:

  • #Worse_than? и # Better_than? методите осигуряват по-изразителен начин за сравняване на рейтинги от вградените оператори на Ruby (напр. и>).
  • Определяне на #hash и #eql? дава възможност да се използва рейтинг като хеш ключ. Code Climate използва това, за да идиоматично групира константи по техните оценки, използвайки Enumberable # group_by .
  • Методът #to_s ми позволява да интерполирам рейтинг в низ (или шаблон) без никаква допълнителна работа.
  • Определението на класа осигурява удобно място за фабричен метод, като връща правилната оценка за даден „разход за възстановяване“ (очакваното време, необходимо за фиксиране на всички миризми в даден клас).

2. Извличане на обекти на услугата

Някои действия в системата налагат обект на услуга да капсулира тяхната работа. Посягам към Service Objects, когато дадено действие отговаря на един или повече от следните критерии:

  • Действието е сложно (например закриване на книгите в края на счетоводен период)
  • Действието обхваща множество модели (напр. Покупка чрез електронна търговия с помощта на обекти Order, CreditCard и Customer)
  • Действието взаимодейства с външна услуга (например публикуване в социални мрежи)
  • Действието не е основна грижа на основния модел (напр. Почистване на остарели данни след определен период от време).
  • Има няколко начина за извършване на действието (например удостоверяване с токен за достъп или парола). Това е моделът на Бандата на четири стратегии.

Като пример бихме могли да извадим метод за удостоверяване на User # в UserAuthenticator:

И SessionsController ще изглежда така:

3. Извличане на обекти на формуляри

Когато множество модели на ActiveRecord могат да бъдат актуализирани чрез едно подаване на формуляр, обект на формуляр може да капсулира агрегирането. Това е далеч по-чисто от използването на accepts_nested_attributes_for, които, по мое скромно мнение, трябва да бъдат оттеглени. Често срещан пример би бил формуляр за регистрация, който води до създаването както на компания, така и на потребител:

Започнах да използвам Virtus в тези обекти, за да получа функционалност, подобна на ActiveRecord. Обектът на формуляр кряка като ActiveRecord, така че контролерът остава запознат:

Това работи добре за прости случаи като горните, но ако логиката за постоянство във формуляра стане твърде сложна, можете да комбинирате този подход с Service Object. Като бонус, тъй като логиката за валидиране често е контекстуална, тя може да бъде дефинирана точно там, където има значение, вместо да е необходимо да се пази валидирането в самия ActiveRecord.

4. Извличане на обекти на заявка

За сложни SQL заявки, съдържащи дефиницията на вашия подклас ActiveRecord (или като обхвати или като методи на клас), помислете за извличане на обекти на заявка. Всеки обект на заявка отговаря за връщането на набор от резултати въз основа на бизнес правила. Например обект на заявка за намиране на изоставени проби може да изглежда така:

Можете да го използвате във фонова работа за изпращане на имейли:

Тъй като ActiveRecord: Примерите за връзки са първокласни граждани от Rails 3, те правят чудесен принос към обект на заявка. Това ви позволява да комбинирате заявки, използвайки композиция:

Не се притеснявайте да тествате клас като този изолирано. Използвайте тестове, които упражняват обекта и базата данни заедно, за да сте сигурни, че правилните редове се връщат в правилния ред и се извършват всякакви обединения или нетърпеливи натоварвания (напр. За избягване на грешки в заявките N + 1).

5. Въведете View Objects

Ако логиката е необходима чисто за целите на дисплея, тя не принадлежи към модела. Задайте си въпроса: „Ако прилагах алтернативен интерфейс на това приложение, като гласов активиран потребителски интерфейс, ще ми трябва ли това?“. Ако не, помислете дали да го поставите в помощник или (често по-добре) обект View.

Например, диаграмите на поничките в Code Climate разбиват рейтингите на класа въз основа на моментна снимка на кодовата база (например Rails on Code Climate) и се капсулират като изглед:

Често намирам индивидуална връзка между Views и шаблони на ERB (или Haml/Slim). Това ме накара да започна да разследвам внедряванията на двустепенния изглед, който може да се използва с Rails, но все още нямам ясно решение.

Забележка: Терминът „Презентатор“ се улови в общността на Ruby, но го избягвам заради багажа и противоречивата му употреба. Терминът „Презентатор“ е въведен от Джей Фийлд, за да опише това, което аз наричам по-горе като „Обекти на формуляри“. Също така, Rails за съжаление използва термина „изглед“, за да опише това, което иначе е известно като „шаблони“. За да избегна двусмислие, понякога наричам тези изгледи обекти като „Преглед на модели“.

6. Извличане на обекти на политиката

Понякога сложните операции за четене може да заслужават собствените си обекти. В тези случаи посягам към обект на политиката. Това ви позволява да запазите тангенциалната логика, като например кои потребители се считат за активни за целите на анализа, извън вашите основни обекти на домейна. Например:

Този обект на политиката капсулира едно бизнес правило, според което потребителят се счита за активен, ако има потвърден имейл адрес и е влязъл в системата през последните две седмици. Можете също да използвате обекти на политики за група бизнес правила като упълномощител, който регулира до кои данни потребителят има достъп.

Обектите на политики са подобни на обектите на услугата, но аз използвам термина „Service Object“ за операции по запис и „Object Policy“ за четения. Те също са подобни на Query Objects, но Query Objects се фокусират върху изпълнението на SQL, за да върнат набор от резултати, докато обектите на политики работят върху модели на домейни, вече заредени в паметта.

7. Извличане на декоратори

Декораторите ви позволяват да сложите функционалността на съществуващите операции и следователно изпълняват подобна цел на обратните обаждания. В случаите, когато логиката за обратно извикване трябва да се изпълнява само при някои обстоятелства или включването й в модела би дало на модела твърде много отговорности, е полезен декоратор.

Публикуването на коментар в публикация в блог може да задейства публикация на нечия стена във Facebook, но това не означава, че логиката трябва да бъде твърдо свързана с класа „Коментар“. Един знак, че сте добавили твърде много отговорности при обратните обаждания, са бавни и чупливи тестове или желание да се намалят страничните ефекти в напълно несвързани тестови случаи.

Ето как можете да извлечете логиката за публикуване във Facebook в декоратор:

И как един контролер може да го използва:

Декораторите се различават от Service Objects, защото наслояват отговорностите към съществуващите интерфейси. Веднъж декорирани, сътрудниците просто се отнасят към екземпляра на FacebookCommentNotifier като към коментар. В стандартната си библиотека Ruby предлага редица съоръжения за улесняване на декораторите на сгради с метапрограмиране.

Обобщавайки

Дори в приложението Rails има много инструменти за управление на сложността в слоя на модела. Нито един от тях не изисква да изхвърлите Rails. ActiveRecord е фантастична библиотека, но всеки модел се разпада, ако зависим изключително от него. Опитайте да ограничите ActiveRecords до поведение на постоянство. Поръсете с някои от тези техники, за да разпространите логиката във вашия домейн модел и резултатът ще бъде много по-поддържаемо приложение.

Също така ще забележите, че много от описаните тук модели са доста прости. Обектите са просто „Обикновени стари рубинени обекти“ (PORO), използвани по различни начини. И това е част от смисъла и красотата на ООП. Не е необходимо всеки проблем да бъде решаван чрез рамка или библиотека, а именуването има голямо значение.

Какво мислите за седемте техники, които представих по-горе? Кои са вашите любими и защо? Пропуснал ли съм някой? Кажете ми в коментарите!

P.S. Ако тази публикация ви е била полезна, може да искате да се абонирате за нашия имейл бюлетин (вижте по-долу). Той е с малък обем и включва съдържание за OOP и рефакторинг на Rails приложения като това.

Допълнителна информация

Благодарим на Стивън Бристол, Пьотър Солница, Дон Морисън, Джейсън Рулофс, Джайлс Боукет, Джъстин Ко, Ърни Милър, Стив Клабник, Пат Мадокс, Сергей Нартимов и Ник Готие за прегледа на тази публикация.