Waarom bijdragen aan open source de moeite waard is

Code schrijven voor de open source-gemeenschap is goed voor je code, je eigen ontwikkeling, de gemeenschap en het bedrijf waar je werkt. Waarom? Ik geef je de voordelen én een voorbeeld uit de praktijk.

Timo
Geschreven door Timo
Coding Timo open source
Coding Timo open source

Linku gaat altijd voor technieken die passen bij het vraagstuk en de beoogde oplossing. Daarbij kiezen we vaak voor flexibele platformen, bijvoorbeeld Symfony. Dit is een PHP framework dat we voornamelijk gebruiken bij de ontwikkeling van de backend API’s van onze maatwerkapplicaties. Symfony is open source.

Voordelen open source

  • Meer dan één ontwikkelaar.

    Symfony heeft een grote gemeenschap aan bedrijven en developers opgebouwd die de beschikbare code gebruiken en verbeteren.

  • Keuzevrijheid voor bedrijven.

    Projecten zijn over te dragen aan andere developers. Bedrijven zitten daarmee niet aan één ontwikkelaar of bureau vast.

  • Betere code schrijven.

    Door je code te delen met andere ontwikkelaars stel je je bloot aan kritiek. Je ontvangt feedback van veel andere developers en leert daardoor een betere code te schrijven, deze goed te documenteren en gereed te maken voor hergebruik.

  • Nieuwe code ontdekken.

    Door aan open source-projecten mee te werken zie je meer van de wereld dan wanneer je je beperkt tot je eigen code. Je kunt leren van andere ontwikkelaars.

  • Gunstig voor je werk.

    Bureaus die ontwikkelaars de ruimte geven om bij te dragen aan de open source-gemeenschap scoren punten. Op de lange termijn kan het ook z’n effect hebben op nieuw talent die graag onderdeel wil zijn van zo’n bureau.

Betere code schrijven is het resultaat van dagelijkse gewoontes, niet van eenmalige veranderingen.

Timo
Timo Bakx
Backend developer bij Linku

Mijn bijdrage aan de PHP-gemeenschap

Ik lever al jaren een bijdrage aan diverse PHP-projecten buiten werktijd om. In die tijd heb ik meer geleerd dan in de acht jaar alleen in werktijd ontwikkelen. Mijn laatste bijdrage geeft antwoord op de vraag: “Hoe kunnen we informatie aan de CLI geven vanuit een service, zonder dat die service gebonden wordt aan de CLI?”

CLI output in services

In de projecten die ik voor Linku maak, werk ik vaak met koppelingen naar API’s van externe partijen.  Veelal hebben deze koppelingen een periodieke synchronisatie van gegevens nodig. Hiervoor schrijf ik normaal gesproken Symfony Commands die handmatig of via een cronjob uitgevoerd kunnen worden. Binnen deze commands stuur ik informatie over de voortgang van de synchronisatie terug naar de terminal, zodat ik kan zien dat er iets gebeurt.

Om snel te testen of het geheel werkt, zet ik eerst alle functionele code in die ene Symfony’s Command klasse. Omdat dit niet heel erg SOLID is, refactor ik daarna de code in losse services en klassen. Uiteindelijk eindig ik vaak met een Synchronizer service die een Repository gebruikt om gegevens lokaal op te slaan en een Client gebruikt om gegevens extern uit te lezen (of andersom, gelang de vereisten van het project).

[php]
final class Synchronizer
{
    /* ... */

    public function synchronize(): void
    {
        $data = $this->client->loadProducts();
        $products = [];

        foreach ($data as $row) {
            try {
                $product = $this->repository->getByExternalId($row['id']);
            } catch (ProductNotFound $exception) {
                $product = new Product();
                $product->setExternalId($row['id']);
            }

            $product->setTitle($row['title']);
            $product->setPrice((float)$row['price']);

            $products[] = $product;
        }

        $this->repository->store(...$products);
    }
}
[/php]

En nu komen we tot het probleem. Hoe zorgen we ervoor dat deze Synchronizer aan de terminal kan laten weten wat er gebeurt (en hoe ver deze is met het synchroniseren) zonder dat die service zo afhankelijk wordt van de terminal dat deze niet meer bruikbaar is vanuit andere delen van de applicatie (zoals controllers, event listeners, etc.)?

Eerste poging, callbacks

In de eerste instantie had ik closures toegevoegd als parameters van de synchronize functie in de Synchronizer. Hierdoor werd de aanroep van de functie echter erg groot. Daarnaast was het bij het aanroepen van de functie erg onduidelijk wat voor parameters deze closures precies nodig hadden.

[php]
final class Synchronizer
{
    /* ... */

