C#Clean Code

Validierung mit Splits und Joins

Auch komplexere Verzweigungen lassen sich mit Splits und Joins auf Datenflussebene mit einem Flow Design-Diagramm lösen. Hier wird dies anhand der Weiterentwicklung einer vorherigen Lösung gezeigt.

Validierung mit Splits and Joins
Glenn Carstens-Peters

Ich möchte heute noch ein weiteres Beispiel für das Thema Verzweigung aufführen. Und zwar möchte ich dazu, meine Lösung für die Kata römische Zahlen weiter ausbauen. Die Erweiterung (z.B. Beschreibung auf Codewars.com) besteht darin, syntaktische und semantische Fehler zu finden.

Hier das erweiterte Flow-Design, dass ich dieses Mal mit Diagrams.net (ehemals Draw.io) erstellt habe. Der Hand-gekrizellte Look gefällt mir dabei sehr gut, da er zeigt, dass man das Diagramm auch schnell auf Papier erstellen könnte:

Image

Die Integration Convert und deren Operationen können quasi unverändert weiterverwendet werden. Die Validierung wird in diesem Falle durch die beiden Operationen CheckSyntax und CheckSemantic vorgeschaltet. Alles zusammen wird durch die neue Integration CheckAndConvert integriert.

Schaut man sich das Diagramm genauer an, stellt man fest, dass es neben Abzweigungen (Splits) auch Zusammenführungen (Joins) gibt. Die eine ist mit den beiden onError-Datenflüsse der beiden Operationen zur Validierung schnell gefunden. Hier werden die beiden Datenflüsse zu einem kombiniert, da in CheckAndConvert nur noch interessant ist, ob ein Fehler aufgetreten ist, aber nicht mehr ob der Fehler in der Syntax oder der Semantik liegt. Das Zusammenführen wird durch einen Querbalken symbolisiert. Der andere ist für den unerfahrenen Flow-Design-Modellierer vielleicht schwerer zu sehen. Es handelt sich dabei um die Datenflüsse im validen Fall. Hier wird durch die Kurznotation (aa) | (bb) eine Abzweigung des Datenflusses angeschrieben. Diese Notation besagt, dass aus der ersten Funktionseinheit der Datenfluss (aa) herausfließt und in die darauffolgende Funktionseinheit der Datenfluss (bb) hineinfließt. Die Daten im Datenfluss (bb) kommen dann in der Regel von anderer Stelle (z.B. von einer zuvor aufgerufenen Funktionseinheit). In unserem konkreten Fall kommen die Daten, nämlich romanNumberUpper, aus der Operation toUpper.

Interessant ist sicherlich die Umsetzung der Integration CheckAndConvert. Diese erfolgt wieder mit Action-Delegaten und Lambda-Ausdrücken:

  public static void CheckAndConvert(string romanNumber, Action<string, int> onNumber, Action<string, string> onError)
  {
    var romanNumberUpper = ToUpper(romanNumber);
    CheckSyntax(romanNumberUpper, 
                () => CheckSemantic(romanNumberUpper, 
                                    () => onNumber(romanNumber, Convert(romanNumberUpper)), 
                                    x => onError(romanNumber, x)), 
                x => onError(romanNumber, x));
  }

Hier die komplette Umsetzung:

using System;
using System.Collections.Generic;
using System.Linq;

internal static class FromRomanNumerals
{
  private static void PrintNumber(string roman, int number) => Console.WriteLine("{0} -> {1}", roman, number);

  private static void PrintError(string roman, string message) => Console.WriteLine("{0} -> {1}", roman, message);

  public static void CheckAndConvert(string romanNumber, Action<string, int> onNumber, Action<string, string> onError)
  {
    var romanNumberUpper = ToUpper(romanNumber);
    CheckSyntax(romanNumberUpper, 
                () => CheckSemantic(romanNumberUpper, 
                                    () => onNumber(romanNumber, Convert(romanNumberUpper)), 
                                    x => onError(romanNumber, x)), 
                x => onError(romanNumber, x));
  }

  private static string ToUpper(string romanNumber) => romanNumber.ToUpper();

  private static void CheckSyntax(string romanNumber, Action onSyntaxValid, Action<string> onSyntaxInvalid)
  {
    var invalidChars = romanNumber.Where(x => !"IVXLCDM".Contains(x));
    var asString = string.Join(",", invalidChars.Select(x => string.Format("'{0}'", x)));
    if (string.IsNullOrEmpty(asString))
      onSyntaxValid();
    else
      onSyntaxInvalid(string.Format("Syntax error. Illegal characters detected: {0}", asString));
  }

  private static void CheckSemantic(string romanNumber, Action onSemanticValid, Action<string> onSemanticInvalid)
  {
      var charFollows = new Dictionary<char, List<char>> {
        {'I', new List<char> {'V', 'X', 'I'}}, 
        {'V', new List<char> {'I'}}, 
        {'X', new List<char> {'L', 'C', 'X', 'V', 'I'}}, 
        {'L', new List<char> {'X', 'V', 'I'}}, 
        {'C', new List<char> {'D', 'M', 'C', 'L', 'X', 'V', 'I'}}, 
        {'D', new List<char> {'C', 'L', 'X', 'V', 'I'}}, 
        {'M', new List<char> {'M', 'D', 'C', 'L', 'X', 'V', 'I'}}};

      for (var i = 0; i < romanNumber.Length - 1; i++)
      {
        var chr = romanNumber[i];
        var nextChr = romanNumber[i + 1];
        if (!charFollows[chr].Contains(nextChr))
        {
          onSemanticInvalid(string.Format("Semantic error. Character {0} follows {1}", chr, nextChr));
          return;
        }
      }

      onSemanticValid();
  }

  public static int Convert(string romanNumber)
  {
    var romanNumerals = SplitRomanNumerals(romanNumber);
    var decimals = ConvertToDecimal(romanNumerals);
    var negatedDecimals = NegateWhenLarger(decimals);
    return Sum(negatedDecimals);
  }

  private static char[] SplitRomanNumerals(string romanNumber) => return romanNumber.ToCharArray();

  private static int[] ConvertToDecimal(IEnumerable<char> romanNumerals)
  {
    var mapping = new Dictionary<char, int> {{'I', 1}, {'V', 5}, {'X', 10}, {'L', 50}, {'C', 100}, {'D', 500}, {'M', 1000}};
    return romanNumerals.Select(x => mapping[x]).ToArray();
  }

  private static int[] NegateWhenLarger(int[] decimals)
  {
    var result = new int[decimals.Length];
    for (var i = 0; i < decimals.Length; i++)
    {
      if (i < decimals.Length - 1 && decimals[i] < decimals[i + 1])
        result[i] = -decimals[i];
      else
        result[i] = decimals[i];
    }
    return result;
  }

  private static int Sum(int[] decimals) => return decimals.Sum();
}

Auch wenn die semantische Prüfung noch nicht vollständig umgesetzt ist (Fehlerhafte Beispiele sind IIIII oder IIX) sieht man schön wie man eine Verkettung dank Continuations und Lambda Ausdrücken einfach umsetzen kann.