OOP – služba nebo továrna na službu

Klobás
Člen | 113
+
0
-

Ahoj,

potřeboval bych poradit od zkušených; dostal jsem se do menšího sporu s kolegou, vnitřně vím, že má asi víc pravdu než já, ale jeho řešení se mi nelíbí o nic víc než to moje.

Ve zkratce:

Mám $trida což je instance nějaké třídy (služba), která má několik závislostí (Databáže plus par dalších nepodstatných služeb).

Pseudokód

<?php

$trida->setType($type); // $type muze nabyvat hodnot Numeric|Text|Boolean|Multiple
$trida->addFilters($formField); // prida nejaka filtry, je to neco jako $formField->addFilter(...)
if (!$trida->validate($formField)) {
	throw new \Exception('Neproslo to ...');
}

?>

Oč mi jde, metody addFilters a validate, jsou za mě OK, uvnitř pracují s nějakou logikou kontroly a normalizací dat.
Jde mi o nastavení datového typu do třídy přes setType (to dál potom slouží k tomu jaké filtry / validátory se použijí v addFilters a validate()).

Kolega mi říká, že kdyby to takhle neviděl napsané, nebo by to zavolal bez setType() tak to třeba pustí sadu na výchozím prvku (což je ve výchozím stavu „Text“) a že to je taková skrytá vazba a prostě se mu to nelíbí.

Navrhl mi, abych` $type` předával do každé výstupní metody (tj do addFilters($type, $formField) `a `validate($type)).
To se zas nelíbí mně (to se mi líbí více můj první způsob).

Co vy si o tom myslíte?

Jediné co mě dál napadá, udělat továrnu tj.

<?php
$trida = $this->tridaTovarna->create($type);
$trida->addFilter($formField);
$trida->validate($formField);
?>

Je to lepší, nebo není to zbytečné? Zbytečné vytvářet třídu přes továrnu kvůli jednomu parametru, který by se tam měl nějak explicitně a jasně poslat (tu třídu ani víckrát než 1× nevytvořím)

A otázka mimo, jak zajistit, aby se $trida, která se zinstancuje uvnitř továrny nedala vytvořit bokem?

Není ještě nějaké lepší řešení než „otřesné“ první 2 způsoby a případně tedy ten třetí factory způsob?

Díky moc za lepší rozhled.

PS. Hledám snažší řešení, ne nějaký šílený OOP návrh.

Editoval Klobás (21. 11. 2023 21:18)

mystik
Člen | 292
+
+1
-

S kolegou souhlasim ze tahle nutnost volat type, ktera neni nijak vynucena ani patrna je zaklad pro budouci chyby. Navoc ten typ se nejak pamatuje v te service tajze je zbytecne stavova a to je zaklad na dalsi tezko debugovatelne bugy.

Za me bych sel cestou tovarny, kde si vytvoris validator pro dany typ a s tim pak pracujes. Pokid potrebujes stav drzet mezi vice volanimi tak je to lepsi.

Pokid by ale addFilter a validate slo spojit do jednoho volani zvazil bych tu verzi s parametrem metody.

Proc vadi kdyby bylo vice instanci? Obecne tomu muzes zabranit navrhovym vzorem Singleton, ale to ze ho potrebujes je obvykle spis naznak, ze by neco melo byt nabrzeno jinak aby voce instanci nevadilo. Dela to pak problemy pri testovani apod.

Za me jeste par otazek:
Co za type je $formField? Nejde $type poznat z nej?
Co presne dela addFilter? Meni nejak stav te validacni sluzby nebo neco dela s $formField?

Editoval mystik (21. 11. 2023 21:52)

Klobás
Člen | 113
+
0
-

mystik napsal(a):

S kolegou souhlasim ze tahle nutnost volat type, ktera neni nijak vynucena ani patrna je zaklad pro budouci chyby.

Za me bych sel cestou tovarny.

Proc vadi kdyby bylo vice instanci? Obecne tomu muzes zabranit navrhovym vzorem Singleton, ale to ze ho potrebujes je obvykle spis naznak, ze by neco melo byt nabrzeno jinak aby voce instanci nevadilo. Dela to pak problemy pri testovani apod.

Za me jeste par otazek:
Co za type je $formField? Nejde $type poznat z nej?
Co presne dela addFilter? Meni nejak stav te validacni sluzby nebo neco dela s $formField?

