Statická proměnná v DI, opravdu zlo?

Klobás
Člen | 113
+
0
-

V tomto mimochodem velmi pěkném článku https://doc.nette.org/…global-state#… je napsáno, že

Neexistují žádné výjimky, žádné situace, kdy by globální stav byl užitečným kamarádem. Každé static $foo uvnitř kódu znamená problém. Aby váš kód byl prostředím respektujícím DI, je nezbytné úplně vymýtit globální stav a nahradit ho pomocí dependency injection.

Já třeba dost často používám statickou proměnnou kvůli zakešování SQL výsledku v metodě. Něco jako

<?php

public function getList()
{
   static $cache;
   if ($cache) {
	return $cache
	}

	$cache = $this->table('xx')->where(..)->fetch()->toArray();

	return $cache;
}

?>

a když volám funkci víckrát na 1 stránce v různých částech, tak se vykoná jen 1× dotaz.

Znamená to, že to je špatně a mám to tedy nahradit, čím a jak?

Infanticide0
Člen | 64
+
-1
-

použij https://doc.nette.org/cs/caching#…

třeba nějak takhle:

private Cache $cache;
public function __construct(Storage $storage) {
	$this->cache = new Cache($storage, 'xx');
}

public function getList()
{
	$data = $cache->load('xx', function (&$deps) {
		$deps[Cache::Expire] = "60 minutes"; // automatická expirace po 60 minutách (ve starší verzi je to ::EXPIRE)
		return $this->table('xx')->where(..)->fetch()->toArray();
	});
}

Editoval Infanticide0 (30. 3. 2023 21:43)

Martk
Člen | 656
+
0
-

Zkus spustit tento kód

class Foo {

	public function __construct(
		private int $value,
	)
	{
	}

	public function getList(): int
	{
		static $cache;

		if ($cache) {
			return $cache;
		}

		return $cache = $this->value;
	}

}

$first = new Foo(1);
$second = new Foo(2);

var_dump($first->getList());
var_dump($second->getList());

Výsledek bude:

int(1)
int(1)

PS: Tohle asi není odpověď úplná odpověď na tvoji otázku, ale tady je jen příklad proč jsou static dobrý sluha, ale zlý pán.

Editoval Martk (30. 3. 2023 23:03)

Klobás
Člen | 113
+
0
-

Martk napsal(a):

Zkus spustit tento kód

class Foo {

	public function __construct(
		private int $value,
	)
	{
	}

	public function getList(): int
	{
		static $cache;

		if ($cache) {
			return $cache;
		}

		return $cache = $this->value;
	}

}

$first = new Foo(1);
$second = new Foo(2);

var_dump($first->getList());
var_dump($second->getList());

Výsledek bude:

int(1)
int(1)

PS: Tohle asi není odpověď úplná odpověď na tvoji otázku, ale tady je jen příklad proč jsou static dobrý sluha, ale zlý pán.

Ahoj, díky za reakci.

Tomuhle kódu samozřejmě rozumím, nicméně, je to zcela něco jiného než můj ne? Ten můj je klasická služba s 1 instancí. A kešuju tímto volání SQL dotazu do statické proměnné.

Kdybych potřeboval parametr tak si z toho udělám pole a kešuju to třeba dle ID nebo jiných parametrů.

Takže mi přijde, že zrovna mého případu se onen špatný návrh netýká, v tvém případě je jasné, že by to nefungovalo dobře.

Nebo je tedy jiná alternativa? Kešování je imho zbytečné, nepotřebuji kešovat ani na 1s jen zabránit vícenásobnému volání. Samozřejmě, jde to napsat i jinak, ale ptám se na konkrétni use-case.

MajklNajt
Člen | 471
+
-1
-

a čo tak nešpekulovať a proste to napísať takto?

class Foo {
	private ?array $cache = null;

	public function getList(): array
	{
		if ($this->cache === null) {
			return $this->cache;
		}

		return $this->cache = $this->table('xx')->where(..)->fetch()->toArray();
	}
}

používam úplne bežne…

EDIT: opravené if ($this->cache) – diky @h4kuna

Editoval MajklNajt (31. 3. 2023 13:18)

