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.
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.
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:
- De Linku/Feedback package bevat 3 direct bruikbare implementaties:
ClosureFeedback
om eigen closures te gebruikenLoggerFeedback
om informatie naar een PSR-3 Logger te versturenChainedFeedback
om meerdereFeedback
implementaties tegelijkertijd te gebruiken
- De Linku/Feedback–SymfonyStyle package bevat een enkele implementatie dat de
SymfonyStyle
gebruikt om gegevens naar de terminal te sturen
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.