Redizajn aplikacie - praca iba s objektami? rubrika: Programování: PHP

6 Ivan Jaros
položil/-a 25.4. 13:53

Ahojte,
nejak sa trosku zacinam pohravat s takou, mozno sibnutou, myslienkou ze jednu aplikaciu ktoru mam by som prerobil tak, ze vsetko by boli objekty.
Cize argumenty funkcii a metod by neboli ziadne stringy, booleany alebo integery ale skratka objekty s presne definovanymi interfejsami.

Nerobili ste uz niekto takuto srandicku(podelte sa o skusenosti)?

Komentáře

  • domgersak : na zaciatok uvazuj o Spl kniznici a z nej SplString, SplInt a pod.: nieco uz pre tieto pripady vyvojari PHP urobili. Kazdopadne, osobne v tom vidim viac problemov ako uzitku, jednoducho je vela pripadov, kde potrebuje clovek pracovat so skalarnym typom ako int a string a kedze PHP to nativne nevie ako objekt, neznasilnoval by som to. Napr. sum(a, b) { return a + b; } by sa zmenil v sum(SplInt a, SplInt b) { return a->getValue() + b->getValue(); } co mi pride pre takyto pripad zbytocne ukecane a nepomaha to citatelnosti kodu. Takze skalarne typy ako take by som vyslovene nezamienal za (s)proste objekty. Naopak, to co popisali viaceri: povytvaraj si viacero objektov ktore maju vyssi vyznam (napr. EmailAddress) a ktore sice mozno funguje len ako wrapper nad stringom, ale mozu mat aj logicke API (getHost()...). Inymi slovami: prosty string je a (zatial) v php bude string a nic viac, ale ak string ma v skutocnosti akusi strukturu a presne stanoveny vyznam, tak vtedy je vhodne ho zapuzdrit pod specificky objekt. 26.4. 9:50
  • Kit : @domgersak: SplString ani SplInt nic neřeší, neboť neobsahují žádnou sémantiku ani chování, ale jen primitivní typovou kontrolu. 26.4. 10:33
  • domgersak : nic neriesi ani ked si urobi CustomString ktory bude fungovat uplne rovnako... Cize ako hovorim, ak to ma byt len zaobalenie do objektu bez akejkovek funkcionality, tak radsej ten Spl ktory je aspon "standardizovany", alebo vlastne objekty ak uz maju aj nejake API. 26.4. 15:53
  • Tomáš Votruba : Moc se mi ten nápad líbí. Pokud to hodíš na Github, pingi mě (@tomasvotruba) - budu ti rád dělat code review. 30.4. 16:51
odkaz Vyřešeno
12 Vašek Ch.
odpověděl/-a 4.5. 14:03

Máme v Bonami třídy jako EmailAddress, Price, Decimal, obecné enumy, ArrayList, Map atd. a pracuje se s tím velmi dobře. Nedáme na to dopustit. Moje zkušenost říká, že bych zdejší komentáře, že PHP se na to z různých důvodů nehodí, vůbec nebral v potaz.

Používáme kombinaci skalárních typů, kde je to vhodné (není třeba mít opravdu všude nějaký datový typ MyAwesomeInteger), a vlastních tříd, které zapouzdřují a označují jiná data.

Pár příkladů:
1) Třeba třída Price: Když sčítám dvě ceny, místo toho, abych použil součet dvou floatů  $finalniCena = $cenaVEur + $cenaVCzk , který povede k nesmyslné hodnotě a nepříjemné, těžko dohledatelné chybě, použiju raději naše $finalniCena = $cenaVEur->add($cenaVCzk); -- mám za to, že zápis není ani o nic složitější ani o nic nepřehlednější než použití pluska. Výhoda je, že v tomhle případě se mi automaticky zkontroluje i měna a i z toho nejzaprděnějšího koutu aplikace dostanu jasnou výjimku, když se pokusím sečíst ceny ve dvou rozdílných měnách.

2) Třeba třída EmailAddress. Proč si neulehčit život a neudělat

$address = EmailAddress::fromString("ahoj@post.cz");
echo $address->getDomain(); /* Vrátí "post.cz" */

Navíc kdekoliv v aplikaci budu dál předávat instanci třídy EmailAddress, budu na všech těch místech vědět, že je to emailová adresa, a nikoliv string popisující dnešní počasí.