Klobás
Člen | 113
+
-1
-

MajklNajt napsal(a):

a čo tak nešpekulovať a proste to napísať takto?

class Foo {
	private ?array $cache = null;

	public function getList(): array
	{
		if ($this->cache) {
			return $this->cache;
		}

		return $this->cache = $this->table('xx')->where(..)->fetch()->toArray();
	}
}

používam úplne bežne…

Ahoj,
to není spekulace.

Tvůj způsob je možný také, ale ten bych použil na sdílení více dotazů nebo bych to ukládal do pole.

Viz. 1 a 2 https://diskuse.jakpsatweb.cz/?…

Jen se snažím pochopit viz ten začátek, proč je statická proměnná zlo :-), přijde mi totiž, že v tomto případe je to v pořádku.

Editoval Klobás (31. 3. 2023 9:40)

mystik
Člen | 292
+
+1
-

Problem je ze to spoleha na to ze vzdy a vsude bude prave jen ta jedna instance. Coz je mozna pravda ted, ale nemusi byt pravda az ten kod za rok nekdo upravi podle novych pozadavku.

Klobás
Člen | 113
+
0
-

mystik napsal(a):

Problem je ze to spoleha na to ze vzdy a vsude bude prave jen ta jedna instance. Coz je mozna pravda ted, ale nemusi byt pravda az ten kod za rok nekdo upravi podle novych pozadavku.

A alternativa? Jinak snad každý kdo to vidí, musí chápat co to dělá ne?

Editoval Klobás (31. 3. 2023 10:24)

mystik
Člen | 292
+
+1
-
  1. Cachovat v instancni a ne staticke property.
  2. Injectnout si cache jako zavislost (pak se lip pracuje s invalidaci)

Ten kdo s tim kodem bude pracovat nemusi cist kod celyho projektu, aby tyhle chytaky objevil a vedel ze si na ne ma dat pozor.

Klobás
Člen | 113
+
0
-

mystik napsal(a):

  1. Cachovat v instancni a ne staticke property.
  2. Injectnout si cache jako zavislost (pak se lip pracuje s invalidaci)

Ten kdo s tim kodem bude pracovat nemusi cist kod celyho projektu, aby tyhle chytaky objevil a vedel ze si na ne ma dat pozor.

  1. Do kódu se musí podívat tak jako tak ne (Je jedno jestli si to daný člověk zkontroluje v instanční property nebo přímo v té metodě jako statickou proměnnou)? Navíc je zřejmé, k čemu ta statická slouží. Když to bude property třídy, tak bude definovaná někde nahoře a není ani hned v té metodě. Když budu chtít kešovat víc SQL dotazů, tak budu mít víc properties nebo jednu sdílenou (obojí se mi nelíbí).
  2. Invalidovat netřeba, nejedná se o keš jako takovou s nějakým nastaveným TTL, tady jde přeci jen o to, aby se zamezilo vykonání ekvivalentního dotazu napříč jedním requestem.

Díky za názor.

dakur
Člen | 493
+
0
-

A proč tu funkci vůbec voláš se stejnými parametry víckrát na jedné stránce – k čemu to potřebuješ? Nedává smysl ten výsledek spíš předat nějak v presenteru nebo kde to používáš? Pak bys žádnou cache ani nepotřeboval.

Editoval dakur (31. 3. 2023 12:12)

h4kuna
Backer | 740
+
+1
-

Pokud potřebuješ dočasnou keš, tak stále není potřeba statika, právě kvůli tomu co ti ukazoval @Martk.

  1. Mohl by jsi použít keš s MemoryStorage, která drží data jen po dobu requestu, pak to jde udělat pomocí DI.
  2. Můžeš si udělat property a ulož to do ní.

Pro tyhle případy jde udělat traita, kterou používám. Funguje jen při requestech, pokud to pustíš v procesu, nikdy se neinvaliduje, je to tak schválně.

  1. zvaš co píše @dakur

Editoval h4kuna (31. 3. 2023 12:18)

Klobás
Člen | 113
+
0
-

dakur napsal(a):

