Масни контролери и модели: неизбежни проблем за већину великих пројеката заснованих на МВЦ оквирима као што су Иии и Ларавел. Примарна ствар која тови контролере и моделе је Ацтиве Рецорд , моћна и суштинска компонента таквих оквира.
Активни запис је архитектонски образац, приступ приступу подацима у бази података. Назвао га је Мартин Фовлер у својој књизи из 2003. године Обрасци архитектуре пословних апликација и широко се користи у ПХП Оквири.
Упркос чињеници да је то веома потребан приступ, образац активне евиденције (АР) крши принцип јединствене одговорности (СРП) јер модели АР:
Ово кршење СРП-а представља добру замену за брзи развој када треба што пре створити прототип апликације, али је прилично штетно када апликација прерасте у средњи или велики пројекат. „Божје“ моделе и масне контролере је тешко тестирати и одржавати, а слободно коришћење модела свуда у контролерима доводи до огромних потешкоћа када неизбежно морате променити структуру базе података.
Решење је једноставно: поделите одговорност Ацтиве Рецорд-а на неколико слојева и убризгајте зависне слојеве. Овај приступ ће такође поједноставити тестирање јер вам омогућава да се ругате оним слојевима који се тренутно не тестирају.
„Дебела“ ПХП МВЦ апликација има зависности свуда, међусобно се блокира и склона је грешкама, док слојевита структура користи ињекцију зависности како би ствари биле чисте и јасне.
Пет основних слојева ћемо обрадити:
Да бисмо применили слојевиту структуру, потребан нам је контејнер за убризгавање зависности , објекат који зна како да инстанцира и конфигурише објекте. Не треба да креирате класу јер оквир управља са свом чаролијом. Узмите у обзир следеће:
class SiteController extends IlluminateRoutingController { protected $userService; public function __construct(UserService $userService) { $this->userService = $userService; } public function showUserProfile(Request $request) { $user = $this->userService->getUser($request->id); return view('user.profile', compact('user')); } } class UserService { protected $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function getUser($id) { $user = $this->userRepository->getUserById($id); $this->userRepository->logSession($user); return $user; } } class UserRepository { protected $userModel, $logModel; public function __construct(User $user, Log $log) { $this->userModel = $user; $this->logModel = $log; } public function getUserById($id) { return $this->userModel->findOrFail($id); } public function logSession($user) { $this->logModel->user = $user->id; $this->logModel->save(); } }
У горњем примеру, UserService
се убризгава у SiteController
, UserRepository
се убризгава у UserService
и АР модели User
и Logs
се убризгавају у UserRepository
класа. Овај код контејнера је прилично једноставан, па разговарајмо о слојевима.
Савремени МВЦ оквири попут Ларавел и Иии преузимају многе традиционалне изазове за вас: Провера ваљаности уноса и предфилтери се премештају у други део апликације (у Ларавелу је то што се зове миддлеваре док се у Иии то зове понашање ) док се рутирањем и ХТТП глаголским правилима рукује у оквиру. Ово оставља врло уску функционалност програмеру да кодира у контролер.
Суштина контролера је добијање захтева и достављање резултата. Контролер не би требало да садржи било какву пословну логику; у супротном је тешко поново користити код или променити начин на који апликација комуницира. Ако, на пример, требате да креирате АПИ уместо приказивања приказа, а ваш контролер не садржи никакву логику, само промените начин на који враћате податке и спремни сте.
Овај танки слој контролера често збуњује програмере, а будући да је контролер задани слој и највиша улазна тачка, многи програмери само додају нови код својим контролерима без икаквог додатног размишљања о архитектури. Као резултат, прекомерне одговорности се додају, одговорности попут:
Размотримо пример преинжењерираног контролера:
//A bad example of a controller public function user(Request $request) { $user = User::where('id', '=', $request->id) ->leftjoin('posts', function ($join) { $join->on('posts.user_id', '=', 'user.id') ->where('posts.status', '=', Post::STATUS_APPROVED); }) ->first(); if (!empty($user)) { $user->last_login = date('Y-m-d H:i:s'); } else { $user = new User(); $user->is_new = true; $user->save(); } return view('user.index', compact('user')); }
Зашто је овај пример лош? Из бројних разлога:
last_login
поље, морате га променити у свим контролерима.Контролер треба да буде танак; заиста, све што треба да уради је да узме захтев и врати резултате. Ево доброг примера:
//A good example of a controller public function user (Request $request) { $user = $this->userService->getUserById($request->id); return view('user.index', compact('user')); }
Али где нестају све те друге ствари? Припада сервисни слој .
Сервисни слој је слој пословне логике. Овде и само овде треба да буду смештене информације о току пословних процеса и интеракцији између пословних модела. Ово је апстрактни слој и он ће се разликовати за сваку апликацију, али општи принцип је неовисност од вашег извора података (одговорност контролора) и складиштења података (одговорност доњег слоја).
Ово је фаза са највише потенцијала за проблеме раста. Често се модел активног записа враћа у контролер, и као резултат тога, приказ (или у случају АПИ одговора контролер) мора да ради са моделом и да буде свестан његових атрибута и зависности. То ствари чини неуредним; ако одлучите да промените релацију или атрибут модела активног записа, морате је променити свуда у свим својим приказима и контролерима.
Ево уобичајеног примера модела Ацтиве Рецорд који се користи у приказу:
@foreach($user->posts as $post) - {{$post->title}}
@endforeach
Изгледа директно, али ако преименујем first_name
поље, одједном морам да променим све приказе који користе поље овог модела, процес подложан грешкама. Најлакши начин да се избегне ова загонетка је коришћење објеката за пренос података или ДТО-а.
Податке из сервисног слоја треба умотати у једноставан непроменљиви објекат - што значи да се не могу променити након што се креира - тако да нам за ДТО нису потребни никакви постављачи. Даље, класа ДТО треба да буде независна и да не проширује ниједан модел Ацтиве Рецорд. Ипак, опрезно - пословни модел није увек исти као АР модел.
Размотрите захтев за доставу намирница. Логично, поруџбина у продавници мора да садржи информације о испоруци, али у бази података складиштимо поруџбине и повезујемо их са корисником, а корисник је повезан са адресом за испоруку. У овом случају постоји више АР модела, али горњи слојеви не би требало да знају за њих. Наша ДТО класа ће садржати не само поруџбину већ и информације о испоруци и било које друге делове који су у складу са пословним моделом. Ако променимо АР моделе повезане са овим пословним моделом (на пример, податке о испоруци преместимо у табелу поруџбина), променићемо само мапирање поља у ДТО објекту, уместо да променимо вашу употребу поља АР модела свуда у коду.
Користећи ДТО приступ, уклањамо искушење да променимо модел активног записа у контролеру или у приказу. Друго, ДТО приступ решава проблем повезаности између физичког складиштења података и логичког представљања апстрактног пословног модела. Ако нешто треба променити на нивоу базе података, промене ће утицати на ДТО објекат, а не на контролере и погледе. Видите образац?
Погледајмо једноставан ДТО:
//Example of simple DTO class. You can add any logic of conversion from an Active Record object to business model here class DTO { private $entity; public static function make($model) { return new self($model); } public function __construct($model) { $this->entity = (object) $model->toArray(); } public function __get($name) { return $this->entity->{$name}; } }
Коришћење нашег новог ДТО-а је једнако једноставно:
//usage example public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); return view('user.index', compact('user')); }
За раздвајање логике приказа (попут избора боје дугмета на основу неког статуса) има смисла користити додатни слој декоратора. А. декоратер је дизајнерски образац који омогућава украшавање основног објекта омотавањем прилагођеним методама. Обично се то дешава у погледу са помало посебном логиком.
Иако ДТО објекат може да обавља посао декоратора, он заиста функционише само за уобичајене радње попут форматирања датума. ДТО треба да представља пословни модел, док декоратер украшава податке ХТМЛ-ом за одређене странице.
Погледајмо фрагмент иконе статуса корисничког профила који не користи декоратера:
@if($user->status == AppModelsUser::STATUS_ONLINE) Online @else Offline @endif {{date('F j, Y', strtotime($user->lastOnline))}}
Иако је овај пример једноставан, програмеру би било лако да се изгуби у сложенијој логици. Овде долази декоратор, како би се очистила читљивост ХТМЛ-а. Проширимо исечак иконе статуса у целу класу декоратора:
class UserProfileDecorator { private $entity; public static function decorate($model) { return new self($model); } public function __construct($model) { $this->entity = $model; } public function __get($name) { $methodName = 'get' . $name; if (method_exists(self::class, $methodName)) { return $this->$methodName(); } else { return $this->entity->{$name}; } } public function __call($name, $arguments) { return $this->entity->$name($arguments); } public function getStatus() { if($this->entity->status == AppModelsUser::STATUS_ONLINE) { return 'Online'; } else { return 'Offline'; } } public function getLastOnline() { return date('F j, Y', strtotime($this->entity->lastOnline)); } }
Коришћење декоратера је једноставно:
public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); $user = UserProfileDecorator::decorate($user); return view('user.index', compact('user')); }
Сада у приказу можемо користити атрибуте модела без икаквих услова и логике, и много је читљивији:
{{$user->status}} {{$user->lastOnline}}
Декоратори се такође могу комбиновати:
public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); $user = UserDecorator::decorate($user); $user = UserProfileDecorator::decorate($user); return view('user.index', compact('user')); }
Сваки декоратер ће радити свој посао и украшавати само свој део. Ово рекурзивно уграђивање неколико декоратера омогућава динамичну комбинацију њихових карактеристика без увођења додатних класа.
Слој спремишта ради са конкретном имплементацијом складиштења података. Најбоље је убризгати спремиште кроз интерфејс ради флексибилности и једноставне замене. Ако промените складиште података, морате да направите ново спремиште које имплементира интерфејс спремишта, али бар не морате да мењате остале слојеве.
Спремиште игра улогу објекта упита: добија податке из базе података и спроводи рад неколико модела активних записа. У овом контексту модели активних записа играју улогу појединачних ентитета модела података - било који објекат у систему за који желите да моделирате и чувате информације. Иако сваки ентитет садржи информације, он не зна како су се појавили (да ли су створени или добијени из базе података) или како да сачува и промени сопствено стање. Одговорност спремишта је да сачува и / или ажурира ентитет; ово обезбеђује боље раздвајање забринутости задржавањем управљања ентитетима у спремишту и поједностављивањем ентитета.
Ево директног примера методе спремишта која гради упит користећи знање о бази података и односима активног записа:
public function getUsers() { return User::leftjoin('posts', function ($join) { $join->on('posts.user_id', '=', 'user.id') ->where('posts.status', '=', Post::STATUS_APPROVED); }) ->leftjoin('orders', 'orders.user_id', '=', 'user.id') ->where('user.status', '=', User::STATUS_ACTIVE) ->where('orders.price', '>', 100) ->orderBy('orders.date') ->with('info') ->get(); }
У новоствореној апликацији наћи ћете само директоријуме за контролере, моделе и погледе. Ни Иии ни Ларавел не додају додатне слојеве у структуру своје примере апликације. Лака и интуитивна, чак и за почетнике, МВЦ структура поједностављује рад са оквиром, али важно је схватити да је њихов пример примера пример; то није стандард или стил и не намеће никаква правила о архитектури апликације. Подјелом задатака у одвојене слојеве са једном одговорношћу добијамо флексибилну и прошириву архитектуру која се лако одржава. Запамтити:
Дакле, ако започнете сложени пројекат или пројекат који има шансе да расте у будућности, размислите о јасној подели одговорности на слојеве контролера, услуге и спремишта.