3) Třeba naše vlastní implementace DateTime (i Date i Time). Zápisy jako zdali bude za deset pracovních dní už po vánocích řešíme takto:

$budePoVanocich = $now->addWorkingDays(10)->isAfter($christmasDay);

4) Máme vlastní implementaci třídy Enum, př.:

class PaymentStatus extends Enum {
 
    const SUCCESSFUL = "SUCCESSFUL";
    const FAILED = "FAILED";
 
    public static function SUCCESSFUL() {
        return self::create(self::SUCCESSFUL);
    }
 
    public static function FAILED() {
        return self::create(self::FAILED);
    }
}

Místo toho, abychom stav platby předávali někde jako obecný string, což opět povede k chybám, uděláme raději  $failedPaymentStatus = PaymentStatus::FAILED(); a takovou hodnotu (tj. instanci třídy PaymentStatus) pak můžeme zase předávat napříč aplikací doaleluja a nechat na všech místech PHP zkontrolovat, že předáváme správný datový typ. Mimochodem, máme pro tyhle enumy i vlastní datový typ do Doctrine, takže nám instance tědle tříd lezou už z databáze.

Je libo použít enum ve switchi? Prosím:

switch ($paymentStatus) {
    case PaymentStatus::SUCCESSFUL:
        /* ... */;
        break;
 
    case PaymentStatus::FAILED:
        /* ... */;
        break;
 
    default:
        throw new InvalidStateException();
 
}

Další výhoda je, že do třídy PaymentStatus mohu přidat další vhodné metody, které se přímo na jednotlivé možnosti enumu vážou.

5) Trošku složitější příklad, ale máme i vlastní ArrayListy a Mapy. Téměř nikde nepoužíváme skalární array. Máme vlastní implementaci ArrayList (klíčem jsou indexy 0, 1, 2, ...) a Map (klíčem je libovolná hodnota včetně objektu) a používají se třeba takto:

class PaymentStatusList extends ArrayList {
    public function __construct(array $items) {
        /* Tímhle řekneme, že tenhle list může obsahovat pouze instance typu PaymentStatus */
        parent::__construct($items, [PaymentStatus::class]); 
    }
}
 
$prazdnyList = PaymentStatusList::fromEmpty();
 
$listSJedinouPolozkou = PaymentStatusList::fromOnly(PaymentStatus::FAILED());
 
$listNezaplacenychStavu = PaymentStatusList::fromArray([
  PaymentStatus::FAILED(),
  PaymentStatus::CANCELLED()
]);

Získávám tak kontrolu, že každá instance třídy PaymentStatusList v sobě nikdy nebude obsahovat nic jiného než instance PaymentStatus. Opět můžu do třídy PaymentStatusList naimplementovat metody, které se ke stavu platby váží.

Zřetelnější by to mohlo být třeba spíš u třídy se seznamem uživatelů UserList. Chcete z načteného seznamu uživatelů dostat array četnosti domén v jejich emailových adresách?

$addresses = $userList
    /* Metoda ve třídě UserList, která promaže všechny neaktivní uživatele a vrátí opět instanci UserList */
    ->removeInactive() 
 
    /* Z ArrayListu uživatelů uděláme ArrayList emailových adres */
    ->map(function(User $user) { 
        return $user->getEmail();
    })
 
    /* Vrátíme klíč, tedy doménu emailové adresy, podle nějž zgroupoujeme emailové adresy do EmailAddressListů */
    ->groupByIntoMap(function(EmailAddress $email) {
        return $email->getDomain();
    }, EmailAddressList::class)
 
    /* Předchozí metoda vrátila mapu, kde klíčem je doména a hodnotou je zgroupovaný seznam emailových adres dle této domény,
       tak přemapujeme hodnoty této mapy na prosté číslo, kolik prvků každý seznam adres dle domény má */
    ->mapValues(function(EmailAddressList $emails, string $domain) {
        return $emails->count();
    })
 
    /* Nakonec z toho můžeme udělat třeba ten obyčejný skalární array */
    ->toArray();

Vrátí mi pole

[
  "seznam.cz" => 4,
  "gmail.com" => 7,
  /* ... */
]

Závěrem: Nechť ti tohle slouží jako inspirace a rozhodně jdi do toho :-).

