Value objects in value objects

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • ZeroXT
  • Registratie: December 2007
  • Laatst online: 01:23
Disclaimer:
Even voor de duidelijkheid: ik wil geen discussie starten over hoe je het beste een URL kan parsen.
Ik weet dat er 1001 bestaande oplossingen zijn, maar dit gaat puur over architectonische keuzes maken met value objects in value objects.



Een URL heeft meerdere fragmenten/componenten:
scheme, user, password, lld, sld, tld, port, path, query en fragment.

Deze componenten zijn opgeslagen in een value object. Echter is de query component op zich zelf ook een value object die opgeslagen wordt in de URL value object.

Een van de eigenschappen van een value object is dat deze immuttable is. En hiermee is direct het probleem.

Even wat (versimpelde) code ter illustratie:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class URL {

    private string $scheme;
    private Query $query;

    public function __construct(string $scheme) {
        $this->scheme = $scheme;
        $this->query = new Query();
    }

    public function setScheme(string $scheme): self {
        return new self($scheme);
    }
    
    public function getScheme(): string {
        return $this->scheme;
    }
    
    public function getQuery(): Query {
        return $this->query;
    }
}

class Query {

    private array $query = [];

    public function setParam(string $key, string $value): self {

        $instance = new self();
        $instance->query = $this->query;
        $instance->query[$key] = $value;
        return $instance;
    }

    public function getParam(string $key): ?string {
        return $this->query[$key] ?? null;
    }
}


Voorbeeld:
PHP:
1
2
3
4
$url = new Url('http');
$url->getScheme(); //http
$url = $url->setScheme('https');
$url->getScheme(); //https


So far so good. Maar wanneer de query param geset wordt, dan is er op dit moment geen mogelijkheid om deze nieuwe waarde terug te krijgen middels het "$url" object:

PHP:
1
2
$url= $url->getQuery()->setParam('foo', 'bar');
$url->getQuery()->getParam('foo'); //Error, want $url is nu een Query object


PHP:
1
2
3
$query = $url->getQuery();
$query->setParam('foo', 'bar');
$url->getQuery()->getParam('foo'); //Null


Dit is heel logisch maar wel een probleem. Ik ben hier over gaan nadenken en kwam met de volgende potentiële oplossingen. De één nog minder mooi dan de ander:
  • Zal ik in plaats van een value object, moeten werken met entities? Dit lijkt alleen geen entity naar mijn mening maar zorgt er wel voor dat deze niet immuttable is. Eventueel kan een getImmuttable method toegevoegd worden.
  • Zal alle methods moeten kopiëren van de query object naar de url object ($url->setParam() ipv $url->getQuery()->setParam()) met als gevolg dat alle methods een kopie zijn en ik me begin af te vragen waarom de query object dan nog nodig is als alleen staand object (het helpt wel tegen de anti pattern genaamd train wreck)
  • Moet er een extra methode komen genaamd setQuery in het url object die enkel het query object accepteert met als gevolg dat de gebruiker de query kan ophalen en opnieuw kan setten (wat erg omslachtig is)?
  • Moet de parent (in dit geval url) mee gegeven worden aan het query object zodat als het query object geüpdatet wordt, deze een seintje kan geven aan de parent dat deze vernieuwd is? Hierdoor is het Query object wel direct gekoppeld aan de parent waardoor deze moeilijk los gebruikt kan worden.
  • Zal ik afstappen van het idee (in dit geval) dat een value object immutable is? Vind ik persoonlijk geen mooi streven.
  • Moet ik afstappen van value objects in value object?
Wat is hier wijsheid in? Of zijn er nog andere ideeën? :)

[ Voor 3% gewijzigd door ZeroXT op 31-07-2021 13:45 ]

Alle reacties


Acties:
  • 0 Henk 'm!

  • martyw
  • Registratie: Januari 2018
  • Laatst online: 20:23
Gewoon iets bestaands gebruiken voor problemen die al opgelost zijn? Bv. https://uri.thephpleague.com/

Acties:
  • 0 Henk 'm!

  • ZeroXT
  • Registratie: December 2007
  • Laatst online: 01:23
@martyw Blijkbaar was het niet duidelijk maar dit gaat puur over architectonische keuzes maken. Ik heb de opening post aangepast met een disclaimer om links naar alle 1000 en 1 oplossingen voor het parsen van URL's te voorkomen :P

