C#Clean Code

Koordinaten – Alternative Datenflüsse

Die Trennung zwischen Integration und Operation ist auch bei Verzweigungen möglich. Dies soll anhand eines einfachen algorithmischen Problems mit abzweigenden Datenflüssen gezeigt werden.

Koordinaten
Jordan Madrid

Letztes Mal habe ich mich mit Alternativen Datenflüssen beschäftigt. Heute möchte ich das noch mal anhand eines Beispiels aufgreifen. Hierzu habe ich mir die Aufgabe NSTEPS von SPOJ heraus gesucht. Es geht dabei darum, den Wert für eine Koordinate zu ermitteln. Zusätzlich muss geprüft werden, ob es sich auch um eine gültige Koordinate handelt. Falls nicht muss die Meldung No Number ausgegeben werden. Folgendes Bild zeigt, für welche Koordinaten eine Zahl ausgegeben wird und welche dies ist. Der dahinterliegende Algorithmus ist nicht bekannt und muss daher bei der Implementierung ermittelt werden.

Image

Flow Design-Entwurf

TDD könnte bei Lösung des Algorithmus helfen. Aber wie hier diskutiert möchte ich erstmal einen Entwurf für die Lösung wagen, um so das Problem herunterzubrechen. Dies gelingt mir, auch ohne, dass ich den Algorithmus kenne. Hier mein Flow Design-Entwurf:

Flow Design fr NSTEPS

Auffällig im Diagramm ist, dass neben den Funktionseinheiten und deren Verbindung über Datenflüsse auch eine Klasse entworfen wird. Hierbei handelt es sich um die Klasse Coordinate, welche eine Koordinate aus den beiden Koordinaten X und Y beschreibt. An der Stelle vielleicht noch ersichtlich, was eine Koordinate ist, aber wenn die Daten komplexer werden, lohnt es sich diese auch näher im Entwurf zu beschreiben. Die Implementierung der Klasse aus dem Diagramm ist naheliegend:

public class Coordinate
{
  public int X { get; set; }
  public int Y { get; set; }
}

Implementierung der Integration

Bei allen Funktionseinheiten handelt es sich um Operationen. Lediglich die Methode Main() wird als Integration alle Teile zusammenstecken:

private static void Main()
{
  var numberOfCoordinates = ReadNumberOfCoordinates();
  ReadCoordinates(numberOfCoordinates, x => ValidateCoordinate(x, y => PrintNumber(CalculateValue(y)), PrintNoNumber));
}

Implementierung der Operationen

Zuerst wird die Anzahl der einzulesenden Koordinaten vom Benutzer abgefragt (ReadNumberOfCoordinates). Die dazugehörige Implementierung ist trivial:

private static int ReadNumberOfCoordinates()
{
  var line = Console.ReadLine();
  return int.Parse(line);
}

Als Nächstes werden die Koordinaten in der Funktionseinheit ReadCoordinates eingelesen und für jede eingelesene Koordinate ein Koordinate als Datenfluss ausgegeben (Realisiert durch den Aufruf der Action onCoordinate). Die Wiederholungen an Koordinaten wird im Flow Design durch das * gekennzeichnet, was soviel wie 0-n Koordinaten bedeutet.

  private static void ReadCoordinates(int numberOfTestCases, Action<Coordinate> onCoordinate)
  {
    for (var i = 0; i < numberOfTestCases; i++)
    {
      var line = Console.ReadLine();
      var match = Regex.Match(line, "(\\d+) (\\d+)");
      var x = int.Parse(match.Groups[1].Value);
      var y = int.Parse(match.Groups[2].Value);
      onCoordinate(new Coordinate { X = x, Y = y });
    }
  }

Die Validierung, ob es sich um eine gültige Koordinate handelt, ist schnell gefunden. Betrachtet man das Diagramm, so sieht man das es zwei Linien gibt. Für die erste Linie haben x und y immer denselben Wert, also y=x. Die andere ist auf der x-Achse um zwei verschoben, also y=x-2. Die Funktionseinheit ValidateCoordinate hat dabei zwei mögliche Datenflüsse. Wird eine gültige Koordinate identifiziert, so wird diese über den Datenfluss onValidCoordinate ausgegeben. Ansonsten wird die Kontrolle über den Datenfluss onInvalidCoordinate weitergegeben. Es handelt sich hier also um eine Alternative, die aber nicht besonders gekennzeichnet werden muss, da sie sich bereits aus den Namen der Datenflüsse herleiten lässt. Hier eine mögliche Implementierung:

  private static void ValidateCoordinate(Coordinate coordinate, Action<Coordinate> onValidCoordinate, Action onInvalidCoordinate)
  {
    if (coordinate.X == coordinate.Y || coordinate.X - 2 == coordinate.Y)
      onValidCoordinate(coordinate);
    else
      onInvalidCoordinate();
  }

Der zu Beginn vermutete komplexe Algorithmus, für die Berechnung des Werts einer gültigen Koordinate, lässt sich am Ende mathematisch einfach beschreiben n=x+y-(x mod 2). Eine Implementierung sieht dementsprechend einfach aus und kann sogar als Einzeiler dargestellt werden (Dank dem C#-Feature: Expression-bodied members):

private static int CalculateValue(Coordinate coordinate) => coordinate.X + coordinate.Y - (coordinate.X % 2);

Verbleiben noch die Umsetzungen der beiden Ausgabemethoden:

private static void PrintNumber(int number) => Console.WriteLine(number);

private static void PrintNoNumber() => Console.WriteLine("No Number");

Damit ist die Aufgabe auch schon gelöst. Wir haben ein einfaches Beispiel für Alternative Datenströme gesehen. Natürlich kann hier wieder argumentiert werden, dass der Code deutlich kompakter wäre, wenn alles in Main() gepackt werden würde. Aber dann leider ja wieder die Weiterentwicklungsfähigkeit. Und darum geht es doch letztendlich. Denn Software wird deutlich länger betrieben als man oft erwartet und in all dieser Zeit, müssen jederzeit Änderungen durchführbar sein.