Ahoj,
prvně díky za komentář.

Nevadí, že by měla více instancí. Jen jsem poznačil, že víc instancí mít asi nikdy nebudu (tak jestli to nevadí) a že tu továrnu vidím jako nejreálnější možnost, protože ten typ prostě je potřeba explicitně nastavit.

$formField je instance formulářového prvku (je to v mém případě vždy TextInput) a $type je zcela něco jiného, tím detekuji rodičovský obal a jaké filtry na $formFieldu budu vyvolávat (uvnitř třídy mám seznam všech filtrů a pak konfiguraci a pořadí pro každý $type).

AddFilter nedělá nic jiného než něco jako (opět pseudo)

<?php
foreach ($this->getConfigurationByType($this->type)['filters'] as $filter) { // $filter je tedy \Closure
  $formField->addFilter($filter);
}
?>

Takže továrna tedy bude asi nejvhodnější, 1 způsob je tedy mlhavý a nečitelný a kolegův návrh ale také není zcela v pořádku ne?
V ideálním světě by to byla kolekce tříd, ale to tady ted rozebírat nechci, zajímá mě čiště jen toto :-)

Děkuji.

Kamil Valenta
Člen | 762
+
0
-

Pokud jsou filtry a validace závislé na setType, tak by mi ve všech případech vadilo, že to nemám na očích. Že zavolám na dvou místech $trida->validate() a ona mi jednou vrátí true, podruhé false. A já si musím všimnout, že někde je jiný ->setType, konstruktor, factory…

Asi by se mi pak více líbila $trida jako wrapper nad poděděnými třídami dle type.
Volání

$trida->text->validate();
$trida->numeric->validate();

je pak jednoznačné.

Pokud filtry na type závislé nejsou, pak bych jen separoval jednotlivé validační metody.

$trida->validateText();
$trida->validateNumeric();
David Grudl
Nette Core | 8150
+
+2
-

Vidím největší rozdíl v tom, že v prvním případě jde o sdílenou službu, kterou můžeš používat na různých místech aplikace, zatímco v druhém případě je sdílenou službou továrna, kterou vyrobíš jednu instanci a tu pak používáš na jednom místě.

Což v tvém případě vypadá jako takřka identická záležitost, jako kdyby ta služba byla třeba formulář v malé aplikaci, kde je jen jeden formulář.

Ale u formuláře cítím, že je to spíš shoda okolností, že je v aplikaci jen jeden, a určitě nepotřebuju, aby se předával na různé místa aplikace pomocí autowiringu, takže bych si i na ten jeden formulář udělal raději továrnu, tedy bych šel cestou 2 automaticky.

mystik
Člen | 292
+
+1
-

Jo tak za me tovarna vypada jako nejlepsi moznost.

Jen bych se zamyslel jestli $type neprenaset v ramci toho $formField kde ny se to nastavilo pres setOption. Logicky mi ta informace sedi k tomu formField nez aby byla na uolne jinem miste kodu.

mystik
Člen | 292
+
0
-
$form->addText('foo')->setOption('validationType', 'Text');
Klobás
Člen | 113
+
0
-

Ahoj,
díky všem za reakce.

Očividně jsem Vás zmátl tím, že se motám kolem formulářového prvku.

Je to tak:

  • Je parametr v eshopu (který je interně označen jako Numeric|Text|Boolean|Multiple). (Např. „Počet pater“)
  • Tento parametr ma pod sebou skupinu X hodnot (např. „1, 2, 3, čtyři, 5) – tady mi filtr ze čtyři udělá 4-ku“, a každá hodnota musí projít nějakou normalizační sadou (v příklade jsou to filtry), které výsledek od uživatele znormalizují na co pokud možno ten nejlepší.
  • Pak na to nastoupí finální validátor nebo více validátoru a finálně tu hodnotu označí jako že je v pořádku nebo není.

To je celá úloha. Je to ted aktuálně v Administraci při snaze hodnotu parametru při uložení znormalizovat a uznat jako validní či nikoli.

Někdy v budoucnou hypoteticky, můžu totéž provést na jiném místě v aplikaci (někde v nějaké service či strojově při importu parametrů a jejich hodnot z jiného e-shopu).

Takže to nemá nic moc společného s formulářem či jeho nějakým prvkem.