[ Voor 85% gewijzigd door ZeroXT op 30-07-2021 00:34 ]


Acties:
  • +1 Henk 'm!

  • Josk79
  • Registratie: September 2013
  • Laatst online: 23:13
Tja, momenteel kun je Url.query nooit een waarde meegeven; misschien in de constructor meegeven, of via een static factory method?

Ik vind setXxx() ook wel erg verwarrend; dat impliceert dat je een waarde kunt setten. Is withXxx() niet een betere naam?

Acties:
  • +1 Henk 'm!

  • Koenvh
  • Registratie: December 2011
  • Laatst online: 01-10 10:43

Koenvh

Hier tekenen: ______

Ontstaat het probleem niet door de volgorde die je aanhoudt?

Stel je voor je doet zoiets:
PHP:
1
2
$query = new Query()->withA("appel")->withB("peer")->withC("banaan")->withD("sinaasappel");
$url = $url->withQuery($query);

Volgens mij is het "aanpassen" dan ook een stuk makkelijker:
PHP:
1
2
3
$query = $url->getQuery();
$query = $query->withE("kiwi");
$url = $url->withQuery($query);


Met meer lagen wordt dit natuurlijk nogal een zooi. Echter als je kijkt hoe dat qua geheugen werkt, weerspiegelt dat het probleem natuurlijk wel. Op een gegeven moment moet je je natuurlijk wel gaan afvragen wat de winst van een immutable is. Je kopieert nu de hele tijd structuren. Dat is voor het geheugengebruik en snelheid niet altijd bevorderlijk. Of je gaat voor een tussenweg met het builder pattern.

Wel een leuke vraag, geloof niet dat er een perfecte manier is om het te doen :)

Ongerelateerd: ik zou setX bewaren voor functies die de staat veranderen, en withX voor immutables, anders vergeet je de "$a = " en ben je zo een paar uur kwijt met je afvragen waarom het niet werkt, zeg ik uit ervaring. O-)

[ Voor 4% gewijzigd door Koenvh op 30-07-2021 02:56 ]

🠕 This side up


Acties:
  • +1 Henk 'm!

  • Jimbolino
  • Registratie: Januari 2001
  • Laatst online: 20-09 08:54

Jimbolino

troep.com

Je setQuery is eerder een createNewQuery en het is idd erg onhandig om op dat niveau een nieuw object te maken.

Persoonlijk zou ik als het mogelijk is voorkomen dat je objecten in objecten stopt. Dus in dit geval zou ik de hele Query class droppen en vervangen door een simpele array.

Je kunt trouwens best het Query object immutable maken, moet je gewoon alle set functies ervan verwijderen, en alleen via de __construct nieuwe data accepteren.
Moet je wel je URL een setQuery geven natuurlijk.

The two basic principles of Windows system administration:
For minor problems, reboot
For major problems, reinstall


Acties:
  • 0 Henk 'm!

  • ZeroXT
  • Registratie: December 2007
  • Laatst online: 01:23
Helemaal eens met de naamgeving. In plaats van setXXX zou withXXX gebruikt moeten worden :).
Jimbolino schreef op vrijdag 30 juli 2021 @ 05:40:
Persoonlijk zou ik als het mogelijk is voorkomen dat je objecten in objecten stopt. Dus in dit geval zou ik de hele Query class droppen en vervangen door een simpele array.
Dat zou kunnen in het bovenstaande voorbeeld. Echter kan ik me voorstellen dat je nog meer methoden zou willen toevoegen voor het ophalen en wegschrijven van een enkele parameter en ook de query als string en als array zou willen ophalen. Dus dat zou betekenen dat al deze methoden ook naar de parent (url object) verplaatst moeten worden.
Koenvh schreef op vrijdag 30 juli 2021 @ 02:51:
Of je gaat voor een tussenweg met het builder pattern.
De builder pattern is ook iets waar ik aan zat te denken, maar in dit geval zou het "gemakkelijk" moeten zijn om een URL aan te maken en te wijzigen inclusief onderliggende query. En juist in het stuk "wijzigen" gaat de builder pattern geloof ik niet voor je oplossen.