A proč tu funkci vůbec voláš se stejnými parametry víckrát na jedné stránce – k čemu to potřebuješ? Nedává smysl ten výsledek spíš předat nějak v presenteru nebo kde to používáš? Pak bys žádnou cache ani nepotřeboval.

Mám třeba následující model.

<?php
	public function getList(): array
	{
		$this->getBase();
		return $this->list;
	}


	public function getFullList(): array
	{
		$this->getBase();
		return $this->fullList;
	}

A metoda getBase je práve zakešovaná a ukládá do členských properties (list / fullList .. je jich víc) data a já na to mám pak gettery, a různě je volám .. přijde mi to OK.

?>
Klobás
Člen | 113
+
0
-

h4kuna napsal(a):

Pokud potřebuješ dočasnou keš, tak stále není potřeba statika, právě kvůli tomu co ti ukazoval @Martk.

  1. Mohl by jsi použít keš s MemoryStorage, která drží data jen po dobu requestu, pak to jde udělat pomocí DI.
  2. Můžeš si udělat property a ulož to do ní.

Pro tyhle případy jde udělat traita, kterou používám. Funguje jen při requestech, pokud to pustíš v procesu, nikdy se neinvaliduje, je to tak schválně.

  1. zvaš co píše @dakur
  1. Zbytečně složité proti jedné statické proměnné, kterou vím jak používat, nevidím v tom bohužel zlo jako vy všichni ostatní …
  2. To dělám viz předchozí ukázka.
mystik
Člen | 292
+
+2
-

Kdyz pouzivas nejakou tridu nemelo by byt nutne si nejdriv prostudovat cely jeji kod.

Je spousta moznosti jak ti statika muze zpusobit problemy.

Napr. Co kdyz jian trida zmeni data ktera mas nacachovana v te staticke cachi? Najednou se system zkatujr chybna data a nevis proc dokud nanajdes nekde schovanou tuhle cache.

Je to sice jednodussi zapis ale za cenu toho ze je mnohem slozitejsi si udrzet prehled kde vsude se neco takoveho deje a dat si na to pozor.

Udrzovat takovy kod je pak mnohem slozitejsi.

Klobás
Člen | 113
+
0
-

mystik napsal(a):

Kdyz pouzivas nejakou tridu nemelo by byt nutne si nejdriv prostudovat cely jeji kod.

Je spousta moznosti jak ti statika muze zpusobit problemy.

Napr. Co kdyz jian trida zmeni data ktera mas nacachovana v te staticke cachi? Najednou se system zkatujr chybna data a nevis proc dokud nanajdes nekde schovanou tuhle cache.

Je to sice jednodussi zapis ale za cenu toho ze je mnohem slozitejsi si udrzet prehled kde vsude se neco takoveho deje a dat si na to pozor.

Udrzovat takovy kod je pak mnohem slozitejsi.

Oceňuji, že to řešíš do hloubky a snažíš se mi to vysvětlit.

Nicméně, to co tu padlo a to mít to v členské property, je to samé v bleděmodrém …
Fungovat to bude stejně, pokud to budu stejně používat a stejně se do té třídy budu muset podívat.

Nevidím v tom rozdíl. Argument, že kdybych chtěl jinou instanci nebo jiné parametry tak to shoří obojí (jak statická tak členská).

Ale jinak mi to asi stačí.

mystik
Člen | 292
+
0
-

Jedine misto kde to je imho ok je cache vysledku pure metod – tedy tam kde vysledek zavisi jen a pouze na hodnotach parametru a nicem externim.

mystik
Člen | 292
+
+1
-

Clenska neshori protoze nova instance bude mit nezavislou cache. Ale nejlepsi je mit cache externe jako explicitni zavislost, aby jsi mohl resit i ty invalidace.

mystik
Člen | 292
+
0
-

Ty nwvyhody nejvic vyniknou v okamziku kdy zacnes pro ten kod psat automaticke testy. Zjistis ze nektere veci zacnou byt neresitelne.

Klobás
Člen | 113
+
0
-

mystik napsal(a):

Clenska neshori protoze nova instance bude mit nezavislou cache. Ale nejlepsi je mit cache externe jako explicitni zavislost, aby jsi mohl resit i ty invalidace.

