Das Studium – Ein EBC Beispiel

Immer wieder werde ich gefragt, ob ich eine komplettes Beispiel für EBC bereitstellen kann, was ich hiermit tun werde.
Allerdings, so muss ich vorweg sagen, dachte ich anfänglich, dass die Codings vom Ralf (Ralfs EBC Blogeinträge) ausreichen, um die Tragweite und Vorteile der EBCs zu erkennen. Da dem nicht so ist, stelle ich hiermit ein erstes komplettes Beispiel zur Verfügung, das auf der Grundlage der ersten EBC-Generation entstanden und durch seinen Forumsbeitrag vom Christof (Christofs Beitrag) inspiriert ist.

Das Szenario

Christof beschrieb eine Platine, die den Bruttowert eines Betrages berechnet und anschließend das Ergebnis bis auf zwei Nachkommastellen rundet. Er gab dabei zu bedenken, dass eine Platine eine implizite Logik bei der Verdrahtung enthält, die aus der Aufgabenstellung resultiert. Man könnte das Runden vor oder nach dem Berechnen durchführen.
An dieser Stelle möchte ich aber nicht implizite oder explizite Logik in der Platine reden, sondern EBC in einem Beispiel vorstellen. Ich habe mich für das Runden nach der Berechnung entschieden.
Textuelle Formulierung der Komponenten
Zitat aus Christofs Beitrag:
”Bauteil A :
- Berechnung der Mehrwertsteuer aus einer Eingabe
- Eingabe: beliebige Zahlen
- Ausgabe: Eingabe * Mehrwertsteuersatz (2% :-() )

Bauteil B :
- Aufgabe: Formatierung der Eingabe
- Eingabe: beliebige Zahlen
- Ausgabe: Zahl mit genau 2 Nachkommastellen, gegebenenfalls passend gerundet

Beide Bauteile werden auf Platine "MwSt" verbunden.”
Grafische Formulierung - Wiring Diagram
EBC VatBoard Dieses Diagramm stellt die Komponenten und das Zusammenspiel, also die Anforderung erst die Berechnung und dann die Rundung durch zuführen, dar.
Ich plädiere dafür, ein Tool zu haben, mit dem das Verdrahten grafisch dargestellt wird, weil diese Form der Darstellung viel leichter zu verstehen ist, als der dahinterliegende Code.

Das Projekt