    public function synchronize(\Closure $info, \Closure $startProcess, \Closure $advanceProcess, \Closure $stopProcess): void
    {
        $info('Starting synchronization');

        $data = $this->client->loadProducts();

        $info(\sprintf('Loaded %d products from remote source.', \count($data)));
        $products = [];

        $startProcess(\count($data));
        foreach ($data as $row) {
            $externalID = $row['id'];

            try {
                $product = $this->repository->getByExternalId($externalID);
            } catch (ProductNotFound $exception) {
                $product = new Product();
                $product->setExternalId($externalID);
            }

            $product->setTitle($row['title']);
            $product->setPrice((float)$row['price']);

            $products[] = $product;

            $advanceProcess();
        }
        $stopProcess();

        $this->repository->store(...$products);

        $info('Done synchronizing');
    }
}
[/php]

De Feedback klasse

Om de long parameter list code smell te voorkomen, heb ik de functionaliteit uit de closures verplaatst naar een klasse die in de Synchronizer gebruikt kan worden. In de eerste versie van deze Feedback klasse heb ik gebruik gemaakt van een SymfonyStyle object om de terugkoppeling net weer te geven.

Omdat deze $feedback variabele ook null kan zijn (bijvoorbeeld wanneer deze vanuit een controller of event listener wordt aangeroepen), staat de synchronize functie nu vol met null-checks:

[php]
use App\Feedback\Feedback;

final class Synchronizer
{
    /* ... */

    public function synchronize(?Feedback $feedback = null): void
    {
        if ($feedback) {
            $feedback->info('Starting synchronization');
        }

        $data = $this->client->loadProducts();

        if ($feedback) {
            $feedback->info(\sprintf('Loaded %d products from remote source.', \count($data)));
        }

        /* ... */
    }
}
[/php]

De fallback: NoFeedback

Om ervoor te zorgen dat er niet steeds gecheckt hoeft te worden of $feedback bestaat of niet, heb ik de NoFeedback klasse gemaakt als implementatie van het null object patroon. Deze klasse heeft dezelfde functies, maar zonder daadwerkelijke uitvoerende code.

Om de naamgeving schoon te houden, heb ik de Feedback klasse hernoemt naar SymfonyStyleFeedback en heb ik de Feedback naam hergebruikt als interface.

[php]
use App\Feedback\Feedback;
use App\Feedback\NoFeedback;

final class Synchronizer
{
    /* ... */

    public function synchronize(?Feedback $feedback = null): void
    {
        if (!$feedback) {
            $feedback = new NoFeedback();
        }

        $data = $this->client->loadProducts();

        $feedback->info(\sprintf('Loaded %d products from remote source.', \count($data)));

        /* ... */
    }
}
[/php]

Feedback verplaatsen van functie naar klasse

Uiteindelijk heb ik het doorgeven van de Feedback instantie verplaatst van de functie naar de klasse zelf. Dit zorgt voor minder gelijke code in iedere functie die de Feedback instantie wil gebruiken en maakt het makkelijker om deze te hergebruiken in andere functies binnen dezelfde klasse.

[php]
use App\Feedback\Feedback;
use App\Feedback\NoFeedback;

final class Synchronizer
{
    /**
     * @var Repository
     */
    private $repository;

    /**
     * @var Client
     */
    private $client;

    /**
     * @var Feedback
     */
    private $feedback;

    public function __construct(Repository $repository, Client $client)
    {
        $this->repository = $repository;
        $this->client = $client;
        $this->feedback = new NoFeedback();
    }

    public function setFeedback(Feedback $feedback): void
    {
        $this->feedback = $feedback;
    }

    /* ... */
}
[/php]

Eigen gebruik

Omdat ik voor deze oplossing hele positieve geluiden uit de community kreeg, heb ik aan Linku gevraagd of ik deze als Open Source package vrij zou mogen geven. Gelukkig waren zij het hier mee eens.

Om ervoor te zorgen dat Feedback ook buiten Symfony projecten te gebruiken is, heb ik de code over 2 packages verdeeld:

  1. De Linku/Feedback package bevat 3 direct bruikbare implementaties:
    • ClosureFeedback om eigen closures te gebruiken
    • LoggerFeedback om informatie naar een PSR-3 Logger te versturen
    • ChainedFeedback om meerdere Feedback implementaties tegelijkertijd te gebruiken
  2. De Linku/FeedbackSymfonyStyle package bevat een enkele implementatie dat de SymfonyStyle gebruikt om gegevens naar de terminal te sturen
Vragen?
Timo
Timo
Timo

Beginnen met open source?

Mijn voornaamste advies is: probeer Feedback uit. Als je vragen, uitbreidingen of verbeteringen hebt, open gerust een issue of pull request op Github of neem contact met me op.

Mail timo@linku.nl