Ok díky! Přepíšu.

m.brecher
Generous Backer | 765
+
-1
-

@Klobás

Jen se snažím pochopit viz ten začátek, proč je statická proměnná zlo :-), přijde mi totiž, že v tomto případe je to v pořádku.

Takhle teoreticky s vymýcením statických proměnných nelze než souhlasit. Máš ale pravdu, že je potřeba jednoduše vyřešit problém duplicity sql dotazů, protože to se stává dost často, že jedny data (typicky ActiveRow, ale i Selection) se v rámci jednoho presenteru využívají na více místech.

Jak průběžně vidím na fóru, řeší to lidi různými způsoby a žádné nejlepší řešení není nejlepší.

Ty Jsi to vyřešil cachováním sql dotazu rovnou v metodě modelu a to v rámci jednoho requestu, kde jak píšeš není žádná invalidace cache nutná. S tím já souhlasím, pokud je presenter jednoduchý tak problém nebude. Při dalším rozvoji presenteru ale k chybě dojít může díky tomu, že cachování je schované (popisuje @mystik).

Cachování můžeš zachovat, ale do property presenteru:

class ArticlePresenter extends BasePresenter
{
   private Selection $articles;

   public function __construct(
      private ArticleModel $articleModel,
      private ArticleMenuFactory $articleMenuFactory,
   )

   public function actionList()
   {
      $this->articles = $this->articleModel->getList();
      ... // první použití dat
   }

   public function renderList()
   {
      $this->template->articles = $this->articles; // druhé použití dat
   }

   public function createComponentArticleMenu(): ArticleMenu
   {
      $this->articleMenuFactory->create(articles: $this->articles); // třetí použití dat
   }
}

Data se cachují, ale přehledným způsobem.

Bohužel nám toto nabourává jiný doporučovaný návrhový vzor a sice delegování práce presenteru do komponent, kdy factory dodá komponentě služby které potřebuje nezávisle na presenteru. Takhle jak jsem to napsal totiž data komponentě dodává presenter.

Bohužel pro často se vyskytující případy kdy více komponent využívá stejná data ideální řešení neexistuje:

a) data dodáme opakovaně z modelu – sql bude duplicitní
b) zacachujeme data v modelu – nepřehledné
c) data cachujeme v property presenteru – komponenta nebude nezávislá

I když nejde splnit všechny dobře míněné návrhové vzory současně, protože se vlastně navzájem vylučují, tak třeba řešení c) vůbec špatné není a pokud se komponenta přesune do traity tak skoro ideální.

mystik
Člen | 292
+
0
-

Za me idealni cachovat v modelu. Je to nejobvyklejsi misto. Je to pro celou aplikaci misto opakovani v ruznych presentetech/komponentach. A hlavne je to misto kde muzes nejlepe resit invalidaci protoze pres model ti jdou i zmeny dat.

m.brecher
Generous Backer | 765
+
0
-

@mystik

Za me idealni cachovat v modelu.

A používáš jakou databázovou knihovnu? Nette\Database\Explorer nebo nějaký ORM?

Mohu požádat o kousek kódu na ukázku jak takový cachovaný model vypadá? Ideálně ve variantě pro Nette\Database\Explorer, ale nevadí i ve verzi ORM.

mystik
Člen | 292
+
+1
-

Mam vlastni „ORM“ nad Nette\Database\Explorer. Nejsem ted u PC, ale neni tam noc sloziteho. Injectnuta nette Cache a volani metod z Repository se v nekterych pripadech hodi do cache. Obvykle resim jen sem tam pro veci jako ciselniky.

m.brecher
Generous Backer | 765
+
0
-

@mystik

Mam vlastni „ORM“ nad Nette\Database\Explorer. Nejsem ted u PC, ale neni tam noc sloziteho.

Joo, to by mě zajímalo, kdyby Jsi si na mě náhodou někdy vzpomněl, něco takového bych si rád prohlédl, abych nemusel vynalézat kolo ;)

Obvykle resim jen sem tam pro veci jako ciselniky.