EBC VatBoard Project
Das Solutionprojekt (Visual C# 2010 Express)  zur Anforderung habe ich so aufgebaut, das klar erkennbar wird, wo was passiert. Ein Projekt für die Contracts, eines für die Implementierung und eines für die Tests. Damit das Ganze lauffähig wird und auch mal ausprobiert werden kann, habe ich noch ein Main Board angelegt, das die MwSt-Platine instanziiert und die Parts übergibt.
Leider habe ich Concerns vermischen müssen, weil ich kein weiteres Projekt für das EBC-Framework eröffnen wollte, denn eine Komponente, die Zahlen runden kann, hat nun wirklich nichts bei der Berechnung eines Bruttowertes zu suchen. Aber vorausschauend habe ich die Komponente und ihren Contract in den Namespace de.ebc.Framework befördert, für den Fall, dass es später ein solches Framework geben sollte.
Die Tests werden noch (!) mit NUnit realisiert, aber ich bevorzuge Behavior Driven Testing und werde wohl in nächster Zeit NSpec oder MSpec wieder aus der Mottenkiste holen und dann alle meine Tests damit schreiben. Ich finde diese Art des Testings wesentlich verständlicher.
Die Contracts
Die Contracts definieren die Components, das ist nicht besonderes, weil das inzwischen State of the Art sein sollte. Das besondere an diesen Contracts ist aber die Tatsache, dass Pins definiert werden und keine Methoden, Funktionen und/oder Felder.
Gut, der Input Pin ist auch eine Methode, aber das liegt in der Natur der Sache. Auf irgend eine Weise muss ja das Signal, die Message, in die Komponente hineingebracht werden. Es muss aber eine grundlegender Unterschied zwischen den Pins eines Boards und denen der Parts gemacht werden.
In einigen Versuchen habe ich die Lesbarkeit des Codes mit Hilfe von Pin-Variationen überprüft und festgestellt, dass ein Unterschied die Lesbarkeit erhöht.
Hier einmal ein komplettes Beispiel nach der Methode vom Ralf;
// InPin Variante 1
// Im Contract:
public interface IComponent{
   void In_InMessage(TypeOfMessage message);
}   

// Die Platine
public class MyBoard{

   private Action<TypeOfMessge> _channelMethod; 
  
   public MyBoard(IComponent component){
      // Verdrahten auf der Platine
      this._channelMethod = component.In_InMessage;
      ...
   }
   
   // Board InPin
   public void In_BoardMessage(MessageType message){
      this._channnelMethod(message);
   }
}

Diese Variante finde ich zwar schon ganz nett, aber ich fand es nicht sonderlich gut lesbar. Schon allein die Notwendigkeit ein extra Feld “_channelMethod” zu definieren um die Message an den ersten InPin des ersten Bauteils weiterzugeben kam mir sperrig vor.

Andere Hilfsmittel wie Wire Klassen, die die Pins verbinden, haben mir auch nicht gefallen. Also habe ich durch Ausprobieren folgende Variante für hinreichend gut befunden. 


EBC Pins


  1. namespace de.ebc.Framework
  2. {
  3.     public delegate void InPin<TInput>(TInput inMessage);
  4.     public delegate void OutPin<TOut>(TOut outMessage);
  5. }



Wie sich diese Pins in den Code integrieren kann man in den folgenden Codeings sehen.

Ich beginnen mit der Definition des Boards, auf dem die beiden Bauteile verdrahtet werden. Auf dem Board sind zwei Pins definiert. Einer empfängt die Nachricht (In_CalcToGross) und der andere gibt das Ergebnis zurück (Out_Gross). Achtung, hier ist der InPin als Feld definiert worden, aus besagtem oben stehendem Grunde.


EBC IVatBoard Contract


  1. namespace de.ebc.vatboard
  2. {
  3.     /// <summary>
  4.     /// Definiert eine Komponente, die mit Mehrwertsteuer umgehen kann
  5.     /// </summary>
  6.     public interface IVatBoard
  7.     {
  8.         /// <summary>
  9.         /// Berechnet den Bruttowert einer Zahl
  10.         /// </summary>
  11.         /// <param name="netto"></param>
  12.         InPin<Decimal> In_CalcToGross { get; set; }
  13.  
  14.         /// <summary>
  15.         /// Gibt den Bruttowert zurück
  16.         /// </summary>
  17.         event OutPin<Decimal> Out_Gross;
  18.     }
  19. }



Die Komponente, die die Berechnung durchführt wird durch das Interface IVatCalculator definiert. Auch hier gibt es keine Überraschung, denn es werden nur die notwendigen Pins beschrieben. Allerdings wird bei einem Part der InPin nicht durch einen Delegaten dargestellt, hier wird wieder eine Methode verwendet.


EBC IVatCalculator Contract


  1. namespace de.ebc.vatboard
  2. {
  3.     /// <summary>
  4.     /// Definiert eine Komponenten, die den Bruttowert einer Zahl berechnet
  5.     /// </summary>
  6.     public interface IVatCalculator
  7.     {
  8.         /// <summary>
  9.         /// Berchnet den Bruttowert einer Zahl
  10.         /// </summary>
  11.         /// <param name="netValue"></param>
  12.         void In_CalculateGross(Decimal netValue);
  13.  
  14.         /// <summary>
  15.         /// Gibt den Bruttowert zurück
  16.         /// </summary>
  17.         event OutPin<Decimal> Out_Gross;
  18.     }
  19. }



Nun noch die Definition der Komponente, die das Runden der Zahlen übernimmt, und schon sind die Contracts fertig. Man beachte, dass dieser Contract nicht dem Namespace des VatBoards zugewiesen ist (aus Gründen der Seperation of Concerns).


EBC IRoundDecimals Contract


  1. namespace de.ebc.Framework
  2. {
  3.     /// <summary>
  4.     /// Definiert eine Komponenten, die Zahlen runden kann
  5.     /// </summary>
  6.     public interface IRoundDecimals
  7.     {
  8.         /// <summary>
  9.         /// Rundet eine Zahl auf zwei Nachkommastellen
  10.         /// </summary>
  11.         /// <param name="number"></param>
  12.         void In_RoundingToTwoDecimals(Decimal number);
  13.         
  14.         /// <summary>
  15.         /// Gibt die gerundete Zahl zurück
  16.         /// </summary>
  17.         event OutPin<Decimal> Out_RoundedNumber;
  18.     }
  19. }


Die Components (Parts und Boards)Was zu erwarten ist, sind die Komponenten das Fleisch der Contracts und es erwartet uns auch hier keine Überraschung.
Zuerst das Board, auf dem die Verdrahtung stattfindet.

EBC VatBoard


  1. namespace de.ebc.vatboard.Components
  2. {
  3.     public class VatBoard : IVatBoard
  4.     {
  5.         public VatBoard(IVatCalculator vatCalculator, IRoundDecimals numberRounder)
  6.         {
  7.             // InPin des Boards mit der ersten Komponente verdrahten
  8.             this.In_CalcToGross = vatCalculator.In_CalculateGross;
  9.             
  10.             // Ergebnis zum Runden an die nächste Komponente weiterleiten
  11.             vatCalculator.Out_Gross += numberRounder.In_RoundingToTwoDecimals;
  12.             
  13.             // Gerundete Zahl zurückgeben
  14.             numberRounder.Out_RoundedNumber += this.OnOutRoundedNumber;
  15.         }
  16.  
  17.         private void OnOutRoundedNumber(Decimal number) {
  18.             this.Out_Gross(number);
  19.         }
  20.  
  21.         public InPin<Decimal> In_CalcToGross {
  22.             get;
  23.             set;
  24.         }
  25.  
  26.         public event OutPin<decimal> Out_Gross = delegate { };
  27.     }
  28. }



Hier wird sichtbar, warum ich einen Delegeate InPin verwende, denn ich kann nun den InPin des Boards direkt mit dem InPin des ersten Bauteils verbinden, ohne Code-Rauschen.

Nun noch die beiden arbeitenden Komponenten:

EBC VatCalculator


  1. namespace de.ebc.vatboard.Components
  2. {
  3.     public class VatCalculator : IVatCalculator
  4.     {
  5.         public VatCalculator(decimal vatInPercent) {
  6.             this.VatInPercent = vatInPercent;
  7.         }
  8.  
  9.         public Decimal VatInPercent { get; private set; }
  10.  
  11.         public void In_CalculateGross(decimal netValue)
  12.         {
  13.             Decimal gross = GetGross(netValue);
  14.             this.Out_Gross(gross);
  15.         }
  16.  
  17.         private decimal GetGross(decimal netValue)
  18.         {
  19.             return netValue * (1 + VatInPercent / 100);
  20.         }
  21.  
  22.         public event OutPin<decimal> Out_Gross = delegate { };
  23.     }
  24. }



und


EBC RoundDecimals


  1. namespace de.ebc.vatboard.Components
  2. {
  3.     public class RoundDecimals : IRoundDecimals
  4.     {
  5.         public event OutPin<decimal> Out_RoundedNumber = delegate { };
  6.  
  7.         public void In_RoundingToTwoDecimals(decimal number)
  8.         {
  9.             decimal roundedNumber = RoundNumber(number);
  10.             this.Out_RoundedNumber(roundedNumber);
  11.         }
  12.  
  13.         private decimal RoundNumber(decimal number)
  14.         {
  15.             return Decimal.Round(number, 2);
  16.         }
  17.     }
  18. }



Ja, es kann Refaktorisiert werden, ich sehe es gerade, aber für das Beispiel ist es jetzt nicht relevant.

Wenn ich den Code gut geschrieben habe, bedarf es keiner weiteren Erklärung was hier passiert. Und um das festzustellen, werde ich auch keinen weiteren Kommentar dazu abgeben, denn ich hoffe auf die Kommentare der Leser ;-)
Das Testen der LösungDas Testen ist denkbar einfach, denn ich kann jede Komponente einzeln auf Herz und Nieren testen. Ich könnte aber auch ganze Baugruppen für die Tests schaffen und durch probieren. Das habe ich hier nicht gemacht.
Wie schon erwähnt kommt NUnit zum Einsatz, was sich meines Erachtens in der nächsten Zeit ändern wird.

