Validering kan være et domene-ansvar


torsdag 3. september 2009 Testing OO Patterns C#

Hvis du skummet gjennom min forrige post, TDD i praksis, så observerte du meg implementere registrering av ny bruker. Selv om det var en lang artikkel var det (minst) en ting jeg utelot der – det var nemlig ingen validering av input. Alle registreringer ville blitt en suksess uansett hva man tastet inn.

Validering kan gjøres på mange måter, men for meg er det for det første et eget ansvar som fortjener å bli implementert som egne tjenester/moduler, og for det andre er det et domene-ansvar, og bør ikke knyttes til infrastrukturen eller brukergrensesnittet (denne påstanden gjelder ikke for de mest banale valideringene, og igjen vil noen kunne kalle denne approachen for overkill).

Merk at dette er en konklusjon jeg mer eller mindre har funnet på av meg selv - en fremgangsmåte som gir mening for meg – så hvis du har noen kommentarer og innvendinger så er de som alltid hjertelig velkomne!

Jeg vil benytte anledningen til å implementere validering ved hjelp av specification pattern. Jeg har blogget om dette før, så jeg vil ikke ta med alle detaljene, men jeg vil spesielt vise hvilke tester jeg har skrevet for validering av input. Jeg skrev en og en test, og sørget for at hver test ble grønn før jeg gikk videre, men her har du hele testklassen i one go. Bruk litt tid på å lese gjennom dem..

  103 [TestFixture]

  104     public class When_validating_a_registration

  105     {

  106         Mock<UserRepository> mockRepository;

  107         Specification<User> validator;

  108         User user;

  109 

  110         [SetUp]

  111         public void SetUp()

  112         {

  113             mockRepository = new Mock<UserRepository>();

  114             validator = new ValidRegistrationSpecification(mockRepository.Object);

  115             // Default valid user registration

  116             user = new User() { Username = "T-Man", Password = "abcd1234#" };

  117         }

  118         [Test]

  119         public void Should_validate_a_strong_password() {

  120             Assert.That(validator.IsSatisfiedBy(user));

  121         }

  122         [Test]

  123         public void Should_not_validate_password_without_letters() {

  124             user.Password = "12345678#";

  125             Assert.IsFalse(validator.IsSatisfiedBy(user));

  126         }

  127         [Test]

  128         public void Should_not_validate_password_without_digits()

  129         {

  130             user.Password = "abcdefgh#";

  131             Assert.IsFalse(validator.IsSatisfiedBy(user));

  132         }

  133         [Test]

  134         public void Should_not_validate_password_without_special_character()

  135         {

  136             user.Password = "abcd12345";

  137             Assert.IsFalse(validator.IsSatisfiedBy(user));

  138         }

  139         [Test]

  140         public void Should_not_validate_password_with_less_than_8_characters()

  141         {

  142             user.Password = "abc123#";

  143             Assert.IsFalse(validator.IsSatisfiedBy(user));

  144         }

  145         [Test]

  146         public void Should_not_validate_usernames_with_less_than_4_characters()

  147         {

  148             user.Username = "MrT";

  149             Assert.IsFalse(validator.IsSatisfiedBy(user));

  150         }

  151         [Test]

  152         public void Username_can_not_be_accepted_if_it_already_exist()

  153         {

  154             var mockRepository = new Mock<UserRepository>();

  155             mockRepository

  156                 .Setup(rep => rep.Find(It.IsAny<Predicate<User>>()))

  157                 .Returns(new[] { new User() });

  158             Assert.IsFalse(validator.IsSatisfiedBy(user));

  159         }

  160     }

