Clean code: trainwreck
Sziasztok! Szerintem nem szóltam még itt hozzá. :)
A clean code elvek közül sokáig nehezen tudtam a megérteni a metódusláncok elkerülésére vonatkozót, nem volt világos, miért kell követni, és az sem, hogy pontosan hogyan kerüljük el.
Most azt hiszem, találtam egy aránylag egyszerű példát, ami kulcsot jelenthet a megértéshez.
Van egy Mailer osztály, ennek kell szolgáltatásokat nyújtani az emailek kezelésére. Alapvetően egy emaillel kétféle dolgot tehetünk: kiküldhetjük azonnal, vagy a várakozási sorhoz adhatjuk, ezeket aztán egyszerre küldjük ki.
Itt az implementációm:Használni pedig így lehet:És igen, valahol itt lent, a mail queue metódusoknál jelentkezik a vonatroncs. Ha jól értem, ennek elkerülésére burkoló metódusokat kéne írnom a Mailer osztályba (addMailToQueue és sendMailsFromQueue), de egyelőre nem teljesen világos, hogy ennek mi a konkrét haszna, és miért kell elkerülni a method chaininget.
Jól gondolkodom? Nektek mi a véleményetek erről?
Válaszokat előre is köszi!
■ A clean code elvek közül sokáig nehezen tudtam a megérteni a metódusláncok elkerülésére vonatkozót, nem volt világos, miért kell követni, és az sem, hogy pontosan hogyan kerüljük el.
Most azt hiszem, találtam egy aránylag egyszerű példát, ami kulcsot jelenthet a megértéshez.
Van egy Mailer osztály, ennek kell szolgáltatásokat nyújtani az emailek kezelésére. Alapvetően egy emaillel kétféle dolgot tehetünk: kiküldhetjük azonnal, vagy a várakozási sorhoz adhatjuk, ezeket aztán egyszerre küldjük ki.
Itt az implementációm:
class Mailer {
//...
/**
* @var MailQueue
*/
private $mailQueue;
/**
* @var MailTransport
*/
private $mailTransport;
/**
* @param Mail $mail
*/
public function send(Mail $mail) {
$this->mailTransport->send($mail);
}
/**
* @return MailQueue
*/
public function getMailQueue() {
return $this->mailQueue;
}
}
class MailQueue {
//...
/**
* @var MailTransport
*/
private $mailTransport;
/**
* @param Mail $mail
*/
public function addMail(Mail $mail) {
//...
}
public function sendMails() {
//...
foreach($mails as $m) {
this->mailTransport->send($m);
//...
}
}
}
$mailer->send($mail);
// vagy:
$mailer->getMailQueue()->addMail($mail1);
$mailer->getMailQueue()->addMail($mail2);
$mailer->getMailQueue()->sendMails();
Jól gondolkodom? Nektek mi a véleményetek erről?
Válaszokat előre is köszi!
Hali, Method
Method chaining
Nagyon sok helyen olvasni hogy bad practice, de ez egyáltalán nem igaz. Nagyon is valid, de ennek is megvan mikorja.
A probléma innen ered (gondolom erről olvastál te is):
Ilyen eseteken kerülendő a chaining, az alábbi viszont teljesen rendben van:
Általános jó tanács az, hogy ahol más osztályt ad vissza, ott nem ajánlott a chain.
@see Doctrine2 QueryBuilder, Symfony2 DI Configuration
Law of Demeter
Ez inkább a konkrét kérdésedre vonatkozna, úgy érzem itt picit kevered a dolgokat, itt úgy tűnhet, hogy a chain a probléma, de egészen másról van szó, a chain itt csak egy tünet, nem a betegség... ebbe itt most nem mennék bele, mivel ennél jobb magyarázat nem kell:
The Paperboy, The Wallet, and The Law Of Demeter Ez kötelező kellene, hogy legyen minden OOP-t tanulónak.
Egyéb
Szerintem nem a legjobb példát sikerült kiválasztani, a linkelt sokkal érthetőbb. Úgy érzem ez a maileres elvitt egy rossz irányba. A Mailernek nem kell tudnia a Queue-ról. Ha gondolod nézd meg a Swiftmailer implementációját, az nagyon közel van az igazsághoz (még ha a CS ma már elavault is).
Köszi! Természetesen a
Általános jó tanács az, hogy ahol más osztályt ad vissza, ott nem ajánlott a chain.
Igen, ezzel tisztában vagyok, hogy a setterek visszatérhetnek az objektummal, tényleg praktikus, a method chaininget én is úgy értettem, hogy különböző típusú objektumokat láncolunk végig.
A Mailer azért jó példa, mert én írtam, tényleg működő kód, egy igazi példán keresztül nézhetem meg, hogy érdemes továbbfejleszteni.
Szerintem a cikkben említett példa eléggé hasonlít az enyémre, itt a Customer~Mailer és Wallet~MailQueue megfeleltetések nagyjából stimmelnek.
Nekem első körben az jött le a cikkből, hogy az első feltételezésem helyes volt, és érdemes a mailQueue mezőt elrejteni a Mailer ügyfelei elől és megírni a két burkolófüggvényt.
Természetesen nem kell tudnia, az ügyfelek használhatnák közvetlenül a MailQueue osztályt is. A fejlesztőtársam kérte, hogy minden emaillel kapcsolatos dolgot egy osztály (Mailer) intézzen, ha úgy tetszik ez egy facade két másik osztályhoz (MailQueue és MailTransport)
A method of an object should invoke only the methods of the following kinds of
objects:
1. itself
2. its parameters
3. any objects it creates/instantiates
4. its direct component objects
Ebből viszont következik (ezen már korábban is gondolkodtam), hogy pl. a rengeteg symfonys tutorialban szereplő
$this->getDoctrine()->getManager()->getRepository()
Ha valaminek csak pl a repositoryról kell tudni, akkor azt kell injektálni, és nem a managert / doctrine registryt és végképp nem az egész service containert. (Circular reference exceptionnek nézz utána ha gondolod, érdemes)
A Controller azért kivétel, mert az egész containerről tud... lehetne definiálni a repót egy külön serviceként, és csak simán $this->get('my_repo'), de ez ugyanúgy a containerből szedné a servicet, ami már violation.
Tehát ennek a "hibának" oka a kényelem, és semmi más.
A javasolt módszer egyébként az, hogy ne extendeld a base Controller osztály, hanem serviceként készítsd a saját controllerjeidet, és csak azt injektáld ami kell. A közösen használt serviceknek pedig csinálhatsz egy saját base classt.
Tipikus RAD probléma egyébként, tradeoff mindig van.
Értem.
Egy osztály használ valamilyen szolgáltatást, amit mondjuk egy gyárból kapunk. Tegyük fel, hogy maga a gyár már ott van a service containerben, ezért könnyen át tudjuk passzolni a konstruktornak.
Ekkor a következő kód szintén sérti Demeter törvényét:
Szerintem ez is sérti a
Symfonyban erre is van megoldás, néhány esetben a prototype scope elég a servicenek, ha viszont rendes factory kell, akkor http://symfony.com/doc/current/components/dependency_injection/factories.html
Request
Nincs ezzel semmi baj, ha a
$request = $array[0]
-t hívnál.A doctrines példád is hasonló lehet, bár annak a belső felépítésébe nem igazán látok bele. Már régen néztem, de úgy emlékszem, hogy a symfony2 és a doctrine2 jól szervezettek.
class A { private
Igen, ezért is írtam hogy
Ez attól függ, hogy mi a cél.
Jelen esetben az objektumot kell választanod, mert egy facade-et szeretnél létrehozni a mailer osztállyal, ami kitakarja a levélküldés és a várakozási sorba rakás mikéntjét a külvilág számára, tehát a get-queue-nek el kell tűnnie és helyette a mailer-hez kell adnod valami enqueue-t vagy ilyesmit.
Tiszta adatstruktúrák esetében ez a fajta lekérés teljesen jól működő dolog lehet:
$(x).get(y).get(z)
, illetve az adatstruktúra építésénél is teljesen működőképes:d3.select(x).append(y).append(z)
. Mindkettőnél DOM műveletekről van szó, tehát a DOM fa, mint adatstruktúra építéséről illetve bejárásáról...Objektumoknál esetleg valamilyen építő mintánál fel lehet használni az objektum gráf, mint adatstruktúra építésére. A setter chain nem igazán jellemző, talán DI container-ben előfordulhat, de pl egy java fejlesztő elég csúnyán nézne rád, ha ilyesmit látna. Egyébként is a legtöbb esetben jobb valamilyen config adatstruktúrát megadni, ami minden paramétert egyszerre tartalmaz, mint egyesével beállítgatni. Így lehetőség van elleőrizni, hogy minden paraméter megvan e, amire az objektumnak szüksége van a működéséhez.