Umgang mit generierten Klassen am Beispiel von AVRO
AVRO ist derzeit ein sehr populäres Serialisierungs-Framework. Dank Schemaregistry für Enterprise-Anforderungen bestens gerüstet. Es macht Sinn, die generierten Klassen im Code zu verwenden - allerdings ist der Code dann oft mit Zusatzcode zugepflastert, der mit der generierten Klasse interagiert, obwohl die Funktionalität eigentlich besser in die integrierte Klasse passen würde.
Ein kurzer Exkurs: Große Schemas verwalten
Ein Finding war, dass komplexe Schemas in einer *.avsc File wenig Sinn machen. Die Herausforderung ist, dass beim ersten Vorkommnis ein record
typ definiert wird und bei weiterer Verwendung nur mehr referenziert wird – das ist echt mühsam.
address.avsc
:
{
"type": "record",
"name": "Address",
"namespace": "com.example",
"fields": [
{
"name": "street",
"type": "string"
},
{
"name": "city",
"type": "string"
},
{
"name": "state",
"type": "string"
},
{
"name": "zipCode",
"type": "string"
}
]
}
person.avsc
:
{
"type": "record",
"name": "Person",
"namespace": "com.example",
"fields": [
{
"name": "firstName",
"type": "string"
},
{
"name": "lastName",
"type": "string"
},
{
"name": "mainResidence",
"type": "com.example.Address"
},
{
"name": "secondaryResidence",
"type": ["null", "com.example.Address"],
"default": null
}
]
}
Mit dem avro maven tool kann anschließend alles zusammengefügt werden (und ggf. mit <includes>
ergänzt werden – je nachdem, wo die Files liegen).
Verhalten mit dem Dekorator erweitern
Man hat jetzt eine schöne Klasse – die aber relativ „dumm“ ist und wohl am ehesten einem POJO / POCO entspricht. Natürlich hätte man jetzt die Möglichkeit, alles nochmal selbst zu schreiben und zu mappen – aber wenn man ab und zu pragmatisch sein will – welche Wege gibt es noch?
In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other instances of the same class … The decorator pattern provides a flexible alternative to subclassing for extending functionality (Wikipedia)
Z.B. könnte man eine Funktion „getPostalCodes“ implementieren. Dazu macht man einfach:
public class PersonDecorator {
private Person person;
public PersonDecorator(Person person) {
this.person = person;
}
public List<PostalCode> getPostalCodes() {
Das Single-responsibility principle bleibt dadurch erhalten, ohne dass man unnötige Code-Duplikation hat.
SRP wahren mit dem Proxy Pattern
The access to an object should be controlled. implements additional functionality to control the access to this subject.
Das Problem ist, dass oft Klassen, deren Aufgabe e.g. irgendwas mit Berechnung zu tun haben, komplizierte Stream Operationen auf Listen machen. Meine Meinung dazu ist klar: im Domain Objekt hat sowas nichts verloren. Jede Stream Operation macht etwas – also kann man sie auch benennen. Mit dem Proxy Pattern kann ich sie sogar elegant in einem Container verstecken:
import java.util.List;
import java.util.Iterator;
import java.util.stream.Collectors;
public class ListContainer<T> implements Iterable<T> {
private final List<T> list;
public ListContainer(List<T> list) {
this.list = list;
}
@Override
public Iterator<T> iterator() {
return list.iterator();
}
public List<T> filterLowerCaseNames() {
return list.stream()
.filter(name -> name.toString().length() > 0 && Character.isLowerCase(name.toString().charAt(0)))
.collect(Collectors.toList());
}
public static void main(String[] args) {
List<String> originalList = List.of("John", "Jane", "Alice", "seppi", "karl");
ListContainer<String> proxyList = new ListContainer<>(originalList);
System.out.println("Names starting with lowercase letters:");
for (String name : proxyList.filterLowerCaseNames()) {
System.out.println(name);
}
}
}
The power of .NET
In .NET funktionieren die ganzen Patterns natürlich auch. Was macht in .NET aber noch nutzen kann: die Roslyn Compiler Plattform. Da der C# Compiler in C# geschrieben ist, kann man sich in allen Stufen in den Prozess einklinken.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
string code = @"
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
// Step 1: Parse the code into a syntax tree
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
// Step 2: Find all property declarations
var properties = root.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Where(p => p.AccessorList != null);
// Step 3: Remove setters from properties and keep only getters
var updatedProperties = properties.Select(p =>
{
var getters = p.AccessorList.Accessors.Where(a => a.Kind() == SyntaxKind.GetAccessorDeclaration).ToList();
// Rebuild the property declaration with only the getter
return p.WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(getters)));
});
// Step 4: Replace the original properties with the updated ones
var updatedRoot = root.ReplaceNodes(properties, (original, _) => updatedProperties.First());
// Step 5: Generate the modified code
string modifiedCode = updatedRoot.NormalizeWhitespace().ToFullString();
Console.WriteLine("Original Code:");
Console.WriteLine(code);
Console.WriteLine("\nModified Code:");
Console.WriteLine(modifiedCode);
}
}