PHP:
1
2
3
$url = new URL('https://domain.com');
$url->getQuery()->addParam('foo', 'bar');
$url->toString(); //https://domain.com/?foo=bar


Dit is prima te maken als het object niet immutable zou zijn. Dus is dan de conclusie dat dit de oplossing is? Immers zijn design patterns een leidraad maar niet vast in beton gegoten.

Acties:
  • +1 Henk 'm!

  • eamelink
  • Registratie: Juni 2001
  • Niet online

eamelink

Droptikkels

Zoek eens op ‘lenses’, dat is een manier om het lezen en updaten van data te composen.

Dus een lens van url naar query kan je composen met een lens van query naar param en dat geeft een lens van url naar param. En met die laatste lens kan je dan een nieuwe url maken met een ge-update param.

Acties:
  • 0 Henk 'm!

  • DJMaze
  • Registratie: Juni 2002
  • Niet online
code:
1
2
addParam('foo[]', 'bar');
addParam('foo[]', 'baz');

Oh zo leuk!

Bijvoorbeeld een webshop waarin je filtert op merk en 2 merken wil...

[ Voor 33% gewijzigd door DJMaze op 31-07-2021 16:06 ]

Maak je niet druk, dat doet de compressor maar


Acties:
  • 0 Henk 'm!

  • Josk79
  • Registratie: September 2013
  • Laatst online: 23:13
ZeroXT schreef op zaterdag 31 juli 2021 @ 13:39:
Dit is prima te maken als het object niet immutable zou zijn. Dus is dan de conclusie dat dit de oplossing is? Immers zijn design patterns een leidraad maar niet vast in beton gegoten.
Tja, je zal een keuze moeten maken; immutable of mutable. Een url zal niet zomaar weer wijzigen; als je hem wijzigt is het immers een ander url, dus immutable lijkt me hier prima op zijn plaats. Als je hem mutable gaat maken heb je kans dat je je in je vinger snijdt en onbedoeld een 'bestaande' url gaat aanpassen elders in je software omdat het een reference object betreft.

Maak je hem immutable en wil je een ander url ervan maken, door bijvoorbeeld een query-parameter toe te voegen, dan kun je toch prima zoiets doen:

code:
1
2
3
4
$newUrl = $oldUrl->withParam('fiets', 'bel');
//of
//Constructor is bijv __construct( $url, $query );
$newUrl = new Url( $oldUrl->getUrl(), $oldUrl->getQuery()->withParam('fiets', 'bel') );


of inderdaad middels een builder... zoveel smaken!

Acties:
  • 0 Henk 'm!

  • ZeroXT
  • Registratie: December 2007
  • Laatst online: 01:23
Volgens de SOLID principles zou je kunnen discussiëren of het URL object wel de verantwoordelijkheid zou moeten hebben om als een soort gate keeper te fungeren naar het Query object om daar aanpassingen in te doen.

Dit zou denk ik ook een goede manier zijn:
PHP:
1
2
3
4
$url = new URL('https://domain.com');
$query = new Query();
$query = $query->addParam('foo', 'bar');
$url = $url->withQuery($query);


Wat denken jullie? :)

Acties:
  • 0 Henk 'm!

  • Koenvh
  • Registratie: December 2011
  • Laatst online: 01-10 10:43

Koenvh

Hier tekenen: ______

Wat is het doel van je immutable? Als het idee is om te voorkomen dat een ander proces iets met je object doet, of dat jij iets met het object doet waardoor een ander proces in de war raakt, dan kun je ook je object by value in plaats van by reference doorgeven. Ik weet niet hoe makkelijk dat in PHP is, maar ik kan me voorstellen dat dit volstaat:
PHP:
1
2
3
4
function clone()
{
    return unserialize(serialize($this));
}

Dan kun je in plaats van foo($bar); te doen een kopie meegeven van $bar, dus foo($bar->clone()); Of je voegt $bar = $bar->clone() toe aan de functie van foo (ervan uitgaande dat de definitie van foo niet foo(&$bar) is, want dan schiet je nog niks op).

Voordeel is dat je gewoon je object als mutable kan gebruiken, en dus minder omslachtig hoeft te doen bij wijzigingen.

[ Voor 8% gewijzigd door Koenvh op 01-08-2021 15:50 ]

