You never code alone!

Meine letzte Woche in der Arbeit würde ich gerne the cleverbridge-University-Woche nennen, so großartig war sie. Wir haben 3 von 5 Tagen damit verbracht, unseren Code, unsere Prozesse, unseres Miteinander zu analysieren, zu sezieren, zu verbessern:

  • Dienstag hatten wir fast einen ganz Tag Architektur-Workshop um die Vision und die Strukturen für unseren Code zu definieren.
  • Den Freitag haben wir gemeinsam bei der dotnet-cologne 2014 verbracht.

Das wichtigste hat allerdings am Donnerstag stattgefunden: wir haben uns einen ganz Tag Zeit genommen, um von Ilker, meinem TDD-Mentor, so viel wie möglich über Specs, Tests, Teams und Kommunikation zu verstehen.

Es geht nichts über Verstehen!

from requirement to specifications
Wie schafft es ein Team, das und nur das zu bauen, was der Kunde braucht?

  1. Findet alle zusammen – product owner, Entwickler, Tester – das richtige Wording, die “ubiquitous language” heraus, die die Anforderung beschreiben
  2. Versteht alle zusammen, worum es geht.
  3. Definiert zusammen das Feature
  4. Schreibt zusammen die Akzeptanzkriterien

Wie werden eigentlich eine paar Menschen zu einem Team?

Dies ist wahrscheinlich das Problem aller Firmen weltweit, die sich weiterentwickeln möchten. Und das zu Recht: stell dir vor, du bist ein C#-Entwickler mit zum Beispiel Schwerpunkt Web, hast JavaScript warscheinlich noch nie als Programmiersprache betrachtet und von der Arbeit der anderen C#-Entwickler weißt nichts, außer dass sie existiert und sie irgendwann später erfolgen wird, nachdem du deine Aufgaben schon längst erledigt hast. Einen Abteilungsleiter hast natürlich auch und deine Entscheidungsgewalt beschränkt sich auf “soll ich foreach oder lieber LINQ verwenden?”

Das ist allerdings nicht alles: deine Aufgaben überschreiten niemals die Grenzen deiner Abteilung, genauso wenig wie dein Verständnis über die Anforderungen. Und das stört dich seit langem und nicht nur dich sondern auch den JavaScript-Typen und die Frontend-Entwicklerin und wenn du Glück hast, dann auch deinen Abteilungsleiter. Wenn das der Fall ist, dann hat deine Firma Glück: ihr seid offen für ein Crossfunktionales Team. Ihr musst nur diese Frage bejahen:

Bin ich bereit, jede Rolle in diesem Team zu erfüllen?
Auch mit der Gefahr, dass ich das nicht kann? Wenigstens noch nicht, oder noch nicht gut genug…

Jedes Team braucht noch ein paar Zutaten:

  1. Ein Ziel
  2. Eine Partizipation – eine gemeinsame “Teilhaberschaft”
  3. Den Respekt der Individualität
  4. Die gemeinsame Verantwortung

oder wie Ilker sagt “oder mindestens 3 davon plus jemanden, der die anderen die ganze Zeit daran erinnert”.

 

Ich will nicht behaupten, dass wir bereits alles darauf haben – aber wir sind auf dem besten Wege dahin. Und was das Wichtigste ist: wir werden diesen Weg sicherlich nicht verlassen.

Kontextabhängige Datenvalidierung

Die Idee stammt von Jimmy Nilsson – Applying Domain-Driven Design and Patterns. Er hat nach einer Möglichkeit gesucht, die immer wiederkehrende Aufgabe, Daten zu validieren, flexibel und kontextabhängig zu gestalten, und zwar so, dass man es nur einmal schreiben muss.

Wie läuft normalerweise so eine Validierung ab? Man will wissen, ob eine Instanz als solche allen Vorschriften entspricht, und wenn nicht, dann welche Felder passen nicht. Jeder, der jemals Webanwendungen geschrieben hat, weiß, wie mühsam und langweilig es ist, jeden Eingabewert auf Gültigkeit zu testen. (Hier geht allerdings nicht unbedingt um Formulare, da kann man ja die Validierung z.B. bei ASP.MVC 2.0 mit Data Annotations durchführen.)

Also zurück zu den Anforderungen: um die verschiedenen Regeln wiederverwendbar zu machen, braucht man diese von einem Interface abzuleiten:

namespace ValidationFramework
{
    public interface IRule
    {
        bool IsValid { get; }
        int IdRule { get; }
        string[] BooleanFieldsThatMustBeTrue { get; }
        string Message { get; }
    }
}

