Rasvan säätimet ja mallit: väistämätön ongelma useimmille MVC-kehyksiin perustuville suurille projekteille, kuten Yii ja Laravel. Tärkein asia, joka lihottaa ohjaimia ja malleja, on Aktiivinen ennätys , voimakas ja olennainen osa tällaisia kehyksiä.
Aktiivinen tietue on arkkitehtoninen malli, lähestymistapa tietojen käyttämiseen tietokannassa. Sen nimitti Martin Fowler kirjassaan 2003 Yrityssovellusarkkitehtuurin mallit ja sitä käytetään laajalti PHP Kehykset.
Huolimatta siitä, että se on erittäin välttämätön lähestymistapa, Active Record (AR) -malli rikkoo yhden vastuun periaatetta (SRP), koska AR-mallit:
Tämä SRP-rikkomus on hyvä kompromissi nopealle kehitykselle, kun sinun on luotava sovelluksen prototyyppi mahdollisimman pian, mutta se on melko haitallista, kun sovelluksesta kasvaa keski- tai laajamittainen projekti. ”Jumala” -malleja ja rasvan säätimiä on vaikea testata ja ylläpitää, ja mallien vapaa käyttö kaikkialla ohjaimissa johtaa valtaviin vaikeuksiin, kun tietokannan rakennetta on väistämättä muutettava.
Ratkaisu on yksinkertainen: jaa aktiivisen tietueen vastuu useaan tasoon ja injektoi kerrosten välisiä riippuvuuksia. Tämä lähestymistapa yksinkertaistaa myös testausta, koska sen avulla voit pilkata niitä tasoja, joita ei tällä hetkellä testata.
'Rasvaisella' PHP MVC -sovelluksella on riippuvuuksia kaikkialla, lukitus ja altis virheille, kun taas kerrostettu rakenne käyttää riippuvuusinjektiota pitämään asiat puhtaina ja selkeinä.
Katamme viisi ensisijaista tasoa:
Kerroksisen rakenteen toteuttamiseksi tarvitsemme a riippuvuusinjektiosäiliö , objekti, joka osaa instantisoida ja konfiguroida objekteja. Sinun ei tarvitse luoda luokkaa, koska kehys hoitaa kaiken taikuuden. Harkitse seuraavaa:
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(); } }
Yllä olevassa esimerkissä UserService
ruiskutetaan SiteController
, UserRepository
ruiskutetaan UserService
ja AR-mallit User
ja Logs
ruiskutetaan UserRepository
luokassa. Tämä säilökoodi on melko yksinkertainen, joten puhutaan kerroksista.
Nykyaikaiset MVC-kehykset, kuten Laravel ja Yii, vastaavat moniin perinteisiin ohjainhaasteisiin: Syötön vahvistus ja esisuodattimet siirretään sovelluksen toiseen osaan (Laravelissa se on ns. väliohjelmisto kun taas Yii: ssä sitä kutsutaan käyttäytymistä ), kun taas reititys ja HTTP-verbisäännöt käsitellään kehyksessä. Tämä jättää ohjelmoijalle hyvin kapean toiminnallisuuden koodata ohjaimeksi.
Ohjaimen ydin on saada pyyntö ja toimittaa tulokset. Ohjain ei saa sisältää sovelluksen liiketoimintalogiikkaa; muuten on vaikea käyttää koodia uudelleen tai muuttaa sovelluksen viestintätapaa. Jos sinun on luotava sovellusliittymä esimerkiksi näkymien hahmontamisen sijaan eikä ohjaimesi sisällä mitään logiikkaa, muutat vain tietojen palautustapaa ja olet valmis lähtemään.
Tämä ohut ohjainkerros hämmentää usein ohjelmoijia, ja koska ohjain on oletuskerros ja ylimmän lähtökohdan, monet kehittäjät lisäävät vain uutta koodia ohjaimiinsa ajattelematta lisää arkkitehtuuria. Seurauksena on liiallinen vastuu, muun muassa:
Tarkastellaan esimerkiksi ylisuunniteltuja ohjaimia:
//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')); }
Miksi tämä esimerkki on huono? Monista syistä:
last_login
kentässä, sinun on vaihdettava se kaikissa ohjaimissa.Ohjaimen tulee olla ohut; oikeastaan kaikki, mitä sen pitäisi tehdä, on ottaa pyyntö ja palauttaa tulokset. Tässä on hyvä esimerkki:
//A good example of a controller public function user (Request $request) { $user = $this->userService->getUserById($request->id); return view('user.index', compact('user')); }
Mutta mihin kaikki muut tavarat menevät? Se kuuluu palvelutaso .
Palvelutaso on liiketoimintalogiikan taso. Täällä ja vain täällä tulisi olla tietoa liiketoimintaprosessien kulusta ja liiketoimintamallien välisestä vuorovaikutuksesta. Tämä on abstrakti kerros ja se on erilainen kullekin sovellukselle, mutta yleinen periaate on riippumattomuus tietolähteestäsi (rekisterinpitäjän vastuu) ja tietojen tallennus (alemman kerroksen vastuu).
Tämä on vaihe, jolla on eniten potentiaalia kasvuongelmiin. Usein Active Record -malli palautetaan ohjaimelle, minkä seurauksena näkymän (tai API-vastauksen tapauksessa ohjaimen) on toimittava mallin kanssa ja oltava tietoinen sen ominaisuuksista ja riippuvuuksista. Tämä tekee asiat sotkuisiksi; Jos päätät muuttaa Active Record -mallin relaatiota tai määritettä, sinun on muutettava sitä kaikkialla kaikissa näkymissäsi ja ohjaimissasi.
Tässä on yleinen esimerkki näkymässä käytetystä Active Record -mallista:
@foreach($user->posts as $post) - {{$post->title}}
@endforeach
Se näyttää suoraviivaiselta, mutta jos nimen uudelleen first_name
minun on yhtäkkiä vaihdettava kaikkia näkymiä, jotka käyttävät tämän mallin kenttää, virhealtista prosessia. Helpoin tapa välttää tämä hämmennys on käyttää tiedonsiirtoobjekteja.
Palvelukerroksen tiedot on käärittävä yksinkertaiseksi muuttumattomaksi objektiksi - eli sitä ei voida muuttaa sen luomisen jälkeen -, joten DTO: lle ei tarvita asettimia. Lisäksi DTO-luokan tulisi olla riippumaton eikä laajentaa mitään Active Record -malleja. Varovainen - liiketoimintamalli ei kuitenkaan aina ole sama kuin AR-malli.
Harkitse päivittäistavarakaupan toimitussovellusta. Loogisesti, ruokakauppatilauksen on sisällettävä toimitustiedot, mutta tietokantaan tallennamme tilaukset ja linkitämme ne käyttäjään, ja käyttäjä on linkitetty toimitusosoitteeseen. Tässä tapauksessa on olemassa useita AR-malleja, mutta ylempien kerrosten ei pitäisi tietää niistä. DTO-luokkamme sisältää tilauksen lisäksi myös toimitustiedot ja muut liiketoimintamallin mukaiset osat. Jos muutamme tähän liiketoimintamalliin liittyviä AR-malleja (esimerkiksi siirrämme toimitustiedot tilaustaulukkoon), muutamme vain DTO-objektin kenttäkartoitusta sen sijaan, että muuttaisimme AR-mallikenttien käyttöä kaikkialla koodissa.
Käyttämällä DTO-lähestymistapaa poistamme houkutuksen muuttaa Active Record -mallia ohjaimessa tai näkymässä. Toiseksi DTO-lähestymistapa ratkaisee fyysisen datan tallennuksen ja abstraktin liiketoimintamallin loogisen esityksen välisen yhteyden ongelman. Jos jotain on muutettava tietokantatasolla, muutokset vaikuttavat DTO-objektiin eikä ohjaimiin ja näkymiin. Näetkö mallin?
Katsotaanpa yksinkertaista DTO: ta:
//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}; } }
Uuden DTO: n käyttäminen on yhtä suoraviivaista:
//usage example public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); return view('user.index', compact('user')); }
Näkymälogiikan erottamiseksi (kuten painikkeen värin valitseminen jonkin tilan perusteella) on järkevää käyttää ylimääräistä sisustajakerrosta. A sisustusarkkitehti on suunnittelumalli, joka sallii ydinobjektin koristamisen käärimällä sen mukautetuilla menetelmillä. Se tapahtuu yleensä näkymässä jonkin verran erityisellä logiikalla.
Vaikka DTO-objekti voi suorittaa sisustustyön, se toimii vain tavallisissa toiminnoissa, kuten päivämäärän muotoilu. DTO: n tulisi edustaa liiketoimintamallia, kun taas sisustaja kaunistaa tietoja HTML-koodilla tietyille sivuille.
Tarkastellaan katkelmaa käyttäjäprofiilin tilakuvakkeesta, joka ei käytä sisustajaa:
@if($user->status == AppModelsUser::STATUS_ONLINE) Online @else Offline @endif {{date('F j, Y', strtotime($user->lastOnline))}}
Vaikka tämä esimerkki on yksinkertainen, kehittäjän olisi helppo eksyä monimutkaisemmassa logiikassa. Tässä tulee sisään sisustaja, joka puhdistaa HTML-koodin luettavuuden. Laajennetaan tilakuvakkeen katkelma täydelliseen sisustajaluokkaan:
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)); } }
Sisustajan käyttö on helppoa:
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')); }
Nyt voimme käyttää mallin määritteitä näkymässä ilman ehtoja ja logiikkaa, ja se on paljon helpommin luettavissa:
{{$user->status}} {{$user->lastOnline}}
Sisustajia voidaan myös yhdistää:
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')); }
Jokainen sisustaja tekee työnsä ja koristaa vain oman osansa. Tämä useiden sisustajien rekursiivinen upottaminen mahdollistaa niiden ominaisuuksien dynaamisen yhdistämisen lisäämättä uusia luokkia.
Varastokerros toimii tietotallennuksen konkreettisen toteutuksen kanssa. On parasta injektoida arkisto käyttöliittymän kautta joustavuutta ja helppoa vaihtamista varten. Jos muutat tietovarastoa, sinun on luotava uusi arkisto, joka toteuttaa tietokannan käyttöliittymän, mutta sinun ei ainakaan tarvitse muuttaa muita tasoja.
Tietovarasto on kyselyobjektin rooli: Se saa tietoja tietokannasta ja suorittaa useiden Active Record -mallien työn. Active Record -mallit näyttävät tässä yhteydessä yksittäisten tietomallien entiteettien - minkä tahansa järjestelmän järjestelmän objektin, josta haluat mallintaa ja tallentaa tietoja. Vaikka jokainen entiteetti sisältää tietoja, se ei tiedä miten se ilmestyi (jos se luotiin tai saatiin tietokannasta) tai kuinka tallentaa ja muuttaa omaa tilaansa. Tietovaraston vastuu on tallentaa ja / tai päivittää yksikkö; tämä erottaa huolenaiheet paremmin pitämällä entiteettien hallinnan arkistossa ja tekemällä yksiköistä yksinkertaisempia.
Tässä on yksinkertainen esimerkki arkistomenetelmästä, joka rakentaa kyselyn tietokannasta ja Active Record -suhteista saatujen tietojen avulla:
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(); }
Äskettäin luotussa sovelluksessa on vain ohjaimia, malleja ja näkymiä koskevat kansiot. Yii ja Laravel eivät lisää ylimääräisiä tasoja esimerkkisovelluksen rakenteeseen. MVC: n rakenne on helppo ja intuitiivinen, myös aloittelijoille, yksinkertaistaa työskentelyä kehyksen kanssa, mutta on tärkeää ymmärtää, että heidän esimerkkisovelluksensa on esimerkki; se ei ole standardi tai tyyli, eikä se aseta mitään sääntöjä sovellusarkkitehtuurista. Jakamalla tehtävät erillisiin yksittäisiin vastuukerroksiin saamme joustavan ja laajennettavan arkkitehtuurin, jota on helppo ylläpitää. Muistaa:
Joten jos aloitat monimutkaisen projektin tai projektin, jolla on mahdollisuus kasvaa tulevaisuudessa, harkitse selkeää vastuunjakoa ohjaimelle, palvelulle ja arkistokerroksille.