🠕 This side up


Acties:
  • 0 Henk 'm!

  • thlst
  • Registratie: Januari 2016
  • Niet online
PHP:
1
return clone $this;


werkt ook

Acties:
  • +1 Henk 'm!

  • drm
  • Registratie: Februari 2001
  • Laatst online: 09-06 13:31

drm

f0pc0dert

Ik krijg de indruk dat je de immutability wilt "hiden" / encapsulaten en dat is m.i. een abstractie die nergens toe dient. Je class is of mutable, of niet. Ik zou je daarom aanraden je builder class en je value class uit elkaar te houden, dan wordt het pattern en hoe je het hoort te gebruiken vanzelf duidelijk. Het probleem is nu dat je builder logica in je value object wilt stoppen en dat verwart de boel alleen maar; is het nou mutable of niet? Maar als ze gescheiden zijn wordt de oplossing m.i. evident, dus iets in de trant van:

PHP:
1
2
3
4
5
6
7
8
$url = UrlBuilder::fromString($theStringUrl)->build();

$otherUrl = UrlBuilder::fromUrl($url)
   ->setQuery(
      QueryBuilder::fromQuery($url->getQuery())
         ->setParam('foo', 'bar')
         ->build()
   )->build();


Vanzelfsprekend kun je hier een utility function omheen bouwen, maar 't begint dan wel heel gauw op een sterk staaltje overengineering te lijken. In het geval van urls zou ik eerder de extra abstractie eruit halen en de parameters gewoon onderdeel maken van de UrlBuilder zodat "setQuery" alleen maar een utility method wordt om alle parameters te overschrijven; maar goed, dat mapt wellicht niet op de use case die je in gedachten hebt.

Het grote voordeel van zo'n builder is (imho) dat je nooit "incomplete" objecten kunt hebben, en dat is juist ook wat je met immutability probeert te bereiken: het voorkomen van variaties in state en daarmee complexiteit, hetgeen je code eenvoudiger en begrijpelijker maakt en dat betekent op zijn beurt doorgaans minder bugs.

[ Voor 1% gewijzigd door drm op 07-08-2021 13:44 . Reden: rml ipv markdown 8)7 ]

Music is the pleasure the human mind experiences from counting without being aware that it is counting
~ Gottfried Leibniz


  • ChaZy
  • Registratie: September 2004
  • Laatst online: 02-09 21:34
My 2 cents,

Aangezien dit object geen identificatie heeft, denk ik dat je de juiste keuze hebt gemaakt om hier een value object van te maken. Van nature zijn value objects immutable en dat moet dus ook zo blijven.

Ik zou voor je tweede optie gaan, waarbij je niet perse direct de methods van de Query class kopieer maar een method maakt waarin de naamgeving duidelijk maakt dat je het over het Query stuk van de URL hebt. Dus zoiets als
code:
1
setQueryParam(string $key, string $value): self


en deze roept dan weer de setParam methode op de Query class aan, want het is de verantwoordelijkheid van de Query class om de Query string (of in dit geval Array) te beheren.