To znamená, že to cachuje dlouhodobě v přesahu přes několik requestů, třeba i řádově měsíce – do další změny v číselníku?

Btw v tomto vlákně se řeší duplicita sql v rámci jednoho requestu. Napadá mě, že když proběhnou dva/tři duplicitní sql dotazy v jednom presenteru bleskově za sebou v rámci jednoho připojení, tak databáze do hloubky neznám, ale tipoval bych, že toto bude nějakým způsobem v databázovém serveru cachováno a zátěž vzniklá duplicitními dotazy bude minimální. Takže v nízkozátěžové aplikaci by se duplicity sql možná nemusely řešit.

Klobás
Člen | 113
+
0
-

mystik napsal(a):

Clenska neshori protoze nova instance bude mit nezavislou cache. Ale nejlepsi je mit cache externe jako explicitni zavislost, aby jsi mohl resit i ty invalidace.

Ano :), ale pořád opakuji, že to je služba a ta novou instanci mít nebude. Je to až moc hypotetické a moc kdyby / kdyby nahodou / až.

Klobás
Člen | 113
+
0
-

m.brecher napsal(a):

@Klobás

Jen se snažím pochopit viz ten začátek, proč je statická proměnná zlo :-), přijde mi totiž, že v tomto případe je to v pořádku.

Takhle teoreticky s vymýcením statických proměnných nelze než souhlasit. Máš ale pravdu, že je potřeba jednoduše vyřešit problém duplicity sql dotazů, protože to se stává dost často, že jedny data (typicky ActiveRow, ale i Selection) se v rámci jednoho presenteru využívají na více místech.

Jak průběžně vidím na fóru, řeší to lidi různými způsoby a žádné nejlepší řešení není nejlepší.

Ty Jsi to vyřešil cachováním sql dotazu rovnou v metodě modelu a to v rámci jednoho requestu, kde jak píšeš není žádná invalidace cache nutná. S tím já souhlasím, pokud je presenter jednoduchý tak problém nebude. Při dalším rozvoji presenteru ale k chybě dojít může díky tomu, že cachování je schované (popisuje @mystik).

Cachování můžeš zachovat, ale do property presenteru:

class ArticlePresenter extends BasePresenter
{
   private Selection $articles;

   public function __construct(
      private ArticleModel $articleModel,
      private ArticleMenuFactory $articleMenuFactory,
   )

   public function actionList()
   {
      $this->articles = $this->articleModel->getList();
      ... // první použití dat
   }

   public function renderList()
   {
      $this->template->articles = $this->articles; // druhé použití dat
   }

   public function createComponentArticleMenu(): ArticleMenu
   {
      $this->articleMenuFactory->create(articles: $this->articles); // třetí použití dat
   }
}

Data se cachují, ale přehledným způsobem.

Bohužel nám toto nabourává jiný doporučovaný návrhový vzor a sice delegování práce presenteru do komponent, kdy factory dodá komponentě služby které potřebuje nezávisle na presenteru. Takhle jak jsem to napsal totiž data komponentě dodává presenter.

Bohužel pro často se vyskytující případy kdy více komponent využívá stejná data ideální řešení neexistuje:

a) data dodáme opakovaně z modelu – sql bude duplicitní
b) zacachujeme data v modelu – nepřehledné
c) data cachujeme v property presenteru – komponenta nebude nezávislá

I když nejde splnit všechny dobře míněné návrhové vzory současně, protože se vlastně navzájem vylučují, tak třeba řešení c) vůbec špatné není a pokud se komponenta přesune do traity tak skoro ideální.

Asi záleží jak člověk potřebuje, ale kešování v presenteru jsem ještě nedělal nikdy. Ale i třeba vytváření komponent v Presenter lze → doporuji přečíst https://f3l1x.io/…te-sablonach

Díky všem, bylo to poučné i náročné.

Editoval Klobás (31. 3. 2023 14:50)

m.brecher
Generous Backer | 765
+
0
-

@Klobás

doporuji přečíst https://f3l1x.io/…te-sablonach

+ jak psal @mystik :

Injectnuta nette Cache a volani metod z Repository se v nekterych pripadech hodi do cache. Obvykle resim jen sem tam pro veci jako ciselniky.

