Публикувано на 24 август 2015 г.

отслабване

Принципът на единната отговорност

Един клас трябва да има една и само една причина да се промени. ? - чичо Боб

Принципът на единната отговорност твърди, че всеки клас трябва да има точно една отговорност. С други думи, всеки клас трябва да бъде загрижен за един уникален блок от функционалност, независимо дали е User, Post или InvitesController. Обектите, създадени от тези класове, трябва да се занимават с изпращане и отговор на съобщения, свързани с тяхната отговорност, и нищо повече.

Това е често срещана мантра за Rails, която много уроци и следователно много начинаещи следват при създаването на следващото си приложение. Въпреки че моделите на мазнини са малко по-добри от контролерите на мазнини, те все още страдат от същите основни проблеми: когато някоя от многото отговорности на обекта се промени, самият обект трябва да се промени, в резултат на което тези промени се разпространяват в цялото приложение. Изведнъж незначителна промяна на модел счупи половината от тестовете ви!

Предимствата от спазването на принципа на единната отговорност включват (но не се ограничават до):

  • DRYer код: когато всеки бит функционалност е капсулиран в свой обект, вие се оказвате, че повтаряте кода много по-малко.
  • Промяната е лесна: сплотени, свободно свързани обекти прегръщат промяната, тъй като те не знаят или не се интересуват от нищо друго. Промените в Потребител изобщо не влияят на Публикацията, тъй като Пощата дори не знае, че Потребителят съществува.
  • Фокусирани модулни тестове: вместо да организирате усукваща се мрежа от зависимости, само за да настроите тестовете си, обектите с една отговорност могат лесно да бъдат тествани единично, като се възползват от дублирания, подигравки и заглушки, за да се предотврати счупването на вашите тестове почти толкова често.

Никой обект не трябва да бъде всемогъщ, включително модели и контролери. Само защото директорията за приложения на vanilla Rails 4 съдържа модели, изгледи, контролери и помощници, не означава, че сте ограничени до тези четири домейна.

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

Капсулиране на ролите на модела с опасения

Представете си, че създавате прост онлайн сайт за новини, подобен на Reddit или Hacker News. Основното взаимодействие, което потребителят ще има с приложението, е изпращането и гласуването на публикации.

Сега си представете, че искате потребителите да могат да гласуват както за публикации, така и за коментари. Решихте да приложите основна полиморфна асоциация и накрая с това:

Ъъъъ. Вече имате дублиран код с #vote! . За да влошите нещата, сега искате да имате както гласове, така и против.

API на Vote се промени, тъй като .new и .create вече изискват аргумент за тип. В малкия случай само на публикации и коментари това не е голяма промяна. Но какво се случва, ако имате 10 модела, за които може да се гласува? 100?

Проблемите по същество са модули, които ви позволяват да капсулирате ролите на модела в отделни файлове, за да изсушите вашия код. В нашия пример, Post и Comment и двата изпълняват ролята на votable, така че включват загрижеността Votable за достъп до това споделено поведение. Притесненията са страхотни при организирането на различните роли, които играят вашите модели. Притесненията обаче не са решението на модел с твърде много отговорности.

По-долу е даден пример за безпокойство, което все още нарушава принципа на една отговорност.

Този проблем не е нещо, което притесненията са добри при разрешаването. Потребителският модел не трябва да знае за UserMailer. Докато действителният файл user.rb не съдържа никаква препратка към UserMailer, класът User съдържа.

Притесненията са чудесен инструмент за споделяне на поведение между модели, но трябва да се използват отговорно и с ясни намерения.

Намаляване на сложността на контролера с Service обекти

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

Какво точно не е наред с този код, който може да попитате? Единствената отговорност на контролера е да приема HTTP заявки и да отговаря с данни. В горния код изпращането на покана до списък с имейли е пример за бизнес логика, която не принадлежи на контролера. Единичното тестване на изпращането на покани е невъзможно, тъй като тази функция е толкова тясно свързана с InvitesController. Може да помислите да включите тази логика в модела за покана, но това не е много по-добре. Какво би се случило, ако искате подобно поведение в друга част на приложението, която не е свързана с конкретен модел или контролер?

