Di solito le cose iniziano in modo semplice…
Si sta progettando un’applicazione Kafka Streams che deve leggere i comandi e produrre l’evento aziendale corrispondente.
I modelli Avro che ci si aspetta di leggere hanno questo aspetto:
Mentre i messaggi di output che si devono produrre hanno l’aspetto seguente:
Si sa che si può sfruttare il plugin sbt-avrohugger per generare la classe Scala corrispondente per ogni schema Avro, in modo da potersi concentrare solo sulla progettazione della logica aziendale.
Poiché i messaggi in sé sono piuttosto semplici, si decide di creare una funzione monomorfa per mappare le proprietà tra ogni comando e l’evento corrispondente.
La topologia risultante ha l’aspetto seguente:
…Ma poi il dominio si amplia
Oggi sono emersi nuovi requisiti funzionali: la vostra applicazione deve ora gestire più tipi di risorse, ognuna con le sue proprietà uniche.
State pensando a come implementare questo requisito e rendere la vostra applicazione più resistente a ulteriori cambiamenti di comportamento?
Flussi multipli
Potreste suddividere sia i comandi che gli eventi in più argomenti, uno per tipo di asset, in modo da mantenere lo schema Avro corrispondente e garantirne la compatibilità.
Questa soluzione, tuttavia, comporta la replica di quasi tutta la stessa topologia più volte, quindi non è consigliata a meno che la logica aziendale non debba essere personalizzata per ogni tipo di risorsa.
Messaggi “Tutti o nessuno”
Avro non supporta l’ereditarietà tra i record, quindi qualsiasi strategia OOP per far sì che gli asset ereditino le proprietà da un antenato comune non è purtroppo praticabile.
Si potrebbe però creare un oggetto “Frankenstein” con tutte le proprietà di ogni singolo asset e compilare solo quelle necessarie per ogni tipo di asset.
Questa è sicuramente la soluzione peggiore dal punto di vista evolutivo e della manutenibilità.
Tipi di unione
Fortunatamente, Avro offre un’interessante funzionalità denominata union types: è possibile esprimere la diversità delle proprietà di ogni asset tramite un’unione di più payload, sempre basandosi su un unico messaggio come wrapper.
Inserire flussi polimorfi
Oggetti senza forma
Per far fronte a questo polimorfismo avanzato, si sfrutta la libreria shapeless, che introduce il tipo Coproduct, compagno perfetto del tipo Avro union.
Innanzitutto, si aggiorna la mappatura dei tipi personalizzati di sbt-avrohugger, in modo che generi un tratto sigillato aggiuntivo per ogni protocollo Avro contenente più record:
La classe di comando generata ha un aspetto simile a questo:
Aggiornamento della logica di business
Grazie al tratto Poly1 di Shapeless, è possibile scrivere la logica aziendale aggiornata in un’unica classe:
Le modifiche alla topologia sono minime, come ci si aspetterebbe:
Un tipo speciale di Serde
Ora il pezzo finale del puzzle, i Serde. Vi presentiamo la libreria avro4s, che porta Avro GenericRecords oltre i limiti.
Si crea una classe di tipo per estendere un vecchio Serde, fornendo un metodo nuovo di zecca:
Ora ogni classe generata ha il suo Serde, personalizzato sullo schema Avro corrispondente.
Mettere tutto insieme
Infine, il programma principale in cui si combinano tutti gli ingredienti:
Conclusioni
Quando più casi d’uso condividono (quasi) la stessa logica di business, è possibile creare un’applicazione di elaborazione dei flussi con un polimorfismo ad hoc e ridurre al minimo la duplicazione del codice, rendendo l’applicazione ancora più a prova di futuro.