Combineren van classes
Ook heb ik een Cart class gemaakt. Ik vind het handig om alle producten zo bij te houden en het totaal bedrag te bereken. De Class methodes worden uitgevoerd via ajax calls, dus de product ids komen vanuit de voorkant meegestuurd via JS. Nu is het zo dat ik nog het id, product naam en prijs meestuur naar de Cart. Maar wil eigenlijk alleen nog maar het id meesturen, om dat de gebruiker daar weinig mee kan saboteren. Ik weet wel hoe ik de data enzo kan ophalen, maar vind het slordig om direct die api call functies van die api class direct in mijn Cart class te gebruiken.
Heeft iemand ideeen, hoe ik dit netjes kan doen?
Code (php)
Hecht niet teveel waarde aan deze specifieke code. Het gaat alleen even om het idee.
>> ... een project waarin ik een class heb gemaakt die api calls doet. Dit haalt producten op, maar kan ook producten/klanten etc opslaan.
Als dit allemaal wordt gedaan door één en dezelfde class, dan zit er veel te veel functionaliteit in die class en moet je eigenlijk e.e.a. opsplitsen.
Ozzie PHP op 08/12/2022 22:50:20:
Hecht niet teveel waarde aan deze specifieke code. Het gaat alleen even om het idee.
Dat idee overtreedt het Single-Responsibility Principle (SRP), de S in SOLID. Twee keer zelfs.
Als je de class Cart beschouwt als een container met references naar producten, dan is het vullen van die container een andere verantwoordelijkheid. Die verantwoordelijkheid hoort daarom buiten de class: de class Cart met dependency injection rechtstreeks afhankelijk maken van een API request handler is een onzalig idee.
Iets soortgelijks geldt voor de methode toonProducten(). Als je de class Cart nog steeds beschouwt als een class die verantwoordelijk is voor het bewaren van references naar producten, dan is het tonen van die producten een verantwoordelijkheid die buiten de class hoort.
Hetzelfde geldt in de vraag voor het berekenen van totalen: dat is niet de verantwoordelijkheid van de container, maar van een class die deze container gebruikt.
Maak daarom eens een lijstje van dingen die moeten gebeuren (de verantwoordelijkheden) en probeer elke verantwoordelijkheid vervolgens zo veel mogelijk in een aparte class onder te brengen.
Ideaal is het niet, maar topic starter vroeg hoe hij op een nette manier de API kan aanspreken in plaats van rechtstreeks aan te roepen in de class zelf. Over een wel of niet juiste interpretatie van OOP heb ik het niet gehad. Dat is een totaal andere discussie. toonProducten is in het voorbeeld inderdaad niet zaligmakend, want een (library) class hoort niks te tonen. Dat lijkt me eerder thuis te horen in een controller. Maar zoals ik zei, niet te veel waarde hechten aan de code. Het ging er even om hoe je het ene object op een nette manier kunt meegeven aan het andere.
Ozzie PHP op 09/12/2022 12:49:44:
Ideaal is het niet, maar topic starter vroeg hoe hij op een nette manier de API kan aanspreken in plaats van rechtstreeks aan te roepen in de class zelf.
En jouw oplossing is dan de API meegeven in de constructor?! Dat verplaatst het probleem, maar lost het niet op. Daarmee maak je zelfs de gehele class afhankelijk van de API. Ook wanneer je de API helemaal niet nodig hebt.
Je hebt hiermee de deur naar Dependency Hell op een kier gezet.
Welcome to hell.
```
Code (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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
class Cart {
protected $membership;
protected $addons = [];
protected $signupCosts = 10.00;
public function addAddon($addonId, $addonName, $addonPrice){
if(array_key_exists($addonId, $this->addons)){
unset($this->addons[$addonId]);
} else {
$this->addons[$addonId] = [
'name' => $addonName,
'price' => $addonPrice,
];
}
}
public function addMembership($membershipId, $membershipName, $membershipPrice){
$this->membership = [
'id' => $membershipId,
'name' => $membershipName,
'price' => $membershipPrice,
];
}
public function getTotalPrice(){
$totalPrice = 0;
$addonsTotalPrice = array_reduce($this->addons, function($total, $addon){
$price = number_format((float) $addon['price'], 2, '.', '');
$total += $price;
return $total;
}, 0);
$totalPrice += $addonsTotalPrice;
if(array_key_exists('price', $this->membership)){
$totalPrice += number_format((float) $this->membership['price'], 2, '.', '');
}
$totalPrice += number_format((float) $this->signupCosts, 2, '.', '');
return $totalPrice;
}
public function getCartItems(){
$cartItems = [];
$cartItems['membership'] = $this->membership;
$cartItems['addons'] = array_values($this->addons);
$cartItems['signupCosts'] = $this->signupCosts;
$cartItems['total'] = number_format((float) $this->getTotalPrice(), 2, '.', '');
return $cartItems;
}
}
?>
class Cart {
protected $membership;
protected $addons = [];
protected $signupCosts = 10.00;
public function addAddon($addonId, $addonName, $addonPrice){
if(array_key_exists($addonId, $this->addons)){
unset($this->addons[$addonId]);
} else {
$this->addons[$addonId] = [
'name' => $addonName,
'price' => $addonPrice,
];
}
}
public function addMembership($membershipId, $membershipName, $membershipPrice){
$this->membership = [
'id' => $membershipId,
'name' => $membershipName,
'price' => $membershipPrice,
];
}
public function getTotalPrice(){
$totalPrice = 0;
$addonsTotalPrice = array_reduce($this->addons, function($total, $addon){
$price = number_format((float) $addon['price'], 2, '.', '');
$total += $price;
return $total;
}, 0);
$totalPrice += $addonsTotalPrice;
if(array_key_exists('price', $this->membership)){
$totalPrice += number_format((float) $this->membership['price'], 2, '.', '');
}
$totalPrice += number_format((float) $this->signupCosts, 2, '.', '');
return $totalPrice;
}
public function getCartItems(){
$cartItems = [];
$cartItems['membership'] = $this->membership;
$cartItems['addons'] = array_values($this->addons);
$cartItems['signupCosts'] = $this->signupCosts;
$cartItems['total'] = number_format((float) $this->getTotalPrice(), 2, '.', '');
return $cartItems;
}
}
?>
```
Leuk, al die feedback, maar nog leuker als je zelf een voorbeeldje plaatst van hoe het volgens jou wel zou moeten. Ik gaf enkel een korte reactie op hoe je iets anders kunt doen. Ik ken heel zijn framework niet, evenmin als jij (neem ik aan). Wellicht werkt hij niet eens volgens OOP. Anyhow ... ik zou zeggen, geef een mooi voorbeeld van hoe het volgens jou zou moeten. Dat lijkt me een waardevolle toevoeging aan de discussie.
Wat ik nu zie is dat Cart niet zozeer een winkelwagen is, maar de rekening met alle specificaties, ofwel alle gegevens worden op 1 hoop gegooid. Het zou mooier zijn wanneer Cart alleen producten bevat. Een product verdient dus een eigen Product class:
Code (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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php
class Product
{
/** @var string productnaam */
protected string $name;
/** @var int prijs in eurocent */
protected int $price;
/**
* ctor
* @param string $name Productnaam
* @param int $price Prijs in eurocent
* @return void (implicit)
*/
function __construct(string $name, int $price)
{
$this->name = $name;
$this->price = $price;
}
/**
* getter
* @return Productnaam
*/
function name() : string
{
return $this->name;
}
/**
* getter
* @return Productprijs in eurocent
*/
function price() : int
{
return $this->price;
}
}
class ProductBuilder
{
static function build_product_by_id(int $id) : Product
{
$sql = 'SELECT name, price FROM product WHERE id = $1';
// $data = $db->query_params(sql, $id);
$data = [['name' => 'Voorbeeld', 'price' => 250]];
if (count($data) == 0) { trigger_error('Product not found'); }
$row = $data[0];
return new Product($row['name'], $row['price']);
}
}
class Cart
{
/** @var array $products Producten */
protected $products;
/**
* ctor
* @return void (implicit)
*/
function __construct()
{
$this->products = [];
}
/**
* Voeg product toe aan winkelwagen
*/
function add_product(Product $product)
{
$this->products[] = $product;
}
/**
* Voeg product toe aan winkelwagen via product ID
*/
function add_product_by_id(int $id)
{
$this->products[] = ProductBuilder::build_product_by_id($id);
}
/**
* Bereken totale prijs in eurocent
*/
function total_price() : int
{
/** @var int $total Totaalbedrag in eurocent */
$total = 0;
foreach ($this->products as $product) {
$total += $product->price();
}
return $total;
}
}
$cart = new Cart;
$cart->add_product_by_id(1);
$cart->add_product(new Product('Nog een voorbeeld', 300));
print 'Totaalbedrag: ' . $cart->total_price();
?>
class Product
{
/** @var string productnaam */
protected string $name;
/** @var int prijs in eurocent */
protected int $price;
/**
* ctor
* @param string $name Productnaam
* @param int $price Prijs in eurocent
* @return void (implicit)
*/
function __construct(string $name, int $price)
{
$this->name = $name;
$this->price = $price;
}
/**
* getter
* @return Productnaam
*/
function name() : string
{
return $this->name;
}
/**
* getter
* @return Productprijs in eurocent
*/
function price() : int
{
return $this->price;
}
}
class ProductBuilder
{
static function build_product_by_id(int $id) : Product
{
$sql = 'SELECT name, price FROM product WHERE id = $1';
// $data = $db->query_params(sql, $id);
$data = [['name' => 'Voorbeeld', 'price' => 250]];
if (count($data) == 0) { trigger_error('Product not found'); }
$row = $data[0];
return new Product($row['name'], $row['price']);
}
}
class Cart
{
/** @var array $products Producten */
protected $products;
/**
* ctor
* @return void (implicit)
*/
function __construct()
{
$this->products = [];
}
/**
* Voeg product toe aan winkelwagen
*/
function add_product(Product $product)
{
$this->products[] = $product;
}
/**
* Voeg product toe aan winkelwagen via product ID
*/
function add_product_by_id(int $id)
{
$this->products[] = ProductBuilder::build_product_by_id($id);
}
/**
* Bereken totale prijs in eurocent
*/
function total_price() : int
{
/** @var int $total Totaalbedrag in eurocent */
$total = 0;
foreach ($this->products as $product) {
$total += $product->price();
}
return $total;
}
}
$cart = new Cart;
$cart->add_product_by_id(1);
$cart->add_product(new Product('Nog een voorbeeld', 300));
print 'Totaalbedrag: ' . $cart->total_price();
?>
In deze trend is de 'signup costs' ook een product, die zou je standaard zo kunnen meenemen:
Belangrijk is om concepten uit elkaar te trekken. Ik weet niet precies hoe je API er uit ziet, maar normaal gesproken wordt onder een API verstaan een generieke manier om code aan te roepen, ongeacht hoe het is geïmplementeerd. Dan kom je op het gebied van interfaces. Met een kleine omweg kan je via JavaScript ook PHP aanroepen. In dat geval kan je vanuit die 'omweg' bovenstaande classes gebruiken.
Wil je Products nog verder uit elkaar trekken, omdat memberships, addons en signup costs verschillende dingen zijn, maar toch allemaal producten, dan kan je dat verder specificeren via inheritance. Maak de class Product eventueel een abstracte class, en maak voor elk soort product een eigen class, die bouwt op de Product class ('extenden'), met daarin de specifieke eigenschappen (methoden en variabelen). Hierover is op PHPHulp.nl een tutorial verschenen.
Inversion of Control Containers and the Dependency Injection pattern door Martin Fowler
Dependency Injection Bad Practices door Luís Soares
Dependency Injection Bad Practices door Luís Soares
Ward van der Put op 13/12/2022 11:01:58:
Inversion of Control Containers and the Dependency Injection pattern door Martin Fowler
Dependency Injection Bad Practices door Luís Soares
Dependency Injection Bad Practices door Luís Soares
Ik ben eerder ook in de dependency hell getrapt. Men en iedereen heeft het erover hoe je wel MVC zou moeten implementeren. Maar inmiddels weet ik beter, door schade en schande.
PHP zit in zijn geheel gewoon te losjes in elkaar waardoor het vrijwel ondoenlijk is om er echt goed in te programmeren. Dat ze in PHP 8 nog de booleaanse logica fundamenteel moesten herzien is mijn inziens een grote smet.
Ondertussen krijg je aan alle kanten verkeerd advies over raamwerken. Mijn ogen werden pas geopend na het lezen van dit artikel. MVC blijkt nooit bedoeld voor de situatie van PHP! Er blijkt ook uit dat het compleet onzinnig is om voor een enkele thread een database object te maken, en daarvan de pointer mee te geven aan elke class om zo in de dependency hell te belanden. Sommigen zoeken dan hun toevlucht tot de verguisde singleton, maar dat is vaak wel een praktische oplossing.
Nog praktischer zou zijn om het hele OOP-geneuzel in PHP met een enorme korrel zout te nemen. Zo is het nog beter om gewoon een globale static class te maken voor de DB, beter dan dat wordt het toch niet voor de klassieke situatie van een eigen thread per HTTP-request.
En laten we eerlijk zijn, zolang je in PHP dit kan doen zonder dat je een error krijgt:
is het dweilen met de kraan open. Een string kan zomaar gecast worden naar een bool, zonder dat duidelijk is waarvoor. Vervolgens heeft $t gewoon de waarde NULL, zoals elke niet bestaande variabele dat standaard heeft.
Nog los van de waslijst van dingen en dingetjes die aan PHP zijn op te merken (wat is er wel perfect?) merk ik dat het programmeren in een georganiseerde taal als Rust wel een stuk makkelijker gaat. Ja je moet alles uitspellen, maar daarmee betrap je jezelf eerder op foutjes, en er is meer controle en ondersteuning vanuit de ontwikkeltools. Programma's bevatten minder bugs.
Het gaat ook beter omdat OOP ook niet zaligmakend is, het is niet langer het antwoord op alles. Sterker, inheritance wordt niet eens ondersteund. In plaats daarvan ontwerp je code op basis van gedrag, waardoor je allerlei OOP-problemen voor bent.
Ad Fundum op 10/12/2022 15:58:36:
Het is een inkopper, maar het hele idee van object georiënteerd programmeren is dat je van objecten classes maakt.
Wat ik nu zie is dat Cart niet zozeer een winkelwagen is, maar de rekening met alle specificaties, ofwel alle gegevens worden op 1 hoop gegooid. Het zou mooier zijn wanneer Cart alleen producten bevat. Een product verdient dus een eigen Product class:
In deze trend is de 'signup costs' ook een product, die zou je standaard zo kunnen meenemen:
Belangrijk is om concepten uit elkaar te trekken. Ik weet niet precies hoe je API er uit ziet, maar normaal gesproken wordt onder een API verstaan een generieke manier om code aan te roepen, ongeacht hoe het is geïmplementeerd. Dan kom je op het gebied van interfaces. Met een kleine omweg kan je via JavaScript ook PHP aanroepen. In dat geval kan je vanuit die 'omweg' bovenstaande classes gebruiken.
Wil je Products nog verder uit elkaar trekken, omdat memberships, addons en signup costs verschillende dingen zijn, maar toch allemaal producten, dan kan je dat verder specificeren via inheritance. Maak de class Product eventueel een abstracte class, en maak voor elk soort product een eigen class, die bouwt op de Product class ('extenden'), met daarin de specifieke eigenschappen (methoden en variabelen). Hierover is op PHPHulp.nl een tutorial verschenen.
Wat ik nu zie is dat Cart niet zozeer een winkelwagen is, maar de rekening met alle specificaties, ofwel alle gegevens worden op 1 hoop gegooid. Het zou mooier zijn wanneer Cart alleen producten bevat. Een product verdient dus een eigen Product class:
Code (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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php
class Product
{
/** @var string productnaam */
protected string $name;
/** @var int prijs in eurocent */
protected int $price;
/**
* ctor
* @param string $name Productnaam
* @param int $price Prijs in eurocent
* @return void (implicit)
*/
function __construct(string $name, int $price)
{
$this->name = $name;
$this->price = $price;
}
/**
* getter
* @return Productnaam
*/
function name() : string
{
return $this->name;
}
/**
* getter
* @return Productprijs in eurocent
*/
function price() : int
{
return $this->price;
}
}
class ProductBuilder
{
static function build_product_by_id(int $id) : Product
{
$sql = 'SELECT name, price FROM product WHERE id = $1';
// $data = $db->query_params(sql, $id);
$data = [['name' => 'Voorbeeld', 'price' => 250]];
if (count($data) == 0) { trigger_error('Product not found'); }
$row = $data[0];
return new Product($row['name'], $row['price']);
}
}
class Cart
{
/** @var array $products Producten */
protected $products;
/**
* ctor
* @return void (implicit)
*/
function __construct()
{
$this->products = [];
}
/**
* Voeg product toe aan winkelwagen
*/
function add_product(Product $product)
{
$this->products[] = $product;
}
/**
* Voeg product toe aan winkelwagen via product ID
*/
function add_product_by_id(int $id)
{
$this->products[] = ProductBuilder::build_product_by_id($id);
}
/**
* Bereken totale prijs in eurocent
*/
function total_price() : int
{
/** @var int $total Totaalbedrag in eurocent */
$total = 0;
foreach ($this->products as $product) {
$total += $product->price();
}
return $total;
}
}
$cart = new Cart;
$cart->add_product_by_id(1);
$cart->add_product(new Product('Nog een voorbeeld', 300));
print 'Totaalbedrag: ' . $cart->total_price();
?>
class Product
{
/** @var string productnaam */
protected string $name;
/** @var int prijs in eurocent */
protected int $price;
/**
* ctor
* @param string $name Productnaam
* @param int $price Prijs in eurocent
* @return void (implicit)
*/
function __construct(string $name, int $price)
{
$this->name = $name;
$this->price = $price;
}
/**
* getter
* @return Productnaam
*/
function name() : string
{
return $this->name;
}
/**
* getter
* @return Productprijs in eurocent
*/
function price() : int
{
return $this->price;
}
}
class ProductBuilder
{
static function build_product_by_id(int $id) : Product
{
$sql = 'SELECT name, price FROM product WHERE id = $1';
// $data = $db->query_params(sql, $id);
$data = [['name' => 'Voorbeeld', 'price' => 250]];
if (count($data) == 0) { trigger_error('Product not found'); }
$row = $data[0];
return new Product($row['name'], $row['price']);
}
}
class Cart
{
/** @var array $products Producten */
protected $products;
/**
* ctor
* @return void (implicit)
*/
function __construct()
{
$this->products = [];
}
/**
* Voeg product toe aan winkelwagen
*/
function add_product(Product $product)
{
$this->products[] = $product;
}
/**
* Voeg product toe aan winkelwagen via product ID
*/
function add_product_by_id(int $id)
{
$this->products[] = ProductBuilder::build_product_by_id($id);
}
/**
* Bereken totale prijs in eurocent
*/
function total_price() : int
{
/** @var int $total Totaalbedrag in eurocent */
$total = 0;
foreach ($this->products as $product) {
$total += $product->price();
}
return $total;
}
}
$cart = new Cart;
$cart->add_product_by_id(1);
$cart->add_product(new Product('Nog een voorbeeld', 300));
print 'Totaalbedrag: ' . $cart->total_price();
?>
In deze trend is de 'signup costs' ook een product, die zou je standaard zo kunnen meenemen:
Belangrijk is om concepten uit elkaar te trekken. Ik weet niet precies hoe je API er uit ziet, maar normaal gesproken wordt onder een API verstaan een generieke manier om code aan te roepen, ongeacht hoe het is geïmplementeerd. Dan kom je op het gebied van interfaces. Met een kleine omweg kan je via JavaScript ook PHP aanroepen. In dat geval kan je vanuit die 'omweg' bovenstaande classes gebruiken.
Wil je Products nog verder uit elkaar trekken, omdat memberships, addons en signup costs verschillende dingen zijn, maar toch allemaal producten, dan kan je dat verder specificeren via inheritance. Maak de class Product eventueel een abstracte class, en maak voor elk soort product een eigen class, die bouwt op de Product class ('extenden'), met daarin de specifieke eigenschappen (methoden en variabelen). Hierover is op PHPHulp.nl een tutorial verschenen.
Thanks dit is precies wat ik zocht. Dit scheidt erg goed de functionaliteit. Het enige wat in mijn geval anders is, is dat ik geen database gebruik, maar een api, maar dan kan ik die sql query gemakkelijk vervangen door een soortgelijke api call. Is het overigens ook handig om per product property een class te maken, of is dat iets te veel van het goede? Vind het zelf niet nodig, maar hoorde dat van iemand.
Jorn Reed op 21/12/2022 09:20:21:
Is het overigens ook handig om per product property een class te maken, of is dat iets te veel van het goede? Vind het zelf niet nodig, maar hoorde dat van iemand.
Ja, dat noemen we value objects (of ook wel data objects):
https://martinfowler.com/bliki/ValueObject.html
https://en.wikipedia.org/wiki/Value_object
Ik gebruik ze zelf zodra je twee of meer dingen hebt die bij elkaar horen. Martin Fowler noemt als voorbeeld de x en y in coördinaten.
Maar wat is het voordeel om bijvoorbeeld voor een product, een aparte class te maken voor bijvoorbeeld: product naam, prijs en beschrijving?
Een persoonsnaam is een ander, misschien wat ingewikkelder voorbeeld. Ik raak al meer dan 50 jaar de "van der" in mijn achternaam kwijt. En dat bij hele grote bedrijven waarvan je wel meer verwacht: dit jaar was het raak/mis bij ING en KPN.
Het probleem is onder andere dat een systeem dat alleen een voornaam plus achternaam verwerkt moet samenwerken met een systeem dat de drie-eenheid voornaam, tussenvoegsels en achternaam gebruikt. En dan heb je ook nog Amerikaanse systemen waarin de middle name (de F. in John F. Kennedy) wordt misbruikt voor Nederlandse tussenvoegsels.
Dit soort problemen kun je voorkomen (en oplossen) door de verantwoordelijkheid bij één class Name te leggen. Andere classes hoeven dan geen onderscheid meer te maken tussen allerlei soort namen en onderdelen van namen: ze gebruiken alleen de class Name.
Ah oke klinkt logisch, in mijn geval heb ik voor een product het bedrag in euros genoeg aan. Dus dat hoeft niet echt complex te zijn, maar ik snap inderdaad wat je bedoelt.
Bijv. je moet een intern productnummer (sku) ophalen. Dat kan gewoon als volgt:
Dat kan ook vaak met een productnaam. Wat Ward als voorbeeld geeft van iemands naam kan in een aparte class, mits er een bepaalde vorm van complexiteit met die functie samenhangt. En dan hangt het ook nog af van de mate van complexiteit. Bijv. stel je wilt dat ieder woord met een hoofdletter begint. Als dat het enige is, dan zou ik daar geen aparte class voor maken.
Ander voorbeeld: stel een user heeft een username, dat is dan gewoon een string, net als hierboven in het voorbeeld van de sku. Daar hoef je geen aparte class voor te maken.
Het is een beetje je eigen keuze. OOP geeft richtlijnen, maar het zijn geen verplichtingen. Je moet vooral voor jezelf kunnen verantwoorden waarom je iets op een bepaalde manier doet, en daar moet je consequent (belangrijk) in zijn.
Een website voor de kruidenier op de hoek, richt zich op de lokale markt en kan daarom om andere vereisten en benodigdheden vragen, dan een commercieel framework dat zich richt op (inter)nationale transacties met verschillende valuta.
Werk je in je eentje of in een team aan de website/webshop? Werk je er alleen aan, dan bepaal jij de spelregels. Werk je met meerdere mensen eraan, dan zul je spelregels moeten opleggen waar iedereen zich aan houdt.
Het gebruiken van een class voor iedere property kan ook zo'n spelregel (standaard) zijn. Niet omdat het per se altijd noodzakelijk is, maar omdat het consistent is en iedereen dan weet dat een property wordt opgehaald via een class.
Kortom, doe wat in jouw situatie nuttig is. Zorg er vooral voor dat je je keuzes kunt verantwoorden en dat je consistent bent in je werkwijze.
Hoop dat je iets aan deze extra toevoeging hebt ;-)
Yes! Bedankt voor de uitleg, het is nu wel een stuk duidelijker en het hangt dus gewoon echt af van de situatie. Voor nu zijn mijn vragen door jullie heel netjes beantwoord en neem ik de adviezen in de toekomst zeker mee. Want ik ga echt wel een keer in een situatie belanden, waar een property een heel object moet zijn met extra functionaliteit.
Dat kan over alles gaan. Bijvoorbeeld ter inspiratie: dit topic. Wat is je return waarde?
Maar zo zijn er heel veel dingen waar je over kunt nadenken. En het is goed om dat 1 keer heel goed te doen, zodat je daarop volgende code consistent is.
Een ander voorbeeld.
versus
Allebei helemaal prima, maar merk op dat de posities van de accolades en de dubbele punt anders is. Net als array|null versus ?array.
Er staat in beide gevallen precies hetzelfde, maar de code is anders 'opgemaakt'. Ook daar zijn standaarden voor, maar het voornaamste is dat je iets kiest, en dat consistent doorvoert. Als je dat doet dan heb je voor jezelf (of voor je team) altijd prettig leesbare code.
Succes!
Maar omdat het vooral een ontwerpkeuze is, zal ik ook uitleggen waarom ik dat doe.
Stel, je doet inderdaad zoiets:
Code (php)
Als een business case (of de baas) vereist dat de SKU's worden aangepast, dan moet je overal die strings en stringfuncties aanpassen. Had je nou voor SKU's echter een aparte class StockKeepingUnit gebruikt, dan hoeft dat niet: je past alleen die ene class StockKeepingUnit aan en de rest van de code werkt als vanouds:
Code (php)
Goede code is easy to change!
Een value object zelf is zelden heel complex. Ik maak ze gemakshalve meestal stringable, zodat je ze kunt echoën in de juiste syntaxis of je ze in een SQL-query kunt steken, bij een SKU bijvoorbeeld een "WHERE product_id = " . $sku.
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
declare(strict_types=1);
class StockKeepingUnit implements \Stringable
{
private readonly string $value;
public function __construct(mixed $value)
{
// ... Hier kun je eventueel nog allerlei validaties kwijt. ...
if (empty($value)) throw new \ValueError();
$this->value = (string) $value;
}
public function __toString(): string
{
return $this->value;
}
}
?>
declare(strict_types=1);
class StockKeepingUnit implements \Stringable
{
private readonly string $value;
public function __construct(mixed $value)
{
// ... Hier kun je eventueel nog allerlei validaties kwijt. ...
if (empty($value)) throw new \ValueError();
$this->value = (string) $value;
}
public function __toString(): string
{
return $this->value;
}
}
?>
Ward van der Put op 21/12/2022 17:58:47:
Voor codes en nummers die aan bepaalde eisen moeten voldoen (syntaxis, opmaak, lengte, enzovoort), zou ik dus wél een value object gebruiken. Denk bijvoorbeeld aan EAN-nummers, BSN-nummers en IBAN-nummers.
Daar kan ik me inderdaad wat bij voorstellen. Daar zit een zekere mate van complexiteit in, en je kunt het op meerdere plekken nodig hebben. In zo'n geval is dat zeker een goed idee.
Ward van der Put op 21/12/2022 11:17:37:
Ja, dat noemen we value objects (of ook wel data objects):
https://martinfowler.com/bliki/ValueObject.html
https://en.wikipedia.org/wiki/Value_object
Ik gebruik ze zelf zodra je twee of meer dingen hebt die bij elkaar horen. Martin Fowler noemt als voorbeeld de x en y in coördinaten.
Jorn Reed op 21/12/2022 09:20:21:
Is het overigens ook handig om per product property een class te maken, of is dat iets te veel van het goede? Vind het zelf niet nodig, maar hoorde dat van iemand.
Ja, dat noemen we value objects (of ook wel data objects):
https://martinfowler.com/bliki/ValueObject.html
https://en.wikipedia.org/wiki/Value_object
Ik gebruik ze zelf zodra je twee of meer dingen hebt die bij elkaar horen. Martin Fowler noemt als voorbeeld de x en y in coördinaten.
Dit is een klassiek voorbeeld van hoe OOP kan worden ingezet. Maar dat is niet de meerwaarde van OOP. Want waarom zou je code schrijven die alleen maar data vast houdt? Dat geeft alleen maar overhead.
In plaats daarvan kan je ook dezelfde functies schrijven waar geen geïnstantieerd object omheen zit dat extra geheugen nodig heeft, dat geeft efficiëntere (snellere) code.
Nou is het wel zo dat PHP vanwege de weak typing praktisch geen enkele ondersteuning biedt aan eigen datatypen zoals dat bij andere programmeertalen wel het geval is. Je kunt in PHP bijvoorbeeld geen enum definieren waarin je meerdere scalars of andere objecten combineert, zoals met structs in C. Vanwege dit gebrek zou je kunnen overgaan om voor elke coördinaat een apart object te instantiëren. Maar erg efficiënt is dat natuurlijk niet.
Met value objects word je als programmeur bij gebrek aan beter eigenlijk de totaal verkeerde kant op gestuurd.
Nou hoef je dat niet van mij aan te nemen, ik heb mijn bias. Maar ik ben niet de enige die dat zo ziet, er is sinds ruim anderhalf jaar een RFC voor structs. Het enige nadeel is dat je nog niet kunt weten hoe lang je er op moet gaan wachten, als het al in PHP geïmplementeerd gaat worden. (status van implementatie is "To Be Determined") ...