de reden waarom ik voor deze optie zou gaan is omdat je URL zou kunnen beschouwen als een aggregate root (https://martinfowler.com/bliki/DDD_Aggregate.html). dat is klopt niet helemaal omdat de class geen identifier heeft, maar de URL class zou je wel de verantwoordelijkheid kunnen geven om de URL in een valide staat te houden.

een voorbeeld van wat ik bedoel:

een verantwoordelijkheid van de URL class zou kunnen zijn dat deze, bij het uitvoeren van setQueryParam, valideert of de key niet al aanwezig is in de Query parameter. Natuurlijk heeft de Query class zelf ook deze controle maar in dat geval zou ik er voor kiezen om een exception te gooien, omdat je de query parameter in een in-valide staat probeert te drukken en dat mag nooit gebeuren. Als je de URL de verantwoordelijkheid geeft om deze validatie te doen, kun je er hier voor kiezen om geen exception te gooien maar <vul maar in>.

Als laatste, je kunt er ook over nadenken om de Query class een onder te brengen in de URL class (ik weet niet of dat kan ik PHP zelf programmeer ik in C#) en de reden daarvoor is omdat de Query class zonder de URL class geen bestaansrecht heeft. Een aantal Query parameters zonder URL is niets.

[ Voor 4% gewijzigd door ChaZy op 12-08-2021 22:38 ]


Acties:
  • 0 Henk 'm!

  • Kontsnorretje
  • Registratie: Augustus 2011
  • Laatst online: 14-06-2024
Ik denk dat het afhankelijk is van het project.

Bij een wat 'simpeler' project bouw je enkel een URL class, bij een uitgebreider project, bouw je ook een query class of interface.

Acties:
  • 0 Henk 'm!

  • krvabo
  • Registratie: Januari 2003
  • Laatst online: 01-10 21:11

krvabo

MATERIALISE!

drm schreef op zaterdag 7 augustus 2021 @ 13:37:
Ik zou je daarom aanraden je builder class en je value class uit elkaar te houden
Dit.
drm schreef op zaterdag 7 augustus 2021 @ 13:37:
maar 't begint dan wel heel gauw op een sterk staaltje overengineering te lijken.
Dit ^ x2.
drm schreef op zaterdag 7 augustus 2021 @ 13:37:
In het geval van urls zou ik eerder de extra abstractie eruit halen en de parameters gewoon onderdeel maken van de UrlBuilder zodat "setQuery" alleen maar een utility method wordt om alle parameters te overschrijven
Dit ^ x10.

Als ik dit soort code zou tegenkomen in een project zou ik het pull request afkeuren. Wat een enorme over-engineering puur en alleen maar om te kunnen laten zien hoe goed de programmeur wel niet is in OO en hun classes maar 3 regels elk zijn! Dat het compleet onpraktisch is en de onderhoudbaarheid van een kernreactor heeft doet er niet toe.

Value Object, Builder Class (/ 'UrlParser'), klaar.

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Url {
  protected string $scheme;
  protected string $domain;
  protected string $tld;
  protected array $parameters;

  public function __construct(string $scheme, string $domain, string $tld, array $parameters)
  {
    $this->scheme = $scheme; // etc
  }

 // getters

  // heel eventueel een convience method van 'getCompleteUrl' of 'getHost'
}

Dat is je value class. Niet meer. Value objects zijn dom. Letterlijk niet meer dan een manier om meerdere waardes door te geven van 1 deel van het systeem naar het andere, waarbij je weet dat je waardes niet aangepast kunnen worden. Persoonlijk ben ik geen fan van static factory methods, maar soit, het kan. Je setter method die dus niet static is maar een nieuw object maakt hoort daar dus niet thuis (want je hebt eerst een object nodig zodat je het aan kan roepen om een object te maken).




Op de vraag of je value objecten in een value object mag gebruiken? Zeker! Geen enkel probleem! Maar dan wel waar het nut heeft, en niet in de use case van een url. Denk bijvoorbeeld aan een Invoice. Een invoice value object heeft bijvoorbeeld een User, en een Product. Dat mogen prima value objects binnen een value object zijn. Je builder zorgt er dan voor dat al die data wordt gevuld

'Manipulatie' van een value object is dan als het echt-echt moet ook een taak van die builder. Bijvoorbeeld het aanpassen van het adres van de user in de invoice gebeurt dan in de builder (nieuw user object met nieuw adres, return new invoice met new user object). Maar als je hiermee bezig bent dan ben je eigenlijk gewoon bezig met het aanmaken van nieuwe objecten. Wat je dan kunt doen is je Builder geen value objecten laten returnen, maar zichzelf. Zo krijg je de ->withHost('bla')->withScheme('https'); die je kunt chainen, en als je dan klaar ben met je url roep je ->getObject() aan, en presto, je url value object.
ZeroXT schreef op vrijdag 30 juli 2021 @ 00:15:
  • Zal ik in plaats van een value object, moeten werken met entities? Dit lijkt alleen geen entity naar mijn mening maar zorgt er wel voor dat deze niet immuttable is. Eventueel kan een getImmuttable method toegevoegd worden.
Ik snap even niet wat je hiermee bedoelt. Voor mij is een entity niet anders dan een 'object'. Een tafel, een gebruiker, een ... etc. Ik zie niet in hoe dit een oplossing is voor je probleem.

Pong is probably the best designed shooter in the world.
It's the only one that is made so that if you camp, you die.

Pagina: 1