IsValid sagt aus, ob der Regel verletzt wurde. Die IdRule ist dafür da, um diese Regeln einfacher identifizieren zu können. BooleanFieldsThatMustBeTrue kann dafür verwendet werden, um Vorbedingungen zu prüfen. Message braucht wohl keine Erklärung.
Jetzt kann man verschiedene Regeln und eine Basisklasse für gemeinsame Funktionalitäten definieren:

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

namespace ValidationFramework
{
    public abstract class RuleBase : IRule
    {
        private readonly object m_value;
        private readonly int m_idRule;
        private readonly string[] m_booleanFieldsThatMustBeTrue;
        private readonly object m_holder;

        protected RuleBase(int idRule, string[] fieldsConcerned, string fieldname, object holder)
        {
            m_value = GetPropertyValue(holder, fieldname);
            m_booleanFieldsThatMustBeTrue = fieldsConcerned;
            m_holder = holder;
            m_idRule = idRule;
        }

        private static object GetPropertyValue(object holder, string fieldname)
        {
            return holder.GetType().GetProperty(fieldname).GetValue(holder, null);
        }

        protected object GetValue()
        {
            return m_value;
        }
        protected object GetHolder()
        {
            return m_holder;
        }

        public abstract bool IsValid { get; }
        public int IdRule
        {
            get { return m_idRule; }
        }

        public string[] BooleanFieldsThatMustBeTrue
        {
            get { return m_booleanFieldsThatMustBeTrue; }
        }

        public abstract string Message { get; }

        protected bool BooleanFieldsConcernedAreTrue()
        {
            return
                m_booleanFieldsThatMustBeTrue.Select(a => (bool)m_holder.GetType().GetProperty(a).GetValue(m_holder, null)).
                    Select(b => b).Count() == m_booleanFieldsThatMustBeTrue.Length;
        }
    }

    public class DateIsInRangeRule : RuleBase
    {
        private readonly DateTime m_minDate;
        private readonly DateTime m_maxDate;

        public DateIsInRangeRule(DateTime minDate, DateTime maxDate, int idRule, string fieldName, object holder) : base( idRule, null,fieldName, holder)
        {
            m_minDate = minDate;
            m_maxDate = maxDate;
        }

        public override bool IsValid
        {
            get { 
                var value = (DateTime) GetValue();
                return value >= m_minDate && value <= m_maxDate;
            }
        }

        public override string Message
        {
            get {
                return IsValid
                           ? string.Empty
                           : string.Format("Das Datum ist nicht in gültigen Bereich: {0}-{1}", m_minDate, m_maxDate); }
        }
    }

    public class MaxStringLengthRule : RuleBase
    {
        private readonly int m_maxLength;

        public MaxStringLengthRule(int maxLength, int idRule, string fieldname, object holder) : base(idRule, null, fieldname, holder)
        {
            m_maxLength = maxLength;
        }

        public override bool IsValid
        {
            get { return GetValue().ToString().Length <= m_maxLength; }
        }

        public override string Message
        {
            get {
                return IsValid
                           ? string.Empty
                           : string.Format("Die zugelassene Länge von {0} Zeichen wurde überschritten",m_maxLength); }
        }
    }
}

Jetzt, da die Grundlagen stehen, schauen wir mal, wie man die Validierungsregeln festlegen würde.
Hier ist eine ganz einfache Beispielklasse:

using System;

namespace ValidationFramework
{
    public class Account
    {
        public Account()
        {
            Created = DateTime.Now;
            Address = string.Empty;
        }

        public DateTime Created { set; get; }
        public string Address { get; set; }
        public bool Activated { get; set; }
    }
}

Jetzt definieren wir die Regeln, wonach eine Instanz valide ist oder nicht. Die neue Eigenschaft IsValidated soll diese Information speichern.

    public class Account
    {
        private readonly IList<IRule> m_validationRules = new List<IRule>();

        public Account()
        {
            Created = DateTime.Now;
            Address = string.Empty;
        }

        public DateTime Created { set; get; }
        public string Address { get; set; }
        public bool Activated { get; set; }
        public bool IsValidated { get; set; }

        private void SetupValidationRules()
        {
            m_validationRules.Add(new DateIsInRangeRule(new DateTime(1990,1,1),DateTime.Now,1,"Created",this ));
            m_validationRules.Add(new MaxStringLengthRule(10,2,"Address",this));
        }
    }

Um Regeln auch dynamisch setzen zu können, wird eine Methode AddValidationRule(IRule) definiert

 
    public class Account
    {
...    
        public void AddValidationRule(IRule rule)
        {
            m_validationRules.Add(rule);
        }
...
    }