Das Testen der Komponente VatCalculator besteht darin, einen festgelegten Nettowert von 12 in die Komponente hinein zu senden, um dann den Bruttowert zu empfangen und auszuwerten.


Testen von VatCalculator


  1. [TestFixture]
  2. public class TestVatCalculator : WithVatCalculator
  3. {
  4.     [Test]
  5.     public void Calculate_For_Number_12()
  6.     {
  7.         decimal netValue = 12;
  8.         decimal vat = this.VatCalculator.VatInPercent;
  9.         decimal shouldBrutto = (netValue * (1+vat/100));
  10.  
  11.         this.VatCalculator.Out_Gross += brutto =>
  12.         {
  13.             Assert.AreEqual(shouldBrutto, brutto);
  14.         };
  15.  
  16.         this.VatCalculator.In_CalculateGross(netValue);
  17.     }
  18. }



Ja gut, es ist nicht wirklich der Brain Burner, aber es veranschaulicht die Einfachheit, mit der eine EBC getestet werden kann.

Das zweite Bauteil, die Rundungskomponente, wir auf die gleiche Art getestet.


Testen von RoundDecimals


  1. [TestFixture]
  2. public class TestRounding : WithRoundDecimals
  3. {
  4.     [Test]
  5.     public void Round_To_Two_Decimals()
  6.     {
  7.         decimal aNumber = 12.34567m;
  8.         this.RoundDecimals.Out_RoundedNumber += roundedNumber =>
  9.         {
  10.             Assert.AreEqual(12.35m, roundedNumber);
  11.         };
  12.  
  13.         this.RoundDecimals.In_RoundingToTwoDecimals(aNumber);
  14.     }
  15. }