Komentáře

  • Vašek Ch. : Pls pokud downvotujete, napište proč ;-). 4.5. 14:45
  • Taco : Mě se tam nelíbí, že toto řešení porovnáváš s prací se stringama. Je to takové nefér, a vyvolává to dojem, že jsi si nedal práci pouvažovat o argumentech těch které: "že bych zdejší komentáře, že PHP se na to z různých důvodů nehodí, vůbec nebral v potaz.". 4.5. 14:48
  • uetoyo : @Vašek Ch.: To přeci všechno dává smysl! Bohužel na to pořád nejsem schopen koukat očima PHP programátora. 4.5. 14:51
  • Taco : Nevýhoda takto nahrazovaných hodnot za objekty je v tom, že si pamatují stav. Což každej odkejvá, že jasný, ale zatím jsem viděl každého programátora si minimálně pětkrát nabít ústa. V PHP se array a ArrayList chová jinak. Což nabourává zvyk. 4.5. 14:54
  • Vašek Ch. : Píšete, že pro PHP to není idiomatické, že budu psát mnoho kódu, že PHP nemá přetěžování operátorů, že se bude zvyšovat chybovost, že prodělám, že budu muset změnit jazyk, že nejlepší řešení by bylo napsat transpiler -- a moje zkušenost, kdy mnoho let používáme, co jsem napsal, hovoří jinak. Ten člověk má projekt v PHP, napadlo ho větší použití objektů a zdali s tím nemáme zkušenosti. Máme. A skvělé. Ale vy ho odkazujete na jiné jazyky (budiž) nebo dle mého názoru úplné nesmysly jako transpilery. 4.5. 14:57
  • Vašek Ch. : @Taco: "Nevýhoda takto nahrazovaných hodnot za objekty je v tom, že si pamatují stav." -- ale to je přece logické, že instance třídy EmailAddress si bude pamatovat, co že za emailovou adresu obaluje. A mutabilní tyhle třídy nejsou. 4.5. 14:59
  • skliblatik : DateTime a PaymentStatus - to je mi silně povědomé :) (měl jsem taky něco takového). Ale tyto třídy mají přidanou hodnotu - a s jejich existencí bych mohl souhlasit a zároveň se netrápit tím, že nějaká metoda bere na vstupu string. Jenže je třeba spousta stringů, k nimž hodnota přidaná v dané aplikaci není. Takže je možnost mít pro tyto případy třídu String - a poctivě a povinně do ní stringy balit. V tom rozhodně výhodu nevidím. Nebo mít specifické třídy jako Street, City, Vat, Email (v systému kde tě doména vůbec nezajímá)... Takových tříd může být velké množství, na jedno brdo, se spoustou byrokracie okolo. A přínosem vzhledem k vynaloženým nákladům si úplně jistý nejsem. 4.5. 15:16
  • Vašek Ch. : @skliblatik: Souhlasím, je potřeba najít balanc a nevytvářet třídy tam, kde to není potřeba, ale zároveň neohrnovat nad nimi nos, kde by mohly pomoct. Cit pro to si podle mě člověk vybuduje až časem po pár přehmatech. Tak či tak to uvažování začít přepisovat aplikaci používající čistě primitivní typy do aplikace, která si sem tam něco vhodně obalí, nepovažuju za scestné. 4.5. 15:30
  • Kit : @Vašek Ch.: Tohle řešení se mi líbí, i když jsem ho ještě celé nestrávil. 1) Bez výhrad. 2) Použil bych standardní operátor new, ale jinak OK, 3) Hezké a praktické, 4) Tady bych raději použil polymorfismus bez konstant, 5) Tohle si ještě budu muset promyslet. Celkově palec nahoru. 4.5. 15:55
  • skliblatik : To se přiznám, že mě vůbec nenapadlo, že by mohlo jít o aplikaci používající čistě primitivní typy. Spíš to na mě působilo, že se tazatel chce striktně zbavit primitivních typů v argumentech. Každopádně nad výrazem "přepisovat aplikaci" se mi docela rozblikává výstražná kontrolka :) 4.5. 16:00
  • Vašek Ch. : @Kit: Díky :). ad 2) Souhlasím. Párkrát se nám ale stalo, že se konstruktor změnil, protože se změnila implementace třídy. Tyhle pojmenované rádobykonstruktory fromString() aspol. ale zůstanou a je pak méně práce s refactoringem. Máme třeba DateTime::fromFormat(), DateTime::fromTimestamp(), DateTime::fromYmdHms(), Time::fromZeroes() apod. 4.5. 16:06
  • Vašek Ch. : @skliblatik: To podle mě buď někomu nebliká a jde si nabít hubu, aby mu blikat pro příště začla ;-). Anebo mu bliká a spíš tím přepisem myslí to, zdali neposunout styl psaní kódu pro budoucí fičury a refactoringy. 4.5. 16:10
  • Taco : @Vašek Ch. Ne, tohle jsem nepsal. Pro mě diskuse skončila. 4.5. 18:16
  • rmaslo : Mě osobně se nelíbí ta 1) Vím, že to jsou jenom 4 znaky navíc, ale prostě jsou to závorky navíc a to nemám rád. Naopak třeba 6) za mě určitě ano, jak zřetězení objektových operací, tak předávání funkce jako parametru v tom kódu vidím krásně na první pohled - pro mě krásně čitelný kód. 4.5. 18:23
  • uetoyo : Jen otázka bokem: "místo toho, abych použil součet dvou floatů $finalniCena = $cenaVEur + $cenaVCzk" Počítáte opravdu peníze s typem float? 4.5. 18:34
  • Kit : @uetoyo: Vtip je v tom, že při zápisu $finalniCena = $cenaVEur->add($cenaVCzk); je tento implementační detail (float vs. decimal) zcela skryt. S operátorem "+" by to bez přetížení nešlo. 4.5. 23:06
  • uetoyo : @Kit Nevím jestli tomu rozumím, ale nechci aby datový typ, který reprezentuje peníze byl implementační detail: aneb "Never use floats for money" 5.5. 10:20
  • Vašek Ch. : @uetoyo: Tu obavu chápu, ale není to vnitřně float :-). Má to pevnou desetinnou čárku. Kontrola měny není jediná přidaná hodnota. 5.5. 10:43
  • Kit : @uetoyo: Zrovna při sčítání rychlosti tady někdo vůbec neuvažoval, že rychlost není ani integer, ani float. Není tedy možné použít operátor "+" bez přetížení. Peníze samozřejmě musí být nějaký decimal, ale to se právě ukryje uvnitř tříd, aby vývojář byl od konkrétního typu odstíněn. Pokud je možné přetížit operátory, tak se dají využít, ale běžné jazyky to neumožňují. 5.5. 10:51
  • Kit : @Vašek Ch.: Na tom je právě vidět elegance zapouzdření. Vnitřně je nastavena pevná desetinná čárka, ve které se provádí vnitřní výpočty. Vně se objekt prezentuje dle potřeby. 5.5. 10:54
  • uetoyo : @Kit, Vašek Ch.: Chápu, jen mne to zajímalo. 5.5. 12:42
  • Andreaw Fean : Ta pětka je taková krásně inženýrská. Psát se to bude dobře. Ale jakmile se v tom objeví nějaké chibi tak je to pěkně blbé. 5.5. 17:46
  • uetoyo : @Andrew Fean: Jo chibi jsou blbí :D 6.5. 17:04
  • Kit : @Andreaw Fean: Fluent interface sice moc rád nemám, ale když se to dobře napíše, tak tam pro chibi moc místa není. 6.5. 17:10
  • Taco : @Kit: To tě ten vtip tak nadchl, že jsi neodolal a napsal takhle bezobsažnou větu? 7.5. 22:10
  • Taco : @Andreaw Fean: https://en.wikipedia.org/wiki/Chibi_(term) :-D 7.5. 22:11
  • Kit : @Taco: Můžeš napsat i nějaký názor, třeba k fluent interface. Jak už jsem psal, rád ho moc nemám, protože "není dost objektový". 7.5. 22:17
  • Taco : @Kit: Myslím, že jeden plácal je tady až až. Nebudu se přidávat. 7.5. 22:26
  • Kit : @Taco: Hlavně se nepokoušej nacpat do PHP haskellovské řešení, přece jen to jsou dost odlišné jazyky. 7.5. 22:36
  • jiri.knesl : Vašku, proč jste vlastně nepoužili jazyk, který obvykle takto funguje (jako C# nebo Javu)? 13.5. 15:39
  • Vašek Ch. : Jirko, protože jsme phpkáři :-). A v mém světě takto funguje i PHP. 13.5. 19:21

Pro zobrazení všech 11 odpovědí se prosím přihlaste:

Rychlé přihlášení přes sociální sítě:

Nebo se přihlaste jménem a heslem:

Zadejte prosím svou e-mailovou adresu.
Zadejte své heslo.