V obou případech to tam sice není jasně napsané, ale dělá to na mě dojem, že autoři cachují na delší dobu nějaké složité dotazy – třeba dny/týdny, že se tam nemá na mysli cachování v rámci jednoho requestu. Ty dotazy mohou za sebou následovat v řádu milisekund. Má smysl na tu dobu to ukládat do cache úložiště a zase z něj číst? Řekl bych že smysl má to uložit do proměnné v RAM paměti – na tak krátkou dobu – tedy property presenteru nebo nějaká proměnná v modelu.

Klobás
Člen | 113
+
-1
-

m.brecher napsal(a):

@Klobás

doporuji přečíst https://f3l1x.io/…te-sablonach

+ jak psal @mystik :

Injectnuta nette Cache a volani metod z Repository se v nekterych pripadech hodi do cache. Obvykle resim jen sem tam pro veci jako ciselniky.

V obou případech to tam sice není jasně napsané, ale dělá to na mě dojem, že autoři cachují na delší dobu nějaké složité dotazy – třeba dny/týdny, že se tam nemá na mysli cachování v rámci jednoho requestu. Ty dotazy mohou za sebou následovat v řádu milisekund. Má smysl na tu dobu to ukládat do cache úložiště a zase z něj číst ?? Řekl bych že smysl má to uložit do proměnné v RAM paměti – na tak krátkou dobu – tedy property presenteru nebo nějaká proměnná v modelu.

Ano. Víceméně se tu řeší jak kešovat a moc to nesouvisí s tím jak přesně píšeš: cachování v rámci jednoho requestu.
Za mě je použití statické property přímo v metodě prostě OK.

mystik
Člen | 292
+
0
-

Nette ma i MemoryStorage to pouzivam na cache v ramci requestu.

Uvedu ti priklad, jak si snadno muzes neco rozbit takovouhle cache. V ramci nejake akce aktualizujes nejaky udaj, nactes data a zobrazis je. Vsechno ok data vidis aktualizovana. Pak ale nejaka nesouviseji zmena (treba komponenta co z zahlaci zobrazi nejaky udaj) sahne na ta data pred tim nez je aktualizujes. Coz naplni cache. A najednou porad vidis stara data i kdyz dotaz co je aktualizuje se provede. A hledas chybu. Pokud to je project co detailne neznas je to problem kterym muzes stravit par hodin.

A takovy kod je taky temer nemozne unit testovat.

mystik
Člen | 292
+
0
-

@m.brecher Nemam to ORM zatim nikde verejne. Planuju ho mozna casem open sourcovat ale pred tim to bude chtit trochu ucesat a na to sem zatim nemel cas. Pokud bys ale chtel ozvi se mi na mail a poslu ti nejake ukazky jak s tim pracuju.

Klobás
Člen | 113
+
0
-

mystik napsal(a):

Nette ma i MemoryStorage to pouzivam na cache v ramci requestu.

Uvedu ti priklad, jak si snadno muzes neco rozbit takovouhle cache. V ramci nejake akce aktualizujes nejaky udaj, nactes data a zobrazis je. Vsechno ok data vidis aktualizovana. Pak ale nejaka nesouviseji zmena (treba komponenta co z zahlaci zobrazi nejaky udaj) sahne na ta data pred tim nez je aktualizujes. Coz naplni cache. A najednou porad vidis stara data i kdyz dotaz co je aktualizuje se provede. A hledas chybu. Pokud to je project co detailne neznas je to problem kterym muzes stravit par hodin.

A takovy kod je taky temer nemozne unit testovat.

Můžu poprosit o ukázku jak to používáš? Stačí mi jen to zakešování dotazu.

m.brecher
Generous Backer | 765
+
0
-

@mystik

Nette ma i MemoryStorage to pouzivam na cache v ramci requestu.

vidím že mám znalostní mezery a mám co dohánět

Pokud bys ale chtel ozvi se mi na mail a poslu ti nejake ukazky jak s tim pracuju.

Díky, ale zatím to nehoří, asi bude stačit když počkám na veřejný opensource