Ваша веб локација се креће напред, а ви брзо растете. Руби / Раилс је ваша најбоља опција за програмирање. Ваш тим је већи и већ сте напустили концепт „дебели модели, танки покретачи“ ( дебели модели, мршави контролори ) као стил дизајна за ваше Раилс апликације. Међутим, и даље не желите да престанете да користите Раилс.
Нема проблема. Данас ћемо разговарати о томе како користити најбоље праксе из ООП-а како бисте свој код учинили чишћим, изолованијим и одвојенијим.
За почетак, размотрите како бисте требали одлучити да ли је ваша апликација добар кандидат за рефакторирање.
Ево листе показатеља и питања која си обично постављам да бих утврдио да ли мојим кодовима треба рефакторирање или не.
Покушајте да употребите овако нешто да бисте сазнали колико редака Руби изворног кода имате:
find app -iname '*.rb' -type f -exec cat {} ;| wc -l
Ова наредба ће претражити све датотеке са наставком .рб (руби датотеке) у директоријуму / апп, а затим одштампати број редова. Имајте на уму да је овај број само процена, јер ће редови коментара бити укључени у овај укупан број.
Друга тачнија и информативнија опција је коришћење задатка грабље stats
Раилс, који излаже брзи сажетак линија кода, броја класа, броја метода, односа метода према класама и односа линија кода по методи:
*bundle exec rake stats* +----------------------+-------+-----+-------+---------+-----+-------+ | Nombre | Líneas | LOC | Clase | Método | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controladores | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Modelos | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Bibliotecas | 0 | 0 | 0 | 0 | 0 | 0 | | Controlador specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Modelo specs | 238 | 182 | 0 | 0 | 0 | 0 | | Petición specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | Vista specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Código LOC: 262 Prueba LOC: 780 Ratio Código a Prueba: 1:3.0
Почнимо са примером из стварног живота.
Претпоставимо да желимо да напишемо апликацију која прати време за људе који трче; На главној страници корисник може видети унесена времена.
Сваки од уноса времена садржи датум, удаљеност, трајање и додатне релевантне информације о статусу (нпр. Време, тип терена итд.) И просечну брзину која се може израчунати када је потребно.
Потребан нам је извештај који показује просечну брзину и удаљеност недељно. Ако је просечна брзина на улазу већа од укупне просечне брзине, обавестићемо корисника путем СМС-а (за овај пример ћемо користити Некмо РЕСТфул АПИ за слање СМС-а).
Главна страница ће вам омогућити да одаберете удаљеност, датум и време на којем трчите како бисте направили унос сличан овом:
Такође имамо estadísticas
страницу, која је у основи недељни извештај који укључује просечну пређену брзину и удаљеност недељно.
Структура директоријума aplicación
изгледа слично овоме:
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Нећу расправљати о моделу Usuario
јер није ништа необично, јер га користимо са Мото за спровођење аутентификације.
Што се тиче Entrada
модела, он садржи пословну логику за нашу апликацију.
Сваки Entrada
припада а Usuario
.
Проверавамо присуство следећих атрибута за сваки улаз distancia
, períodode_tiempo
, fecha_hora
и estatus
.
Сваки пут када креирамо унос, упоређујемо просечну брзину корисника са просеком свих корисника у систему и обавештавамо корисника путем СМС-а, користећи Некмо (Нећемо разговарати о томе како се користи Некмо библиотека, иако сам желео да демонстрирам случај када користимо спољну библиотеку).
Имајте на уму да Entrada
модел садржи више од саме пословне логике. Такође обрађује неке валидације и позиве.
Тхе entries_controller.rb
има акције Сурово главни (иако без ажурирања). EntriesController#index
добија уносе за тренутног корисника и сортира записе по датуму креирања, док EntriesController#create
креирајте нови унос. Нема потребе да расправљамо о очигледном или одговорностима EntriesController#destroy
:
Док је statistics_controller.rb
одговоран је за израчунавање недељног извештаја, StatisticsController#index
добија улазе за пријављеног корисника и групише их по недељама, користећи #group_by
која је у разреду Бројне у Раилс. Затим покушајте да украсите резултате неким приватним методама.
Овде не расправљамо много о ставовима јер изворни код сам по себи објашњава.
Испод је приказ листе уноса за пријављеног корисника (index.html.erb
). Ово је образац који ће се користити за приказ резултата радње индекса (методе) у обрађивачу уноса:
Имајте на уму да користимо render @entries
Учесници, да делимо код делимично _entry.html.erb
тако да можемо задржати свој код СУВ и за вишекратну употребу:
Исто се односи и на _forma
делимично. Уместо да користимо исти код са (новим и измењеним) радњама, креирамо делимични образац за вишекратну употребу:
Што се тиче изгледа странице недељног извештаја, statistics/index.html.erb
приказује неке статистике и извештава о недељним активностима корисника груписањем неких уноса:
И коначно, помоћник за улазе, entries_helper.rb
, укључује два помагачи readable_time_period
и readable_speed
што би требало олакшати читање атрибута:
За сада ништа превише компликовано.
Већина вас може тврдити да је рефакторирање ово против принципа КИСС и то ће систем учинити сложенијим.
Дакле, да ли ову апликацију заиста треба реконструисати?
Апсолутно не , али ми ћемо га узети у обзир само у сврху узорковања.
На крају, ако погледате следећи одељак и карактеристике које указују на то да је апликацији потребан рефакторинг, постаће очигледно да апликација у нашем примеру није ваљани кандидат за рефакторирање.
Почнимо са објашњавањем структурног обрасца МВЦ у Раилс.
Обично започиње претраживачем подношењем захтева попут https://www.toptal.com/jogging/show/1
.
Веб сервер прима захтев и користи rutas
да дефинише шта controlador
употреба.
Контролори обављају посао анализе захтева корисника, испоруке података, колачића, сесија итд., А затим траже modelo
добити податке.
Тхе modelos
Они су Руби класе које разговарају са базом података, спремају и потврђују податке, извршавају пословну логику и раде тешко. Прикази су оно што корисник може видети: ХТМЛ, ЦСС, КСМЛ, Јавасцрипт, ЈСОН.
Ако желимо да прикажемо редослед захтева за животни циклус програма Раилс, то би изгледало овако:
Оно што желим да постигнем је да додам више апстракције, користећи ПОРО-ове и направим образац сличним следећем, за акције create/update
И нешто слично овоме за акције list/show
:
Додавањем ПОРО апстракција осигуравамо потпуно раздвајање одговорности СИЦКЛЕ , нешто што Раилс не савлада у потпуности.
Да бих постигао нови дизајн, послужићу се смерницама у наставку, али имајте на уму да то нису правила која морате поштовати до краја. Сматрајте их флексибилним смерницама које олакшавају рефакторирање.
АцтивеРецорд модели могу садржати асоцијације и константе, али ништа друго. То значи да неће бити позива (користите услужне објекте и тамо додајте позиве) и неће бити валидације (усе Обликујте објекте да би се укључила имена и валидације за модел).
Држите контролере као танке слојеве и увек позивајте сервисне објекте. Неки од вас се можда питају зашто користити контролере ако желимо да наставимо да позивамо сервисне објекте да садрже логику? Па, контролори су добро место за њихово коришћење усмеравање ХТТП, рашчлањивање параметара, аутентификација, преговарање о садржају, позивање исправне услуге или објекта уређивача, хватање изузетака, форматирање одговора и враћање исправног статуса ХТТП кода.
Упити треба правити са предметима упит . Објектне методе Упит мора вратити предмет, а хасх или низ , али не и АцтивеРецорд асоцијација.
Избегавајте употребу Помагачи , боље користити декоратере. Зашто? Честа потешкоћа са помагачи у Раилс-у је да се они могу претворити у гомилу ОО , који деле име простора и међусобно се преклапају. Али много је горе, чињеница да се ниједан полиморфизам не може користити са помагачи Шине - пружањем различитих примена за различите контексте или типове, и заобилажењем или пот класификацијом помагача. Мислим да врсте помоћник у шинама би се углавном требало користити за корисне методе, а не за посебне случајеве употребе; како форматирати атрибуте модела за било коју логику презентације. Нека буду лагане и лако их је пратити. Боље декоратори / делегати. ** Зашто? Напокон, чини се да су бриге део шина и могу се осушити ( Исушити ) код који се дели између више модела. Међутим, већи је проблем што забринутост не чини предмет модела кохезивнијим. Само је код боље организован. Другим речима, нема стварне промене у АПИ-ју модела.
Покушајте да извучете Драгоцени предмети модела да би ваш код био чистији и да бисте повезали његове атрибуте.
Пре него што почнем, желим да разговарам о нечем другом. Када започне рефакторирање, обично се на крају запитате: „Да ли је то добра рефакторизација?“
Ако се осећате као да правите више раздвајања или изолације између одговорности (чак и ако то значи додавање још кода и нових датотека), онда је ово добра ствар. На крају, одвајање апликације је врло добра пракса и олакшава нам обављање одговарајућег јединственог теста.
Нећу расправљати о стварима, попут пребацивања логике са контролера на моделе, јер претпостављам да то радите и да вам је угодно да користите Раилс (обично Скинни Цонтроллер и ФАТ модел).
Да би овај чланак био сажет, нећу расправљати о тестирању, али то не значи да не бисте требали тестирати.
Напротив, требало би увек почните са тестом како би били сигурни да ствари иду добро, пре него што кренете напред. Ово је нешто потребан, посебно када се ради рефакторирање.
Тада можемо применити промене и осигурати да тестови прођу кроз одговарајуће делове кода.
Прво, шта је предмет вредности?
Мартин Фовлер Објасните:
Објект вредности је мали објекат, као што је новац или предметни опсег. Њихова кључна карактеристика је да следе вредносну семантику, а не референтну семантику.
Понекад се можете наћи у ситуацији када концепт заслужује своју апстракцију и где се његова једнакост не заснива на вредностима, већ на идентитету. Примери за то могу бити: Руби-ов датум, УРИ и име путање. Издвајање драгоценог објекта (или модела домена) велика је погодност.
Зашто гњавити?
Једна од великих предности вредносног објекта је што помажу у постизању изражајности вашег кода. Ваш код ће бити јаснији или барем може бити ако имате добру праксу именовања. С обзиром да је Валуе Објецт апстракција, то доводи до јаснијих кодова и мање грешака.
Још један добитак је непроменљивост . Непроменљивост предмета је веома важна. Када складиштимо одређене скупове података, који се могу користити у вредносном објекту, обично не волим податке којима се манипулише.
Када је ово корисно?
На ово питање нема савршеног одговора. Радите оно што је у вашем најбољем интересу и оно што има највише смисла у датој ситуацији.
Поред овога, постоје неке смернице које ми помажу да донесем одлуку.
Ако мислите да је група метода повезана, па, са драгоценостима је скупља. Ова експресивност значи да вредносни објекат треба да представља препознатљив скуп података, који ваш просечни програмер може закључити само гледањем имена објекта.
Како се то ради?
Вредности треба да следе одређена правила:
У нашем примеру, креираћу вредносни објекат EntryStatus
, да апстрахујем атрибуте Entry#status_weather
и Entry#status_landform
својој класи, која изгледа овако:
Напомена: Ово је само ПОРО (обичан стари руби објекат), не наслеђује се из ActiveRecord::Base
. Дефинисали смо методе читача за наше атрибуте и додељујемо их приликом покретања. Такође, користимо упоредиву комбинацију за подударање објеката помоћу методе ().
Можемо модификовати модел Entry
да користимо објект вредности који смо креирали:
Такође можемо изменити методу EntryController#create
да сходно томе употребимо нови објект вредности:
Шта је услужни објекат?
Посао услужног објекта је задржавање кода током одређеног простора пословне логике. За разлику од стила 'Дебели модел' , где мали број објеката садржи много, много метода, за сву потребну логику, коришћење услужних објеката резултира у многим класама, од којих свака има јединствену сврху.
Зашто? Које су предности?
Искидати. Услужни објекти помажу вам да постигнете већу изолацију између објеката.
Видљивост. Објекти услуге (ако су правилно именовани) показују шта апликација ради. Могу да погледам директоријум услуга да видим које могућности нека апликација пружа.
СУВ и Прихватите промену. Услужне предмете држим што једноставнијим и мањим. Компонујем сервисне објекте са другим услужним објектима и поново их користим.
Очистите и убрзајте свој тестни пакет. Услуге се брзо и лако тестирају, јер су то мали Руби објекти са улазном тачком (метода која се назива). Сложене услуге се састоје од других услуга, тако да можете лако раздвојити тестове. Такође, коришћење услужних објеката олакшава преузимање повезаних објеката без потребе за учитавањем читавог окружења шина.
С друге стране, ништа није савршено. Један недостатак Услужних објеката је тај што могу бити претерани за сваку малу радњу. У тим случајевима можете на крају компликовати и не поједноставити свој код.
Када треба издвојити услужне објекте?
Ни овде не постоји фиксно правило.
Типично су услужни објекти најбољи за средње до велике системе: оне са пристојном количином логике, изван стандардних ЦРУД операција.
Дакле, када мислите да комад кода не припада директоријуму, на месту где сте га намеравали додати, било би добро да га преиспитате и видите да ли би било боље да иде на услужни објекат.
Ево неколико упута када треба користити услужне објекте:
Како треба да дизајнирате услужне објекте?
Дизајнирање класе за услужни објекат релативно је једноставно, јер вам није потребно драгуље не би требало да учите а ДЛС нова, али можете се, мање или више, ослонити на вештине дизајнирања софтвера које већ поседујете.
Обично користим следеће смернице и конвенције за дизајн објекта услуге:
app/services
. Саветујем вам да користите поддиректоријуме за јаке домене пословне логике. На пример, датотека app/services/report/generate_weekly.rb
дефинисаће Report::GenerateWeekly
док app/services/report/publish_monthly.rb
дефинисаће Report::PublishMonthly
.ApproveTransaction
, SendTestNewsletter
, ImportUsersFromCsv
.Ако погледате StatisticsController#index
, приметићете групу метода (weeks_to_date_from
, weeks_to_date_to
, avg_distance
, итд.) Груписане у контролер. То није добро. Размотрите последице ако желите да генеришете недељни извештај изван statistics_controller
.
У нашем случају ћемо створити Report::GenerateWeekly
и извуците логички извештај из StatisticsController
:
Дакле StatisticsController#index
сада изгледа чистије:
Применом обрасца објекта Сервице, ми групишемо код око сложене и специфичне акције и промовишемо стварање мањих и јаснијих метода.
Домаћи задатак: размислите о коришћењу Вредан објекат за WeeklyReport
уместо Struct
.
Шта је објекат Упит ?
Објекат Упит је ПОРЕ, који представља базу података упита. Може се поново користити на различитим местима у апликацији, истовремено скривајући логику упита. Такође пружа добру изоловану јединицу за испитивање.
Требали бисте извући сложене СКЛ / НоСКЛ упите у њихове властите класе.
Сваки објекат Упит одговоран је за враћање скупа резултата на основу критеријума / правила пословања.
У овом примеру немамо упит ( упит ) сложен, па користи објект Упит не би било ефикасно. Међутим, у сврху демонстрације, издвојићемо упит у Report::GenerateWeekly#call
и ми ћемо створити generate_entries_query.rb
:
А у Report::GenerateWeekly#call
заменимо:
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
са:
def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
Узорак предмета упит (упит) помаже да логика вашег модела буде строго повезана са понашањем класе, а да истовремено контролори остану мршави. Јер они нису ништа друго до обичне старе класе Руби , предмети упит не требају насљеђивати од ActiveRecord::Base
и не би требали бити одговорни за ништа осим за извршавање упита.
Сада ћемо извући логику стварања новог уноса у нови објект услуге. Искористимо конвенцију и креирајмо CreateEntry
:
А сада наш EntriesController#create
је као што следи:
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
Сад ствари постају занимљивије.
Запамтите да смо се у нашим смерницама сложили да желимо да модели имају асоцијације и константе, али ништа друго (без валидације или позива). Па кренимо уклањањем додатних описа и уместо тога користимо облик Схапе.
Објект Схапе је ПОРО (обичан стари рубин објекат). Преузмите команду над објектом контролера / услуге када требате разговарати с базом података.
Зашто користити Схапе објекте?
Када требате да рефакторизујете своју апликацију, увек је добро имати на уму, главну појединачну одговорност ( СИЦКЛЕ ).
СИЦКЛЕ помаже вам у доношењу бољих дизајнерских одлука у погледу одговорности коју треба да има час.
На пример, модел табеле базе података (модел АцтивеРецорд у контексту програма Раилс) представља јединствени запис базе података у коду, тако да нема разлога да се бринете о било чему што ваш корисник ради.
Овде долази објект Схапе.
Објект Схапе одговоран је за представљање облика у вашој апликацији. Дакле, свако поље за унос може се третирати као атрибут у класи. Можете проверити да ли ти атрибути испуњавају нека правила за проверу ваљаности и можете проследити „чисте“ податке тамо где треба (нпр. Модел базе података или можда ваш креатор претраживања упита).
Када треба да користите објект Схапе?
То вам омогућава да на једно место ставите сву логику облика (конвенције именовања, валидације и друго).
Како створити Схапе објект?
ActiveModel::Model
(У Раилс 3, уместо тога, морате да укључите Име, Конверзију и Валидацију).Имајте на уму да можете да користите драгуљ реформа , али настављајући са ПОРО, створићемо entry_form.rb
што изгледа овако:
И ми ћемо изменити CreateEntry
за почетак коришћења објекта Формат EntryForm
:
class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
Белешка: Неки од вас ће рећи да није потребно приступити објекту Схапе из објекта Сервице и да објект Схапе можемо позвати директно из контролера, што је ваљан аргумент. Међутим, радије бих имао јасан ток, зато увек зовем Схапе објект из Сервице објекта.
Као што смо се раније договорили, не желимо да наши модели садрже потврде и позиве. Издвојили смо валидације помоћу Схапе објеката. Али и даље користимо неке позиве (after_create
у моделу Entry
compare_speed_and_notify_user
).
Зашто желимо да уклонимо додатне описе са модела?
Раилс Девелоперс обично почну да примећују бол током позива, током тестова. Ако не тестирате своје АцтивеРецорд моделе, бол ћете почети примећивати касније, како ваша апликација расте и што је више логике потребно за позивање или избегавање позива.
después_*
позиви се користе првенствено у односу на чување или наставак са објектом.
Једном када је објекат сачуван, сврха објекта (нпр. Одговорност) је испуњена. Дакле, ако и даље видимо да се позивају позиви, након што је објекат сачуван, то су вероватно позиви који желе да изађу из подручја одговорности објекта и тада наилазимо на проблеме.
У нашем случају, кориснику шаљемо СМС који није повезан са улазном доменом.
Једноставан начин за решавање проблема је премештање позива у повезани услужни објекат. На крају крајева, слање СМС-а одговарајућем кориснику повезано је са Услужним објектом CreateEntry
а не модел Ентри, као такав.
Радећи ово, више не морамо да искључујемо, compare_speed_and_notify_user
у нашим тестовима. Ово смо учинили једноставним, створили смо унос без потребе за слањем СМС-а и пратимо добар објектно оријентисан дизајн, водећи рачуна да наши часови имају јединствену одговорност СИЦКЛЕ ).
Дакле, сада CreateEntry
то је нешто слично овоме:
Иако колекцију можемо лако користити Драпер модела и декоратера, остајем при ПОРО-у за овај чланак, као и до сада.
Оно што ми треба је класа која позива методе на украшеном објекту.
Могу да користим method_missing
да то имплементирам, али користићу стандардну Руби библиотеку, SimpleDelegator
. Следећи код показује како се користи SimpleDelegator
да имплементирамо наш основни декоратор:
% app/decorators/base_decorator.rb require 'delegate' class BaseDecorator Зашто метода _h
Овај метод делује као прокси за контекст приказа. Подразумевано је контекст приказа инстанца класе погледа, која је ActionView::Base
. Можете приступити помагачи погледа на следећи начин:
_h.content_tag :div, 'my-div', class: 'my-class'
Да би било погодније додајемо метод decorado
а ApplicationHelper
:
module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= '#{object.class}Decorator'.constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
Сада можемо да померимо помагачи EntriesHelper
декоратерима:
# app/decorators/entry_decorator.rb class EntryDecorator И можемо користити readable_time_period
и readable_speed
као што следи:
# app/views/entries/_entry.html.erb - +
- +
Структура након рефакторирања
Завршили смо са више датотека, али то није нужно лоше (и запамтите ово, од почетка смо били свесни да је овај пример у демонстрацијске сврхе и не је нужно био добар случај за рефакторирање):
app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
закључак
Иако се у овом посту на блогу фокусирамо на Раилс, РоР (Руби он Раилс) није зависност од услужних објеката или других ПОРО-ова. Овај приступ можете користити са било којим фрамеворк веб , апликација за мобилне уређаје или конзоле.
Када користиш МВЦ Као архитектура, све се држи и успорава јер већина промена утиче на друге делове апликације. Такође вас приморава да размислите где да ставите неку пословну логику - да ли то треба да иде у модел, контролер или поглед?
Користећи једноставан ПОРО, пребацили смо пословну логику на моделе или услуге који се не наслеђују из ActiveRecord
, што је већ добитак, а да не помињемо да имамо јаснији код који подржава СИЦКЛЕ и бржи јединични тестови.
Чиста архитектура покушава да постави оквире за употребу у средиште / врх ваше структуре тако да можете лако видети шта ваша апликација ради. Такође олакшава усвајање промена, јер је више модуларна и изолована. Надам се да сам показао како треба Обични стари рубинасти објекти и више апстракција, раздваја бриге, поједностављује тестирање и помаже у стварању чистог и одрживог кода.
Повезан: Које су предности Руби он Раилс? После две деценије програмирања. Користите шине