Wat is SOLID? SOLID is een acroniem dat gebruikt wordt in verband met het programmeren van applicaties. De afkorting staat voor de vijf belangrijkste principes die in object-georiënteerd programmeren gebruikt worden. Als al deze principes correct gebruikt wordenis het waarschijnlijker dat een programmeur een systeem zal maken dat duurzamer en gemakkelijk te onderhouden is. De SOLID-principes maken deel uit van een meer globale strategie van agile ontwikkeling en adaptief programmeren.
S - Single Responsibility Principle
Elk component heeft één, en alleen een verantwoordelijkheid, denk bij component aan bijvoorbeeld een class of object. Denk bijvoorbeeld aan een een Zwitsers zakmes. Zo heb je vaak een mes, fles opener, vijl, schaar en vele andere componenten. Erg handig! Echter als het op software aan komt overtreed een Zwitsers zakmes wel het Single Responsibility Principle. Dit omdat je er erg veel mee kan. Neem nu bijvoorbeeld een aardappelschilmesje. Dit kan alleen gebruikt worden om te snijden. Je kan het niet gebruiken om te vijlen.
High cohesion
Cohesion refereert naar de samenhang tussen de verschillende verantwoordelijkheden binnen een object of class. Je wilt dat de samenhang binnen de class altijd zo hoog mogelijk is. Zoals in onderstaand voorbeeld 1 is te zien zijn de calculateArea
en calculatePerimeter
methodes aan elkaar gerelateerd aangezien ze beide met de afmetingen te maken hebben. De draw
en rotate
zijn ook aan elkaar gerelateerd aangezien ze beide te maken hebben met het tonen van het vierkant. Als je echter naar de calculatePerimeter
en draw
methodes kijkt zijn deze niet aan elkaar gerelateerd. Als we naar voorbeeld 2 kijken hebben we de class opgesplits in twee verschillende classes. Deze twee losse classes hebben allebei high cohesion.
Voorbeeld 1
class Square {
public $side = 5;
public function calculateArea() {
return $this->side * $this->side;
}
public function calculatePerimeter() {
return $this->side * 4;
}
public function draw() {
if (highResolutionMonitor) {
// Render a high resolution image of the square
} else {
// Render a normal image of the square
}
}
public function rotate($degree) {
// Rotate the image of the square
}
}
Voorbeeld 2
class Square {
public $side = 5;
public function calculateArea() {
return $this->side * $this->side;
}
public function calculatePerimeter() {
return $this->side * 4;
}
}
class SquareUI {
public function draw() {
if (highResolutionMonitor) {
// Render a high resolution image of the square
} else {
// Render a normal image of the square
}
}
public function rotate($degree) {
// Rotate the image of the square
}
}
Loose coupling
Een loosely coupled systeem is een systeem waarin elk component weinig of geen kennis heeft van de werking of definities van andere componenten. In een loosely coupled systeem kunnen componenten dus vervangen worden door alternatieve implementaties van dezelfde service, denk hierbij bijvoorbeeld aan een database.
In onderstaande class hebben we de save
methode. Zoals je kan zien zorgt deze methode voor heel wat low-level afhandeling van het opslaan in de database. In onderstaand voorbeeld gebruiken we een MySQL connectie. Als we nu in de toekomst bijvoorbeeld van PostgreSQL gebruik willen maken het grootste gedeelte van de code binnen de save
methode worden aangepast. Zoals je kunt zien is de Dog
class dus tightly coupled
met de database layer van de applicatie. Wat we het beste kunnen doen is het database gedeelte naar een nieuwe class verplaatsen. Door het verplaatsen van de database logica naar een eigen plek hebben we de tight coupling loose gemaakt. Als we nu van database veranderen hoeven we de Dog
class niet aan te passen. Hierdoor zouden andere classes ook gebruik kunnen maken van de database class.
Tight coupling
class Dog {
private $id;
private $name;
private $race;
public function save() {
try {
$conn = new PDO("mysql:host=$servername;dbname=myDB", $username, $password);
} catch(PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
$sql = "INSERT INTO dogs (name, race) VALUES ($this->name, $this->race)";
if ($conn->query($sql) !== TRUE) {
echo "Error: " . $sql . "<br>" . $conn->error;
}
}
public function getId() {
return $this->id;
}
public function getName() {
return $this->name;
}
...
...
...
}
Loose coupling
class Dog {
private $id;
private $name;
private $race;
public function save() {
$db = new DB();
$db->save(...);
}
...
...
...
}
class DB {
public $conn
public function __construct() {
try {
$this->conn = new PDO("mysql:host=$servername;dbname=myDB", $username, $password);
} catch(PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
}
public function save() {
$sql = "INSERT INTO ... ";
if ($conn->query($sql) !== TRUE) {
echo "Error: " . $sql . "<br>" . $conn->error;
}
}
}
O - Open-Closed Principle
Software componenten zouden gesloten moeten zijn voor aanpassing maar open voor uitbreiding. Een goed voorbeeld hiervan is de Wii console van Nintendo. Standaard krijg je de Wii zelf en een of meerdere controllers. Als uitbreiding hebben ze het bekende racestuurtje, het enige wat je hoeft te doen is je controller in het stuurtje stoppen en alles werkt. Plug and play. Stel je voor dat je de Wii open zou moeten maken en een jumper zou moeten verplaatsen om het stuurtje te gebruiken. Daar wordt niemand blij van. Dus toen de Wii uitgebracht werd was deze “Closed for modification” maar “Open for extension”. Als we nu terug gaan naar software bedoelen we met Closed for modification
dat nieuwe functionaliteit de bestaande code NIET hoeft aan te passen. Met Open for extension
bedoelen we dat de componenten uitbreidbaar moeten zijn om nieuwe functionaliteit toe te voegen.
In onderstaand voorbeeld 1
hebben we een InsurancePremiumDiscountCalculator
die op basis van de loyaliteit van een klant de korting berekend. In eerste instantie werd dit alleen voor een ziektekostenverzekering gedaan echter moet nu ook voor de auto verzekering de korting worden berekend. Daarna misschien ook nog voor de opstalverzekering. Als we dit nu uitbreiden moeten we veel code aanpassen om dit mogelijk te maken zoals te zien is in voorbeeld 2
. We kunnen echter ook de aanpassingen doen op een andere manier zoals te zien is in voorbeeld 3
. Het lijkt alsof we in eerste instantie misschien meer werk doen voor deze aanpassing. Door een interface aan te maken en de nieuwe classes daar aan te koppelen is het veel makkelijker om de code uit te breiden in de toekomst. Mocht er nu in de toekomst bijvoorbeeld een levensverzekering worden toegevoegd dan hoeft alleen de interface te worden geïmplementeerd. Hierdoor is de applicatie een stuk robuuster.
Voorbeeld 1
class InsurancePremiumDiscountCalculator {
public function calculateDiscount(CustomerProfile $customer) {
if ($customer->isLoyalCustomer()) {
return 20;
}
return 0;
}
}
class CustomerProfile {
public function isLoyalCustomer() {
return true; // or false
}
}
Voorbeeld 2
class InsurancePremiumDiscountCalculator {
public function calculateHealthInsuranceDiscount(HealthInsuranceCustomerProfile $customer) {
if ($customer->isLoyalCustomer()) {
return 20;
}
return 0;
}
public function calculateVehicleInsuranceDiscount(VehicleInsuranceCustomerProfile $customer) {
if ($customer->isLoyalCustomer()) {
return 20;
}
return 0;
}
public function calculateHomeInsuranceDiscount(HomeInsuranceCustomerProfile $customer) {
if ($customer->isLoyalCustomer()) {
return 20;
}
return 0;
}
}
class HealthInsuranceCustomerProfile {
public function isLoyalCustomer() {
return true; // or false
}
}
class VehicleInsuranceCustomerProfile {
public function isLoyalCustomer() {
return true; // or false
}
}
class HomeInsuranceCustomerProfile {
public function isLoyalCustomer() {
return true; // or false
}
}
Voorbeeld 3
public interface CustomerProfile {
public function isLoyalCustomer();
}
class InsurancePremiumDiscountCalculator {
public function calculateInsuranceDiscount(CustomerProfile $customer) {
if ($customer->isLoyalCustomer()) {
return 20;
}
return 0;
}
}
class HealthInsuranceCustomerProfile implements CustomerProfile {
public function isLoyalCustomer() {
return true; // or false
}
}
class VehicleInsuranceCustomerProfile implements CustomerProfile {
public function isLoyalCustomer() {
return true; // or false
}
}
class HomeInsuranceCustomerProfile implements CustomerProfile {
public function isLoyalCustomer() {
return true; // or false
}
}
L - Liskov Substitution principle
Het substitutieprincipe van Liskov, ook wel Liskov Substitution Principle genoemd, heeft betrekking tot het overerven. Het principe luidt als volgt: Objecten zouden vervangbaar moeten zijn met hun sub-types zonder de correctheid van het programma aan te passen. Om dit uit te leggen beginnen we met overerven wat een basis functionaliteit is van elke object-oriented programmeer taal. overerving wordt ook wel aangeduid als een “is een” relatie. Als voorbeeld: We hebben een class Auto
. Hatchback
breid de Auto
class uit. We kunnen ook zeggen: een Hatchback
is een Auto
. Nog een voorbeeld: We hebben een Vogel
class en Struisvogel
breid de Vogel
class uit, we kunnen dus zeggen een Struisvogel
is een Vogel
. Het klinkt misschien als een goed voorbeeld maar er is een onderliggend, verborgen probleem. Een struisvogel kan niet vliegen. In het onderstaande codevoorbeeld wordt het probleem duidelijk. De Struisvogel
class overschrijft de vlieg
methode door er een niet geïmplementeerde methode van te maken. Niet geïmplementeerde methodes duiden vrijwel altijd op een ontwerpfout. Het statement een Struisvogel
is een Vogel
is wellicht nog correct. Als we hier het Liskov principe hier toepassen, Objecten zouden vervangbaar moeten zijn met hun sub-types zonder de correctheid van het programma aan te passen, faalt het principe. Dit komt omdat de Struisvogel
class niet op elke plek gebruikt kan worden waar de Vogel
class gebruikt wordt. Zodra iemand de vlieg
methode aanroept gaat de applicatie stuk. Dus het Liskov substitutie principe heeft een striktere test dan de “is een” test. Dit is een gezegde wat vaak met het Liskov principe wordt geassocieerd: Als het er uit ziet als een eend en het kwaakt als een eend maar je hebt batterijen nodig dan heb je waarschijnlijk de verkeerde abstractie.
Een mooi voorbeeld hiervan is de RaceAuto
class. Je zou verwachten dat een RaceAuto
een Auto
is. Echter heeft een normale auto een “cabine”, een race auto daarintegen heeft een “cockpit”. Als je dit in code zou uitdrukken zou het er uit zien als voorbeeld 2. Om het probleem op te lossen maken we een generiekere voertuigen
class aan waarin we niet de breedte van de cabine willen weten maar van de binnenkant van het voertuig. Door het generieker te maken zouden we bijvoorbeeld ook een boot of vliegtuig toe kunnen voegen. In voorbeeld 3 is het Liskov princiepe toegepast.
Voorbeeld 1
class Vogel {
public function vlieg() {
// flapper met die vleugels
}
}
class Struisvogel extends Vogel {
public function vlieg() {
throw new Exception("dit kan ik niet");
}
}
Voorbeeld 2
class Car {
public function getCabinWidth() {
// cabin width
}
}
class RacingCar extends Car {
public function getCabinWidth() {
// unimplemented
}
public function getCockpitWidth() {
// return cockpit width
}
}
Voorbeeld 3
class Vehicle {
public function getInteriorWidth() {
// return interior width
}
}
class Car extends Vehicle {
public function getInteriorWidth() {
return $this->getCabinWidth();
}
public function getCabinWidth() {
// return cabin width
}
}
class RacingCar extends Vehicle {
public function getInteriorWidth() {
return $this->getCockpitWidth();
}
public function getCockpitWidth() {
// return cabin width
}
}
I - Interface Segregation Principle
Veel client-specifieke interfaces zijn beter dan één algemene interface. Ook zou een class niet afhankelijk moeten zijn van methodes die de class niet implementeerd. Neem bijvoorbeeld een multifunctionele print/fax/scan machine. Als je de functionaliteit om zou zetten naar code zou het er ongeveer zo uit zien als in voorbeeld 1
. In theorie ziet het er goed uit. Zodra we echter een iets minder multifunctionele machine hebben die alleen maar kan printen en scannen valt de interface al niet meer te implementeren. Als we dat wel zouden doen implementeren we methodes die we niet gebruiken. Dat gaat tegen het Interface Segregation principe in. Het is beter om meerdere kleinere interfaces te hebben welke gericht zijn op een specifieke taak. In vrijwel elke programmeertaal is het mogelijk om meerdere interfaces te implementeren. Het opsplitsen van de interfaces zou je kunnen doen zoals in voorbeeld 2
. Nadat we de interfaces hebben opgesplitst hebben we indirect ook gelijk het Liskov principe toegepast.
interface MultifunctionPrinter {
public function print();
public function getPrintSpoolDetails();
public function scan();
public function scanPhoto();
public function fax();
public function internetFax();
}
Voorbeeld 2
interface Print {
public function print();
public function getPrintSpoolDetails();
}
interface Scan {
public function scan();
public function scanPhoto();
}
interface Fax {
public function fax();
public function internetFax();
}
D - Dependency Inversion Principle
High level modules zouden niet afhankelijk moeten zijn van low level modules, beide zouden afhankelijk moeten zijn van abstracties. Abstracties zouden niet afhankelijk moeten zijn van details maar details zouden afhankelijk moeten zijn van de abstracties.
In Voorbeeld 1
is de MySQLConnection
de de low level module, de PasswordReminder
de high level module. Als we het Dependency Inversion principe volgen zien we dus dat Voorbeeld 1
zich niet aan de regels van dit principe houd omdat de PasswordReminder
class geforceerd wordt om gebruik te maken van de MySQLConnection
class. Zoals ook al eerder als voorbeeld is gebruikt, stel we willen van database wisselen dan zouden we ook de PasswordReminder
class moeten aanpassen. Dit is ook in strijd met het Open-Close principe. De PasswordReminder
class zou database onafhankelijk moeten werken. Omdat high level en low level modules afhankelijk moeten zijn van abstracties creeren we een interface. De interface heeft een connect
methode en de MySQLConnection
class kan deze interface vervolgens implementeren. Daarnaast kunnen we in plaats van direct de MySQLConnection
class te type-hinten ook de interface type-hinten. Hierdoor maakt het niet uit welke database engine er wordt gebruikt zolang de interface maar wordt gebruikt. In voorbeeld 2
is te zien hoe dit werkt.
Voorbeeld 1
class PasswordReminder {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}
Voorbeeld 2
interface DBConnectionInterface {
public function connect();
}
class MySQLConnection implements DBConnectionInterface {
public function connect() {
return "Database connection";
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
}