Die Lösung als Applikation
Und wie sieht das Ganze im Applikationkontext aus?

Ich habe als Beispiel die Console gewählt, was nicht wirklich überrachend ist und dann den Aufruf wie folgt formuliert:


VatBoard als Applikation


  1. namespace de.ebc.VatMainBoard
  2. {
  3.     class Program
  4.     {
  5.         static void Main(string[] args)
  6.         {
  7.  
  8.             Decimal vat = 16m;
  9.  
  10.             IVatCalculator vatCalculator = new VatCalculator(vat);
  11.             IRoundDecimals numberRounder = new RoundDecimals();
  12.  
  13.             IVatBoard vatBoard = new VatBoard(vatCalculator, numberRounder);
  14.  
  15.             // Ergebnisausgabe
  16.             vatBoard.Out_Gross += gross => {
  17.                 Console.WriteLine(gross);
  18.             };
  19.  
  20.             string netValueString = Console.ReadLine();
  21.             Decimal netValue = 0;
  22.  
  23.             Decimal.TryParse(netValueString, out netValue);
  24.             vatBoard.In_CalcToGross(netValue);
  25.  
  26.             Console.ReadLine();
  27.         }
  28.     }
  29. }



Und auch hier nicht viele Worte verlieren, denn es ist nur ein Beispiel und es soll sich selbst erklären, ansonsten habe ich es einfach nicht gut geschrieben.

Das Projekt kann bei codeplex unter folgendem Link EBC Samples heruntergeladen werden.

Na dann mal ran und her mit den Kommentaren.


Jan(ek)