Akorát jsem ty svoje filtry využil na $formPrevek->addFilter(), kterým disponuje každý Nettí formulářový prvek a tím jsem vás asi zmátl.

Filtrů jsou desítky a jak je ke každému typu parametru nadefinuji je na mě (pořadí a počet je nezavislý na sobě).

Text nemusí mít žádný filtr
Numeric (může mít filtry aby to znormalizovalo na číslo, vyházelo mezery atd), validátor může být pak prosté reálnhé číslo nebo rozsah dvou čísel
Boolean zase převádí hodnoty ano / ne pouze na jejich lowercase varianty a validátor zkontroluje jestli hodnoty jsou jen ano|ne a nic jiného.

V dané třídě pak mám nějakou konfiguraci všech typu a jejich filtrů a validátorů včetně pořadí spouštění (což je taky důležité).

To si myslím, že je v pořádku. Jistě měla by to být kolekce tříd, mělo by to být více robustní, ale prostě teď je to tak jak to je.

Nevěděl jsem, jak prostě tam nastavit ten typ do třídy nějak explicitně, neskrytě a hlavně povinně.

Zdá se tedy, pokud jsem něco nepochopil špatně, že nejlepší je továrna, které předám typ a ta vyrobí třídu validátor+normalizátor (pracovní označení) a pak už si volám filtry / validátory jak potřebuji.

Pokud by k tomu někdo něco měl co jsem třeba nepochopil, tak si to rád přečtu a všem moc díky pokud jste dočetli až sem a za snahu poradit.

Editoval Klobás (21. 11. 2023 23:16)

Klobás
Člen | 113
+
0
-

David Grudl napsal(a):

Vidím největší rozdíl v tom, že v prvním případě jde o sdílenou službu, kterou můžeš používat na různých místech aplikace, zatímco v druhém případě je sdílenou službou továrna, kterou vyrobíš jednu instanci a tu pak používáš na jednom místě.

Což v tvém případě vypadá jako takřka identická záležitost, jako kdyby ta služba byla třeba formulář v malé aplikaci, kde je jen jeden formulář.

Ale u formuláře cítím, že je to spíš shoda okolností, že je v aplikaci jen jeden, a určitě nepotřebuju, aby se předával na různé místa aplikace pomocí autowiringu, takže bych si i na ten jeden formulář udělal raději továrnu, tedy bych šel cestou 2 automaticky.

První odstavec jsi pochopil správně, zatím je to na jednom místě a asi to do budoucna nebudu potřebovat, ale co kdyby.
Potřebuji jen jak jsem již psal, nastavit typ parametru, takže řeším jak co nejvíc čitelněji a neskrytě. To ostatní jak jsem psal v předposledním postu pro mé potřeby docela v pořádku.

A formulář asi do toho netahejme, v tomto místě (administrace) je prostě formulář, na jiném místě to bude řádek z DB, takže potřebuji zase nastavit datový typ a něco s hodnotou toho parametru udělat.

Takže továrna co přes typ vyrobní onu třídu? A je jedno, že v tuto chvili to potřebuji na jednom místě a vic než jednu instanci zatím nepotřebuji a třeba ani potřebovat nebudu, je to v pořádku? Nebo je nějaké pravidlo, že továrna se zpravidla dělá pro potřeby více instancí?

Pozorně jsem četl https://doc.nette.org/…introduction#… takže si nejsem jist jestli postupuji správně.

PS. Nějaký nástřel kódu (stačí pseudo) at vím přesně co tím myslíš?

Editoval Klobás (21. 11. 2023 23:29)

Klobás
Člen | 113
+
0
-

mystik napsal(a):

$form->addText('foo')->setOption('validationType', 'Text');

Díky okomentoval jsem to dalším postem a snad jsem pochopitelně vysvětlil, že s formulářovým prvkem to nemá nic společného :-), díky.

Klobás
Člen | 113
+
0
-

Kamil Valenta napsal(a):

Pokud jsou filtry a validace závislé na setType, tak by mi ve všech případech vadilo, že to nemám na očích. Že zavolám na dvou místech $trida->validate() a ona mi jednou vrátí true, podruhé false. A já si musím všimnout, že někde je jiný ->setType, konstruktor, factory…

Asi by se mi pak více líbila $trida jako wrapper nad poděděnými třídami dle type.
Volání

$trida->text->validate();
$trida->numeric->validate();

