Das Monolith-Problem
Ein häufiges Muster: Eine Roboterzelle-Klasse mit 5000+ Zeilen Code.
class RobotCell
{
// IO-Logik
public void HandleInput() { }
public void SetOutput() { }
// Sequenzen
public void PickSequence() { }
public void PlaceSequence() { }
// Fehler
public void HandleError() { }
...
// >> 5000 Zeilen auf bei 50+ Methoden
}
Das ist unmöglich zu testen. Unmöglich zu debuggen. Ein Fehler im Pick-Sequenz kann sich durch E/A-Logik verlaufen. Eine neue Sequenz zusammenzukopieren? Du riskierst, etwas zu überschreiben.
Das Modul-Ansatz
Teile die Zelle in spezialisierte Module ein — jedes mit einer Aufgabe (Single Responsibility Principle).
Module-Typen
1. Data Module
Speichert Status und Daten. Beispiel:
public class JobData
{
public string JobID { get; set; }
public int PartCount { get; set; }
public DateTime StartTime { get; set; }
}
Einfach. Dumb. Das ist gut. Keine Logik, nur Daten.
2. IO Module
Abstrahiert alle Ein- und Ausgaben (Sensoren, Ventile, etc.).
public interface IIOModule
{
bool IsDoorClosed { get; }
bool IsGreiferBusy { get; }
void SetVacuum(bool active);
void SetRGBLight(Color color);
}
Mit einer Schnittstelle: Einfach austauschbar. Real Hardware? Eine Implementation. Simulator? Andere. Tests? Mock. All das ohne Zellcode zu ändern.
3. Handler Module
Führen Operationen durch. Beispiel:
public class PickHandler
{
private readonly IIOModule _io;
private readonly ILogger _logger;
public async Task Execute(PickData data)
{
_logger.Info("Pick starting...");
_io.SetVacuum(true);
await Task.Delay(500);
if (!_io.IsGreiferBusy)
{
throw new PickException("Greifer konnte nicht greifen");
}
_logger.Info("Pick completed");
}
}
Jeder Handler ist isoliert, testbar, wiederverwendbar.
4. Trigger Module
Reagiert auf Events (Sensoren, externe Commands).
public class SafetyTrigger
{
public event EventHandler DoorOpenedEvent;
public void Monitor(IIOModule io)
{
if (!io.IsDoorClosed)
{
DoorOpenedEvent?.Invoke(this, EventArgs.Empty);
}
}
}
Trigger sind Events, die Handler oder andere Module anstupsen können.
5. Orchestrator/Coordinator
The "big picture" Klasse, die Handler und Trigger zusammenbringt:
public class CellOrchestrator
{
private readonly PickHandler _pickHandler;
private readonly PlaceHandler _placeHandler;
private readonly SafetyTrigger _safetyTrigger;
public CellOrchestrator(PickHandler ph, PlaceHandler pl, SafetyTrigger st)
{
_pickHandler = ph;
_placeHandler = pl;
_safetyTrigger = st;
_safetyTrigger.DoorOpenedEvent += (s, e) => Stop();
}
public async Task RunSequence(JobData job)
{
await _pickHandler.Execute(job.PickData);
await _placeHandler.Execute(job.PlaceData);
}
}
Vorteile dieser Struktur
Testbarkeit
Mock eine IO-Schnittstelle, test einen Handler isoliert:
[Test]
public async Task PickHandler_WithoutGreifer_ThrowsException()
{
var mockIO = new Mock<IIOModule>();
mockIO.Setup(x => x.IsGreiferBusy).Returns(false);
var handler = new PickHandler(mockIO.Object);
Assert.ThrowsAsync<PickException>(() => handler.Execute(data));
}
Keine Real Hardware nötig. Kein 5000-Zeilen-Monolith zu debuggen.
Wiederverwendbarkeit
Ein PickHandler funktioniert in Zelle A und Zelle B. Musst nur die IO-Implementierung tauschen.
Skalierbarkeit
Neue Sequenz? Neuer Handler. Neue Safety-Regel? Neuer Trigger. Bestehendes Code bleibt unberührt.
Lesbarkeit
Der Orchestrator-Code sagt klar: "Pick → Place → done". Das ist wartbar und nachvollziehbar.
Dependency Injection
Mit DI (z.B. Microsoft.Extensions.DependencyInjection) wird die Zusammensetzung elegant:
var services = new ServiceCollection();
services.AddSingleton<IIOModule, RealIOModule>();
services.AddSingleton<PickHandler>();
services.AddSingleton<PlaceHandler>();
services.AddSingleton<SafetyTrigger>();
services.AddSingleton<CellOrchestrator>();
var container = services.BuildServiceProvider();
var cell = container.GetService<CellOrchestrator>();
await cell.RunSequence(jobData);
Best Practice: Interface Segregation
Nutze viele kleine Interfaces, nicht ein großes:
// FALSCH
public interface IEverything
{
bool IsDoor { get; } // Safety
void SetVacuum(); // Greifer
Color GetSensorColor(); // Vision
}
// RICHTIG
public interface ISafetyIO { bool IsDoor { get; } }
public interface IGriperIO { void SetVacuum(); }
public interface IVisionIO { Color GetSensorColor(); }
So kann ein Handler nur das abhängen, was er wirklich braucht.
Error Handling in Modulen
Jeder Handler sollte unterschiedliche Fehler transparent machen:
public class PickException : Exception { }
public class GreiferTimeoutException : PickException { }
public class GreiferBlockedException : PickException { }
Der Orchestrator kann dann spezifisch reagieren:
catch (GreiferBlockedException ex)
{
_logger.Warn("Greifer blocked, retrying...");
await Task.Delay(1000);
await _pickHandler.Execute(data);
}
Fazit
Modularer Code ist nicht kompliziert — er ist einfach richtig strukturiert.
Mit Handler, Trigger, IO-Layer und Orchestrator entstehen Systeme, die:
- Einfach zu verstehen sind
- Einfach zu testen sind
- Einfach zu warten sind
- Einfach zu erweitern sind
Das ist nicht extra Aufwand — es ist bessere Struktur von Anfang an.