Nun müssen wir diese Regeln nur noch auswerten. Dafür wird in RuleBase eine statische Methode definiert und in der Beispielklasse die Methode IEnumerable GetBrokenRules()

    public abstract class RuleBase : IRule
    {
...
        public static IEnumerable<IRule> CollectBrokenRules(IList<IRule> rulesToCheck)
        {
            return rulesToCheck.Where(a => !a.IsValid).Select(a => a);
        }
    }
    public class Account
    {
...
        public IEnumerable<IRule> GetBrokenRules()
        {
            SetupValidationRules();
            return RuleBase.CollectBrokenRules(m_validationRules);
        }
...
    }

Um zu beweisen, dass es funktioniert, hier ein Paar Tests:

using System;
using System.Linq;
using NUnit.Framework;

namespace ValidationFramework.Tests
{
    [TestFixture]
    [Category("DateIsInRangeRule")]
    public class If_the_CreationDate_of_the_Account_is_in_range
    {
        [Test]
        public void Then_the_property_Created_is_valid()
        {
            var sut = new Account { Created = DateTime.Now.AddYears(-1) };
            Assert.That(sut.GetBrokenRules().Count(),Is.EqualTo(0));
        }
    }
    [TestFixture]
    [Category("DateIsInRangeRule")]
    public class If_the_CreationDate_of_the_Account_is_to_old
    {
        [Test]
        public void Then_the_property_Created_is_not_valid()
        {
            var sut = new Account { Created = new DateTime(1989,1,1)};
            Assert.That(sut.GetBrokenRules().Count(), Is.EqualTo(1));
            Assert.That(sut.GetBrokenRules().First().IdRule, Is.EqualTo(1));
        }
    }

    [TestFixture]
    [Category("MaxStringLengthRule")]
    public class If_the_Address_of_the_Account_is_to_long
    {
        [Test]
        public void Then_the_property_Address_is_not_valid()
        {
            var sut = new Account { Address = "12345678901"};
            Assert.That(sut.GetBrokenRules().Count(), Is.EqualTo(1));
            Assert.That(sut.GetBrokenRules().First().IdRule,Is.EqualTo(2));
        }
    }
}

Mit den vorhandenen Tests kann man nun refaktorisieren um die Prinzipien der Separation Of Concern einzuhalten. Außerdem ist nun Zeit, auch über die Kontextbezogenheit nachzudenken.
Die Klasse Account wird wahrscheinlich durch irgendeinen ORMapper gefüllt und braucht auf keinem Fall die Verantwortung, die Validierungsregeln zu verwalten. Deshalb kann man das Specification-Pattern von DDD anwenden, und diese Regeln in so eine Spezifikation verschieben:

using System;
using System.Collections.Generic;

namespace ValidationFramework
{
    public interface IValidationSpecification
    {
        IList<IRule> GetValidationRules();
    }

    public class AccountValidationSpecification : IValidationSpecification
    {
        private readonly Account m_objectToValidate;

        public AccountValidationSpecification(object objectToValidate)
        {
            m_objectToValidate = (Account) objectToValidate;
        }

        public IList<IRule> GetValidationRules()
        {
            return new List<IRule>
                       {
                           new DateIsInRangeRule(new DateTime(1990, 1, 1), DateTime.Now, 1, "Created",
                                                 m_objectToValidate),
                           new MaxStringLengthRule(10, 2, "Address", m_objectToValidate)
                       };
        }
    }
}

Die Spezifikationen werden selbstverständlich von einer Factory geliefert und sie werden per Setter Injection gesetzt.

    public  class ValidationSpecificationFactory
    {
        public static IValidationSpecification Create<T>(object objectToValidate)
        {
            if (typeof(T) == typeof(Account))
                return new AccountValidationSpecification(objectToValidate);
            throw new NotSupportedException();
        }
    }
    public class Account
    {
...
        public void SetValidationSpecification(IValidationSpecification specification)
        {
            foreach (IRule rule in specification.GetValidationRules())
                m_validationRules.Add(rule);
        }
...
    }
//Der Aufruf ist dann
var account = new Account();
account.SetValidationSpecification(ValidationSpecificationFactory.Create<Account>(account));

Dadurch ist die Bedingung, Kontextabhängige Validierungsregeln festlegen zu können, erfüllt.

Man kann generische Spezifikationen definieren, die die Regeln für verschiedene Zwecke, z.B. für Persistieren oder auch einfach nur für Akzeptieren definieren:

   
    public interface IValidationSpecification
    {
        IList<IRule> GetValidationRules();
        IList<IRule> GetValidationRulesRegardingPersistence();
    }

Hier der komplette Quellcode zum Herunterladen.