je pak jednoznačné.

Pokud filtry na type závislé nejsou, pak bych jen separoval jednotlivé validační metody.

$trida->validateText();
$trida->validateNumeric();

Myšlenku chápu, ale asi to není potřeba, nebo co si mám představit pod $trida->text->validate() že se interně provede?

A filtry na sobě zavislé nejsou, existuje obecný seznam, (samozřejmě některé filtry mají jasné regexpy pro konkrétní situace) ale můžou se mezi sebou kombinovat.

Kamil Valenta
Člen | 762
+
0
-

Takže vůbec nepotřebuješ znát type v počátku. Je Ti přece jedno, jaká hodnota do objektu vstoupí.
Na vstupu můžeš mít z inputu „1“, a při volání:

$trida->validateText(); // => (string) "1"
$trida->validateNumeric(); // => (int) 1
$trida->validateBoolean(); // => (bool) TRUE

Už je více jasné, proč to chtěl Tvůj kolega jako parametr metody validate(). Jako samostatné metody to ale bude napovídat a člověk si nemusí pamatovat možné hodnoty pro type (které by byly konstantami).

Klobás
Člen | 113
+
0
-

Kamil Valenta napsal(a):

Takže vůbec nepotřebuješ znát type v počátku. Je Ti přece jedno, jaká hodnota do objektu vstoupí.
Na vstupu můžeš mít z inputu „1“, a při volání:

$trida->validateText(); // => (string) "1"
$trida->validateNumeric(); // => (int) 1
$trida->validateBoolean(); // => (bool) TRUE

Už je více jasné, proč to chtěl Tvůj kolega jako parametr metody validate(). Jako samostatné metody to ale bude napovídat a člověk si nemusí pamatovat možné hodnoty pro type (které by byly konstantami).

Ahoj,

no to ano, ale nepoužije se jenom validate, použije se ještě addFilter a případně se může použít zase další metoda, která uvnitř pracuje s tím typem, takže hypoteticky budu muset v celém procesu použít 3 funkce závislé na typu a tech typů bude později více třeba 10, tak budu muset mít ve výsledku 30 funkcí dle této logiky? (Ano, dá se to přes __call na pozadí vyřešit, abych se neupsal, ale to se mi nelíbí).

Postup ve smyslu

<?php
$type = 'Numeric';
$trida = tridaFactory->create($type);

$trida->addFilter($formField);
$trida->validate($formField);
$trida->necoDalsiho();

?>

mi přijde pochopitelnější než

<?php
$trida->addFilterNumeric($formField);
$trida->validateNumeric($formField);
$trida->necoDalsihoNumeric();
?>

nehledě na to, že klidne v onom jednom procesu zavolám addFilterNumeric a podtím (protože jsem blb) validateText a logicky to je špatně …

Pokud mám někde mezeru, prosím o upřesnění a díky za čas.

Kamil Valenta
Člen | 762
+
0
-

Před chvílí jsi uvedl, že filtry na typu závislé nejsou, tak proč bys volal addFilterNumeric()…
Pokud je to tedy tak, že celý proces s hodnotou je na type závislý, nechal bych to jako samostatné třídy.

$type = 'Numeric';

Tohle je peklo. Co když někdo napíše

$type = 'numeric';
$type = 'Number';

apod.? Konstanty jsou tady minimum.

Předpokládám, že stejně budeš vždy validovat větší hromádku dat, takže si tu factory budeš volat pro každý type.
Proto mi dává větší smysl mít to rovnou samostatně a pracovat s tím jako $trida->numeric->validate().

Klobás
Člen | 113
+
0
-

Filtry uvnitř jsou zavislé na typu.

Takhle jsem to myslel.

<?php
	private function getRules(string $type): array
	{
		$x = [
			'Text' => [
				'validators' => [],
				'filters' => [
					'filterTrim'
				],
			],
			'Boolean' => [
				'validators' => ['validateIsBoolean'],
				'filters' => [
					'filterTrim',
					'filterLowerCase',
				],
			],
			'Numeric' => [
				'validators' => ['validateIsNumber', 'validateIsNumberRange'],
				'filters' => [
					'filterTrim',
					'filterCommaToDot',
					'filterRemoveSpaces',
					'filterRangeWords',
					'filterRangeDash',
				],
			]
		];

		return $x[$type];
	}