За щастие има решение! Много пъти специфична бизнес логика като групово изпращане на имейли може да бъде капсулирана в обикновен стар рубинен обект (наричан галено PORO). Тези обекти, често наричани обекти на услуга или взаимодействие, приемат вход, извършват работа и връщат резултат. За сложни взаимодействия, които включват създаване и унищожаване на множество записи на различни модели, сервизните обекти са чудесен начин да се капсулира тази отговорност от моделите, контролерите, изгледите и помощните рамки, които Rails предоставя по подразбиране.

В този пример отговорността за изпращане на имейли в групово състояние е преместена от контролера в обект на услугата, наречен BulkInviter. InvitesController не знае и не се интересува как точно BulkInviter постига това; всичко, което прави, е да помоли BulkInviter да изпълни работата си. Макар и много по-добра от версията на контролера за мазнини, все още има място за подобрение. Забележете как InvitesController все още трябва да знае, че BulkInviter има списък с невалидни имейли? Тази допълнителна зависимост допълнително свързва InvitesController с BulkInviter .

Едно от решенията е да обгърнете целия изход от сервизни обекти в обект на отговор.

Сега InvitesController наистина не знае как работи BulkInviter; всичко, което прави, е да поиска BulkInviter да свърши някаква работа и да изпрати отговора на изгледа.

Сервизните обекти са лесен за единичен тест, лесни за промяна и могат да бъдат използвани повторно, докато приложението ви расте. Въпреки това, както всеки модел на проектиране, обектите за услуги имат свързана цена. Злоупотребата с модела на проектиране на обект на услуга често води до тясно свързани обекти, които се чувстват по-скоро като променящи се методи и по-малко като следват принципа на една отговорност. Повече обекти също означава по-голяма сложност и намирането на точното местоположение на дадена характеристика включва ровене в директорията на услугите.

Най-голямото предизвикателство, с което се сблъсках при проектирането на сервизни обекти, е дефинирането на интуитивен API, който лесно съобщава отговорността на обекта. Един от подходите е да се третират тези обекти като procs или lambdas, като се приложи метод #call или #perform, който изпълнява работа. Въпреки че това е чудесно за стандартизиране на интерфейса между обектите на услугата, той силно разчита на описателни имена на класове, за да съобщи отговорността на обекта.

Една идея, която използвах за по-нататъшна комуникация на целта на обектите на услугата, е да ги разпределя в имена в техния специфичен домейн:

Точното внедряване на тези обекти на услуги до голяма степен се основава на стил и зависи от сложността на вашата бизнес логика.

Възползвайки се от Active Record Model

Последната тема, която искам да разгледам, е идеята за модели без маса. Започвайки от Rails 4, можете да включите ActiveModel: Model, за да позволите на обект да взаимодейства с Action Pack, получавайки пълния интерфейс, който се радват на моделите Active Record. Обектите, които включват ActiveModel: Model, не се съхраняват в базата данни, но могат да бъдат създадени с присвояване на атрибути, валидирани с вградени проверки и да имат форми, генерирани с помощници на формуляри и много други!

Кога бихте направили модел без маса? Нека разгледаме един пример!

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

Първият ни опит може да включва някакъв обект на услуга.

Докато това работи, нещо не се чувства в новия ни обект на услугата PasswordChecker. Той не взаимодейства с никакви модели и не променя състоянията. API на сервизния обект е неудобен, тъй като не е ясно дали #perform е заявка или команда. Ако направим крачка назад и помислим каква точно е отговорността на този обект на услугата, скоро стигаме до проверка на силата на паролата. С други думи, PasswordChecker съдържа и проверява набор от данни, подобно на моделите Active Record.

Това е чудесен калъф за модели без маса!

Ние не само получаваме силата на вградените проверки, но става много по-лесно да изобразяваме съобщения за грешки и да генерираме формуляра за изпращане на нова парола.

Загрижеността, сервизните обекти и моделите без маса са отлични начини за борба с нарастващите болки, изпитани при изграждането на приложението Rails. Важно е да не се налагат дизайнерски модели, а да ги откриете, докато изграждате приложението си. В много сценарии изсъхването на ролите на модела с Concerns, създаването на слой обект на услуга между вашите модели и контролери или капсулирането на временни данни в модели без таблици има много смисъл. Друг път мирише много на преждевременна оптимизация и не е най-добрият подход.

Както при всяко програмиране, най-добрият начин да се научите е да си изцапате ръцете!