Welkom op julesgraus.nl

Google Podcasts

30-12-2019

Afhankelijkheden tussen classes scheiden (IoC/Di)

Een kijkje in het effectief samenbrengen van code via inversion of control / dependency injection. Ik raad je aan om eerst even te kijken naar wat interfaces zijn en hoe ze werken. Daarover heb ik ook een blog artikel over geschreven.

Introductie

Als je programmeert kom je er vroeg of laat achter, dat je wel eens code herhaalt. Je hebt letterlijk dubbele code in je programmatuur zitten. Een van de eerste stappen die je leert, is om deze patronen te herkennen en ze dan in functies te stoppen. En als je bijvoorbeeld een reeks functies hebt die in dezelfde denkbeeldige categorie kunnen vallen, voeg je ze samen in classes.

Zo zijn er bijvoorbeeld veel applicaties die een UserService class hebben. Die bijvoorbeeld de functies Add, Delete, Activate, ChangePassword enzovoorts hebben. Een andere reeks functies, bijvoorbeeld voor het versturen van berichten via e-mail stop je dan bijvoorbeeld in de MailService class.

<?php
class UserService {
    public function add(string $firstname, string $lastname): User {
        //Code om gebruiker toe te voegen hier
    }

    public function delete(int $id): void {
        //Code om gebruiker toe te verwijderen hier
    }

    public function update(int $id, string $firstName = null, string $lastName = null): void {
        //Code om gebruiker toe te verwijderen hier
    }

    public function activate(int $id): bool {
        //Code om gebruiker te activeren hier
    }

    public function changePassword(int $id, string $oldPassword, string $newPassword, string $newPasswordConfirmation = ''): bool {
        //Code om wachtwoord van gebruiker te veranderen hier
    }
}
<?php
class MailService {
  public function sendMessage(string $to, string $from, string $subject, string $message): bool {
    //Code om een bericht te versturen hier
  }

  public function getLatestMessages(string $from): array {
    //Code om de laatste berichten op te halen hier.
  }
}

Het probleem

Op een gegeven moment kun je in dubio met jezelf raken bij dit groeperen. Bijvoorbeeld: Je hebt van gehoord dat je code het best groepeert op basis van waar verantwoordelijkheden liggen. Separation of concerns noemen we dat. De UserService handelt alles af met betrekking tot het beheren van gebruikers, en de MailService handelt alles af met betrekking tot het versturen/ontvangen van e-mail.

Stel nou dat ja als taak hebt dat wanneer een gebruiker wordt aangemaakt via de UserService, een e-mail wordt verstuurd naar de gebruiker om hem te verwelkomen. Hoe los je dit nou eigenlijk op? Want de verantwoordelijkheden van de twee classes overlappen elkaar hier!

Je kunt het oplossen door in de UserService constructor een “new instance” te maken van de MailService. En vervolgens gebruik je de mailService in de activate methode.

public function __construct()
{
    $this->mailService = new MailService();
}
public function activate(int $id): bool {
    //Code om gebruiker op te halen en te activeren hier.
    $this->mailService->sendMessage('admin@example.org', 'nieuwegebruiker@example.org', 'Welkom', 'Bedankt voor je registratie');
}

Door de tijd heen gebruik je de MailService op een exact dezelfde manier in andere classes. Een jaar later blijkt het niet meer zo nodig te zijn om mails te versturen omdat de klant heeft besloten dat de berichten via instant messaging verstuurd moeten worden. Overal waar je de MailService gebruikt, moet je nu aanpassingen maken. Want je gebruikt voor het versturen van instant message berichten nu de InstanceMessaging class. Deze heeft ook dezelfde sendMessage methode als de MailService class, maar verstuurt berichten niet via de mail maar via XMPP. XMPP is een protocol voor instant messaging.

Je verandert vrolijk alle new MailService codes naar new InstanceMessaging.

public function __construct()
{
    $this->mailService = new InstantMessaging();
}

Als je op dit moment afvraagt hoe dit makkelijker kan, dan zit je op een perfect punt om te gaan kijken naar Inversion of control / Dependency Injection.

De oplossing

Laten we eerst kijken naar het eerste probleem wat we hebben met onze code. Namelijk het feit dat we op verschillende plekken de MailService moesten vervangen door de nieuwe InstanceMessaging class. Dit probleem los je stap voor stap bijvoorbeeld als volgt op:

Stap 1: Eerst maak je een interface waarin je beschrijft dat beide classes een sendMessage methode hebben die op dezelfde manier werkt. Je kunt deze bijvoorbeeld MessagingInterface noemen.

<?php
interface MessagingInterface {
    public function sendMessage(string $to, string $from, string $subject, string $message): bool;
}

Stap 2: Je zorgt dat in ieder geval de InstanceMessaging class deze interface implementeert.

<?php
class InstantMessaging implements MessagingInterface{
   //Methodes en properties hier. En in ieder geval de sendMessage methode. Die staat immers in je interface.
}
?>

Stap 3: Vervolgens zorg je ervoor dat je via de constructor of een setter op de UserService class een implementatie van de interface kan meegeven aan de UserService. De parameter die je daarvoor gebruikt kun je type hinten. Gebruik daarvoor de MessagingInterface. Werk variabele namen bij zodat de namen duidelijk zijn. Je class is nu enkel en alleen afhankelijk van de methodes die in de interface staan, en niet meer van een specifieke implementatie zoals bijvoorbeeld de Mailservice. Je kunt als je in de toekomst weer iets anders hebt zoals de nieuwe instantMessaging class, ook andere implementaties meegeven. Eventueel kun je deze stap herhalen op andere classes zodat ook deze de interface gebruiken.

public function __construct(MessagingInterface $messenger)
{
    $this->messenger = $messenger;
}

Stap 4: Overal waar nieuwe class instances worden gemaakt, die de MessagingInterface in de constructor ontvangen, geef je deze mee. Als je ooit nog een stapje verder gaat, ga je service containers en andere technieken gebruiken om dit automatisch te doen.

$messengerImplementation = new InstantMessaging();
$userService = new UserService($messengerImplementation);
$userService->activate(42);

Conclusie

Je UserService class is nu niet meer afhankelijk van de MailService of InstanceMessaging class dankzij de interface. Wel van de interface. Dat is niet echt een probleem omdat je zelf je implementatie kan kiezen. Het belangrijkste is dat je de controle hebt omgedraaid. De UserService instantieert nu niet zelf meer de implementatie die hij nodig heeft, maar krijgt de instance van buitenaf. De controle is dus omgedraaid van binnen (in de UserService class) naar buiten (buiten de UserService class). Dit wordt bedoeld met inversion of control. De dependency (nu een implementatie van de interface) wordt van buitenaf via de constructor of setter geïnjecteerd. Dit wordt dependency injection genoemd. En hiermee heb je de verantwoordelijkheden (mails versturen en werken met gebruikers) van elkaar gescheiden.