?>

Můžeš mi říct, co se na pozadí stane, když zavolám $trida->**numeric**->validate()

Jinak samozřejmě ta třída má uvnitř seznam podporovaných typů a kontrolu, jinak selže (Exception).

Kamil Valenta
Člen | 762
+
0
-

Klobás napsal(a):

Můžeš mi říct, co se na pozadí stane, když zavolám $trida->**numeric**->validate()

$trida je kolekce tříd předávaná jako service.
$trida->numeric->validate() zavolá metodu validate() nad objektem numeric.
Pokud totiž každý „type“ má své validátory, své filtry, případně své vlastní metody necoDalsiho(),
nedává smysl mít to v jedné třídě a oddělovat to typem.
Měly by to být samostatné třídy se společným interfacem.

Jinak samozřejmě ta třída má uvnitř seznam podporovaných typů a kontrolu, jinak selže (Exception).

A necítíš rozdíl mezi psaním stringu a mačkáním F5 dokud se exceptiony nezbavím a napsáním → a našeptáním možných variant?

Klobás
Člen | 113
+
0
-

Kamil Valenta napsal(a):

Klobás napsal(a):

Můžeš mi říct, co se na pozadí stane, když zavolám $trida->**numeric**->validate()

$trida je kolekce tříd předávaná jako service.
$trida->numeric->validate() zavolá metodu validate() nad objektem numeric.
Pokud totiž každý „type“ má své validátory, své filtry, případně své vlastní metody necoDalsiho(),
nedává smysl mít to v jedné třídě a oddělovat to typem.
Měly by to být samostatné třídy se společným interfacem.

Jinak samozřejmě ta třída má uvnitř seznam podporovaných typů a kontrolu, jinak selže (Exception).

A necítíš rozdíl mezi psaním stringu a mačkáním F5 dokud se exceptiony nezbavím a napsáním → a našeptáním možných variant?

Pochopil jsem :-)

mystik
Člen | 292
+
+2
-

Pokud $type neni nejaky predavany parametr ale uvadis ho tam exixitne tak souhlas s @KamilValenta. Idealne mit polymorfni tj. interface Validator, jeho implementace jako NumericValidator, TextValidator a ta predavana service bude tovarna nebo rovnou kolekce hotovych instanci.

Mas nejaky duvod proc pouzivas takovy slozity config v poli misto explicitniho kodu? Chapal bych pokud ten config musis nacitat z nejakeho externiho zdroje, ale tohle vypada jen jako hardcoded config. A takovahe zbytecna magie je dobry zaklad na spoustu osklivych bugu, narocne refaktorovani, neumozni to naseptavani a statickou analyzu, …

Pokud config chces zachovat tak bych minimalne zvazil si ty filtry predavat jako first-class callable a ne string.

Klobás
Člen | 113
+
0
-

mystik napsal(a):

Pokud $type neni nejaky predavany parametr ale uvadis ho tam exixitne tak souhlas s @KamilValenta. Idealne mit polymorfni tj. interface Validator, jeho implementace jako NumericValidator, TextValidator a ta predavana service bude tovarna nebo rovnou kolekce hotovych instanci.

Mas nejaky duvod proc pouzivas takovy slozity config v poli misto explicitniho kodu? Chapal bych pokud ten config musis nacitat z nejakeho externiho zdroje, ale tohle vypada jen jako hardcoded config. A takovahe zbytecna magie je dobry zaklad na spoustu osklivych bugu, narocne refaktorovani, neumozni to naseptavani a statickou analyzu, …

Pokud config chces zachovat tak bych minimalne zvazil si ty filtry predavat jako first-class callable a ne string.

Uff. Nejsem backendak, pulce věcí nerozumím, což není omlouva, ale je to prostě tak.

Nevím co je first-class callable, něco jako [Trida:class] ?

Vzniklo to jako nevinná hra a začalo se to nabalovat, není prostor pro refactoring (časový ani finanční).

Každopádně díky za pohled.

mystik
Člen | 292
+
+2
-

PHP od verze 8.1 umoznuje callback zapsat jako $callback = $trida->metoda(...) (ty tri tecky jsou primo ta syntaxe). Ten callback pak jednoduse zavolas jako $callback($param). Zustane ti ale typova kontrola a naseptavani na rozdil od toho kdy jmena metod pises jako obyc string