Metodenavnene i testklassen gir en perfekt oppsummering av reglene for hva en som er en gyldig brukerregistrering. Et passord må inneholde bokstaver, tall, spesialtegn – totalt minst 8 stykker, og brukernavnet må være minst 4 tegn, og ikke eksistere fra før. Nå kunne jeg ha implementert denne valideringen med noen ganske få linjer kode, men jeg vil at designet skal reflektere kunnskapen om reglene, og at det skal være veldig enkelt å endre en og en regel, og å legge til eller fjerne regler. ValidRegistrationSpecification ser derfor slik ut:

   78 public class ValidRegistrationSpecification : Specification<User>

   79 {

   80     private List<Specification<User>> _specifications;       

   81     public ValidRegistrationSpecification(UserRepository repository)

   82     {

   83         _specifications = new List<Specification<User>>();

   84         _specifications.Add(new UsernameShouldBeAtLeastFourCharacters());

   85         _specifications.Add(new UsernameMustBeAvailable(repository));

   86         _specifications.Add(new PasswordMustHaveLetter());

   87         _specifications.Add(new PasswordMustHaveDigit());

   88         _specifications.Add(new PasswordMustHaveSpecialCharacter());

   89         _specifications.Add(new PasswordMustHaveAtLeastEightCharacters());

   90     }

   91     public bool IsSatisfiedBy(User candidate)

   92     {

   93         try

   94         {

   95             foreach (var specification in _specifications)

   96                 if (!specification.IsSatisfiedBy(candidate))

   97                     return false;

   98         }

   99         catch (Exception) { }

  100         return true;

  101     }

  102 }

Klassen arver fra Specification (for User) – et enkelt interface som bare inneholder én metode: IsSatisfiedBy(T candidate). Hvis IsSatifiedBy returnerer true for en gitt bruker, så kan registreringen gjennomføres.

ValidRegistrationSpecification er videre egentlig en liste med med mer detaljerte spesifikasjoner, én for hver test jeg har skrevet, og IsSatifiedBy spoler gjennom alle disse. La oss se nærmere på én av dem, som spesifiserer at et passord må inneholde bokstaver:

   19 public class PasswordMustHaveLetter : Specification<User>

   20 {

   21     public bool IsSatisfiedBy(User candidate)

   22     {

   23         for (int i = 0; i < candidate.Password.Length; i++)

   24             if (Char.IsLetter(candidate.Password[i]))

   25                 return true;

   26         return false;

   27     }

   28 }

Du er kanskje ikke vandt til klasser på bare 10 linjer (inkludert klammene)? Det er ingenting galt i det skal jeg si deg.

Her følger den mest avanserte spesifikasjonen (med en Cyclomatic Complexity på hele 3) som kontrollerer at brukernavnet ikke er opptatt. Den er avhengig av en UserRepository som den benytter til å se om den finner en match på brukernavnet.

   64 public class UsernameMustBeAvailable : Specification<User>

   65 {

   66     private UserRepository _repository;

   67     public UsernameMustBeAvailable(UserRepository repository)

   68     {

   69         _repository = repository;

   70     }

   71     public bool IsSatisfiedBy(User candidate)

   72     {

   73         return _repository.Find((user)

   74             => user.Username == candidate.Username)

   75             .Count() == 0;

   76     }

   77 }

Jeg har ikke lest Eric Evans bok, så jeg vet ikke om denne løsningen er helt i tråd med Domain-Driven Design. Men jeg vet han spesifikt snakker om Specification pattern (excuse the pun). Og i sommer lærte Bellware meg at entiteter aldri skal referere domenetjenester (som repositories), så valideringen kunne ikke vært plassert i User-klassen.

For ordens skyld – jeg sier ikke at det jeg driver med er Domain-Driven Design. Jeg vil heller, som Bellware, kalle det DDD-light: Jeg bruker mønstrene og ideene fra DDD der det gir verdi og mening i det systemet jeg til enhver tid utvikler.

I neste blogpost skal jeg samle trådene fra utenfra-og-inn programmering, TDD og mocking i praksis og denne posten, og analysere designet jeg har kommet opp med.

Knagger: , , , , , , ,


comments powered by Disqus