OO-design/clean code

En god utvikler er opptatt av objektorientert design og ren kode. I denne kategorien skiver jeg om design patterns og relaterte ting. Forvent endel kode-eksempler..

TDD og mocking i praksis

I de to forrige postene, Utenfra-og-inn programmering og Avhengigetsisolering (a.k.a Mocking), har jeg forklart hvordan jeg praktiserer testdreven utvikling. Denne gangen gjør jeg et forsøk på å vise det i praksis, en slags “tutorial” om du vil. Det er selvfølgelig en overhengende fare for at jeg produserer en svært lang blogpost, men om du har lyst til å bli med meg og oppleve Mockist TDD i praksis så er du i alle fall svært velkommen.

Jeg dokumenterer steg for steg hva jeg gjør, og om du vil må du gjerne kode oppgaven parallelt med meg – det vil du nok lære mest av. I så fall trenger du NUnit og Moq (eller annet testrammeverk og mockingrammeverk du er komfortabel med), db4objects, samt Visual Studio. Jeg har ikke lagt ved den ferdige løsningen, det er selve prosessen som er poenget her.

Når jeg gjør testdreven utvikling forsøker jeg å utvikle i så små steg som mulig, og jeg hopper mye frem og tilbake mellom test og produksjonskode. Jeg har også en tendens til å foretrekke mange, små klasser som jobber sammen for å løse oppgaver, til fordel for større klasser med mer kompleks logikk. Noen vil kalle det overkill, jeg kaller det et fleksibelt design. Du kan gjøre deg opp din egen mening.

Oppgaven

“På vår fantastiske webside skal man kunne registrere seg som ny bruker med brukernavn og passord. Passordet skal selvsagt lagres kryptert. Når man har en brukerkonto skal man kunne logge inn.”

Ok, det hørtes ikke så vanskelig ut – de fleste utviklere har gjort dette noen ganger før. Asp.net har jo også kontroller og providere som gjør dette veldig enkelt. Men vi skal bruke denne oppgaven til å øve på TDD, så vi later som om alt det der ikke finnes, glemmer hva vi har gjort tidligere, og setter i gang.

En liten påminnelse: Det finnes hundrevis av måter å implementere dette på, og det finnes hundrevis av måter å gjøre TDD på. Dette er min fremgangsmåte, slik jeg foretrekker akkurat nå. Det finne singen fasit, og neste gang ville jeg kanskje ha gjort det anderledes. Så ikke kopier meg, men reflekter over hvorfor jeg gjør det jeg gjør, og du vil sikkert lære noe på veien.

Forberedelser

Jeg fyrer opp Visual Studio og åpner et nytt prosjekt basert på Asp.net Web Application templaten. Vi innbiller oss at dette er den eksisterende websiten vi skal utvikle den nye “featuren” for. For å gjøre dette enkelt vil alt opprettes i dette ene prosjektet, inkludert testene – normalt ville jeg ha fordelt elementene i løsningen i diverse moduler.

Den første testen, en kontroller og et view

Oppgaven vi har fått er todelt: Brukerregistrering og brukerautentisering. Registrering kommer naturlig før autentisering, så vi starter med det. Er du en utvikler som ikke er vandt til TDD og utenfra-og-inn programmering kan det nå være fristene å tenke at du har lyst til å starte med å opprette en bruker-klasse, eller kanskje definere hvordan databasen skal se ut. Det skal vi ikke gjøre. Ditt første instinkt skal være å skrive en test, en test som passerer om brukerregistreringen fungerer.

La oss opprette en testklasse – jeg velger å gi den det merkelige navnet When_registering_a_new_user:

    9 [TestFixture]

   10 public class When_registering_a_new_user

   11 {

   12 }

Jeg ser for meg at jeg vil ha en eller annen form for controller som tar seg av registreringen. Controlleren kan kommunisere med et view hvor brukeren skal taste inn et brukernavn og passord og trigge registreringen. Når registreringen er gjennomført kan controlleren kommunisere dette tilbake via viewet. La meg forsøke å skrive en test:

    9 [TestFixture]

   10 public class When_registering_a_new_user

   11 {

   12     [Test]

   13     public void Should_report_registration_ok_in_view()

   14     {

   15         var view = new UserRegistrationView();

   16         var controller = new UserRegistrationController(view);

   17         view.TriggerRegistrationRequest(“Kong Håkon”, “1234″);

   18         Assert.IsTrue(view.RegistrationOk);

   19     }

   20 }

Ingen av disse klassene finnes enda. Dette er helt vanlig når man gjør TDD; man forsøker først å finne ut hvordan klassene bør se ut – deretter implementerer man dem.

Jeg er ikke helt fornøyd med hvordan jeg må trigge registration request ved å kalle en metode på viewet, og jeg like heller ikke at jeg må implementere to ulike klasser for denne testen – jeg ønsker å fokusere på én ting om gangen. Men hvis jeg foreløpig lar viewet være et abstrakt interface, og tar i bruk mockingrammeverket (Moq) kan vi få dette til å bli litt bedre. Jeg endrer testen:

   13 [Test]

   14 public void Should_report_registration_ok_in_view()

   15 {

   16     var mockView = new Mock<UserRegistrationView>();

   17     var controller = new UserRegistrationController(mockView.Object);

   18     mockView.Raise(view => view.RegistrationRequested += null, EventArgs.Empty);

   19     mockView.Verify(view => view.SetRegistrationResult(true), Times.Once());

   20 }

Det vil være mye rødt i editoren nå siden det fortsatt ikke er noen av disse klassene, metodene eller eventene som eksisterer, men det lærer vi oss raskt å leve med, så forsøk å se bort i fra det.

Denne testen synes jeg ble litt bedre. På linje 18 trigger jeg et RegistrationRequested event, som jeg tenker vil tilsvare eventet som fyres når brukeren klikker på “register” knappen på websiden. På neste linje verifiserer jeg at SetRegistrationResult har blitt kalt med argumentet “true”. Jeg vil nemlig at controlleren skal plukke opp registreringsforespørselen og kalle den metoden om alt går som det skal. I stedet for å verifisere på tilstand verifiserer jeg nå adferd.

Det er på tide å gjøre noe med alle de røde advarslene, så nå vil jeg opprette controlleren og viewet. Siden jeg bruker CodeRush er dette kun noen få tastetrykk – bruker du ikke noe slikt verktøy må du gjøre det for hånd. Dette er hva jeg kommer opp med.

   10 public interface UserRegistrationView

   11 {

   12     event EventHandler RegistrationRequested;

   13     void SetRegistrationResult(bool success);

   14 }

   15 public class UserRegistrationController

   16 {

   17     private UserRegistrationView _view;

   18     public UserRegistrationController(UserRegistrationView view)

   19     {

   20         _view = view;

   21     }

   22 }

Nå kompilerer prosjektet vårt, men kjører jeg testen vil den selvfølgelig feile, for SetRegistrationResult vil aldri bli kalt. Og det er helt perfekt. I TDD følger man et mønster som heter “Red, Green, Refactor”, som innebærer at vi først skal sørge for å lage en test som feiler. Det har vi oppnådd.

En grønn test

Så hvordan kan vi komme til neste steg – en vellykket test? Jo, det er veldig enkelt, selv om det virker som en helt idiotisk ting å gjøre:

   18 public UserRegistrationController(UserRegistrationView view)

   19 {

   20     _view = view;

   21     view.SetRegistrationResult(true);

   22 }

Jeg kaller rett og slett SetRegistrationResult i konstruktøren til controlleren. Hvorfor det? Jo, nå vet jeg at testen fungerer – at testen blir grønn hvis SetRegistrationResult kalles. To av tre steg er gjennomført, og vi kan gå igang med det litt vage steget “refakturering”.

Men hva med eventet som trigges? Det var jo det som skulle føre til registreringen. Stemmer det! Jeg kunne ha skrevet en ny test som passet på at SetRegistrationResult ikke ble kalt uten at eventet ble trigget, men akkurat nå synes jeg ikke det er nødvendig. Jeg gjør denne endringen med den ene testen jeg har:

   15 public class UserRegistrationController

   16 {

   17     private UserRegistrationView _view;

   18     public UserRegistrationController(UserRegistrationView view)

   19     {

   20         _view = view;

   21         _view.RegistrationRequested += _view_RegistrationRequested;

   22     }

   23 

   24     void _view_RegistrationRequested(object sender, EventArgs e)

   25     {

   26         _view.SetRegistrationResult(true);

   27     }

   28 }

.. jeg kjører testen på nytt etter endringen for å være sikker på at jeg ikke har ødelagt noe; den er fortsatt grønn, så alt er ok.

Test nummer 2: User repository

Men det skjer jo ingen registrering her. Hmmm. En registrering innebærer å opprette og lagre en ny bruker, ikke sant?! (Som du ser spiller jeg dum, og forplikter meg dermed ikke til et konkret design før det er absolutt nødvendig.) Ok, da trenger vi en eller annen form for Repository som kan lagre nye brukere. Jeg får skrive en ny test – kan jo ikke begynne å implementere noe nytt uten…

   40 [Test]

   41 public void Should_persist_user_in_repository()

   42 {

   43     var mockView = new Mock<UserRegistrationView>();

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

   45     var controller = new UserRegistrationController(mockView.Object, mockRepository.Object);

   46     mockView.Raise(view => view.RegistrationRequested += null, EventArgs.Empty);

   47     mockRepository.Verify(r => r.Insert(It.IsAny<User>()));

   48 }

Jeg lager en ny mock som skal representere en repository, og sender den også inn til controlleren. I linje 47 kontrollerer jeg at Insert har blitt kalt, og at det ble sendt inn et User objekt. Hverken UserRepository eller User eksisterer enda.

Det meste av testen ligner på den forrige testen, så DRY-prinsippet (kanskje det aller viktigste prinsippet innen softwareutvikling) tvinger meg til å trekke ut det som er felles. Jeg ender opp med denne testklassen:

   29 [TestFixture]

   30 public class When_registering_a_new_user

   31 {

   32     [Test]

   33     public void Should_report_registration_ok_in_view()

   34     {

   35         When_doing_a_registration();

   36         mockView.Verify(view => view.SetRegistrationResult(true), Times.Once());

   37     }

   38     [Test]

   39     public void Should_persist_user_in_repository()

   40     {

   41         When_doing_a_registration();

   42         mockRepository.Verify(r => r.Insert(It.IsAny<User>()));

   43     }

   44     private void When_doing_a_registration()

   45     {

   46         mockView = new Mock<UserRegistrationView>();

   47         mockRepository = new Mock<UserRepository>();

   48         controller = new UserRegistrationController(mockView.Object, mockRepository.Object);

   49         mockView.Raise(view => view.RegistrationRequested += null, EventArgs.Empty);

   50     }

   51     private Mock<UserRepository> mockRepository;

   52     private Mock<UserRegistrationView> mockView;

   53     private UserRegistrationController controller;

   54 }

For at dette skal kompilere må jeg gjøre tre ting; opprette UserRepository, fikse controllerens konstruktør til å ta inn en repository, og opprette en User klasse. Noen CodeRush-shortcuts senere har jeg opprettet følgende:

   15 public class User

   16 {

   17 }

   18 public interface UserRepository

   19 {

   20     void Insert(User userToInsert);

   21 }

   22 public class UserRegistrationController

   23 {

   24     private UserRepository _repository;

   25     private UserRegistrationView _view;

   26     public UserRegistrationController(

   27         UserRegistrationView view, UserRepository repository)

   28     {

   29         _repository = repository;

   30         _view = view;

   31         _view.RegistrationRequested += _view_RegistrationRequested;

   32     }

Prosjektet kompilerer igjen. Vår første test passerer fortsatt, men den nye testen gjør ikke det, så det får vi gjøre noe med. Vi må rett og slett sørge for at den nye brukeren lagres:

   33 private void _view_RegistrationRequested(object sender, EventArgs e)

   34 {

   35     _repository.Insert(new User());

   36     _view.SetRegistrationResult(true);

   37 }

Der, nå er begge testene grønne. Kontrolleren lagrer en ny bruker til repositorien, og sier fra til viewet at det er gjort. Men den mangler noe veldig sentralt. Trenger vi ikke noe mer input? Hvor er brukernavnet og passordet?

Har du noen gang programmert så mye adferd uten å lage en eneste public property? Er det ikke interessant hvordan TDD lar oss fokusere på adferd? Men nå er det kanskje på tide å legge til i alle fall brukernavn og passord. Men først må vi ha en test for dette. Den siste testen vi skrev verifiserer at brukeren lagres til basen. Hva om vi legger til å validere selve brukeren som legges til også – at den har riktig navn f.eks.? Jeg forsøker meg på dette ved å endre testen til dette her:

   48 [Test]

   49 public void Should_persist_user_in_repository()

   50 {

   51     User addedUser = null;

   52     mockRepository

   53         .Setup(r => r.Insert(It.IsAny<User>()))

   54         .Callback((User user) => addedUser = user);

   55     When_doing_a_registration();

   56     Assert.AreEqual(“Kong Håkon”, addedUser.Username);

   57 }

Jeg bruker litt Moq-magic til å sette opp en callback som tar vare på brukeren som sendes til repositorien, og sjekker at navnet på den er “Kong Håkon”. Men hvor har jeg fått dette navnet fra? Det skal jo brukeren sende inn.., via viewet selvfølgelig. Så vi setter opp dette også:

   50 [Test]

   51 public void Should_persist_user_in_repository()

   52 {

   53     mockView.SetupGet(v => v.Username).Returns(“Kong Håkon”);

   54     User addedUser = null;

   55     mockRepository

   56         .Setup(r => r.Insert(It.IsAny<User>()))

   57         .Callback((User user) => addedUser = user);

   58     When_doing_a_registration();

   59     Assert.AreEqual(“Kong Håkon”, addedUser.Username);

   60 }

Jeg forventer nå at viewet har en Username property, og at den vil returnere “Kong Håkon” for oss når den blir forespurt. Registreringsjobben skal da overføre dette navnet til brukeren som lagres. Peace of cake! Jeg oppretter de to propertiene jeg trenger for å få kompilert, kjører testen én gang for å se det røde lyset, implementerer litt mer logikk i kontrolleren, og har to grønne tester igjen.

PS: Jeg har forøvrig trukket ut litt logikk fra When_doing_a_registration() og plassert det i en SetUp-metode – hvis ikke hadde jeg fått en null-ref exception i linje 53.

   10 public interface UserRegistrationView

   11 {

   12     event EventHandler RegistrationRequested;

   13     string Username { get; }

   14     void SetRegistrationResult(bool success);

   15 }

   16 public class User

   17 {

   18     public string Username { get; set; }

   19 }

   35 private void _view_RegistrationRequested(object sender, EventArgs e)

   36 {

   37     _repository.Insert(new User

   38     {

   39         Username = _view.Username

   40     });

   41     _view.SetRegistrationResult(true);

   42 }

Passordkryptering

Nå kunne jeg gjort det samme for passord som for brukernavn, men oppgaven sa at jeg skulle kryptere passordet. Det er derfor viktig at passordet som lagres IKKE er det samme som sendes inn fra viewet. Jeg jobber litt til med testene:

   63 Assert.AreNotEqual(“passw0rd”, addedUser.Password);

Nei, dette føles ikke særlig riktig. Når jeg tenker over det er hashing av passord et helt annet ansvar, noe kontrolleren kan deligere til noen andre. Ved å utnytte det kan jeg skrive en enkel test som validerer at passordet hashes, mens jeg utsetter selve hash-logikken.

   77 [Test]

   78 public void Should_hash_password()

   79 {

   80     When_doing_a_registration();

   81     mockHasher.Verify(h => h.HashPassword(It.IsAny<string>()));

   82 }

Jeg har begynt å gå litt raskere frem nå, men jeg regner med at du klarer å henge med. Jeg har lagt til en ny avhengighet, PasswordHashService, som jeg injecter i controller-konstruktøren. Og så skriver jeg en ny test som verifiserer at HashPassword kalles. Noen sekunder senere får jeg dette til å kompilere og testen til å passere ved å gjøre følgende..

   39 private void _view_RegistrationRequested(object sender, EventArgs e)

   40 {

   41     _repository.Insert(new User

   42     {

   43         Username = _view.Username,

   44         Password = _hasher.HashPassword(_view.Password),

   45     });

   46     _view.SetRegistrationResult(true);

   47 }

Password settes i linje 44. Jeg bruker to ulike properties som ikke finnes enda.., så jeg oppretter dem. Done!

En konkret hasher

Vi har nå laget en fungerende controller, men ingen av de objektene den sammarbeider med eksisterer enda – de ble alle “mocket ut” i testene. Vi kan nå jobbe oss innover. Jeg velger å ta PasswordHashService først. Jeg skriver selvsagt tester før produksjonskode.., og her er den første:

   55 [TestFixture]

   56 public class MD5HasherTests

   57 {

   58     [Test]

   59     public void QuickBrownFoxTest()

   60     {

   61         Assert.AreEqual(

   62             “9e107d9d372bb6826bd81d3542a419d6″,

   63             new MD5Hasher().HashPassword(“The quick brown fox jumps over the lazy dog”));

   64     }

   65 }

Jeg fant hashen til denne setningen i Wikipedia’s artikkel om MD5, så jeg tenkte den kunne være et greit utgangspunkt for å teste algoritmen. Litt cut n’ paste fra nettet og prøving og feiling gir meg følgende implementasjon som passerer testen:

   53 public class MD5Hasher : PasswordHashService

   54 {

   55     public string HashPassword(string passwordToHash)

   56     {

   57         var passwordBytes = Encoding.UTF8.GetBytes(passwordToHash);

   58         var hashBytes = MD5.Create().ComputeHash(passwordBytes);

   59         var buffer = new StringBuilder();

   60         for (int i = 0; i < hashBytes.Length; i++)

   61             buffer.Append(hashBytes[i].ToString(“x2″).ToLower());

   62         return buffer.ToString();

   63     }

   64 }

En konkret repository

Neste interface jeg trenger en konkret implementasjon av er UserRepository. For å implementere den trenger jeg et persisteringsmedium, en eller annen form for database. Det enkleste jeg vet om er å bruke en objektdatabase, db4objects, så det vil jeg gjøre her. Jeg kan enkelt sette opp en test som oppretter en database, lagrer en bruker, kontrollerer at jeg kan hente brukeren ut igjen, og rydder opp ved å slette basen. Jeg gjør dette i små steg, men her er alt i en batch:

   44 [TestFixture]

   45 public class Db4oUserRepositoryTests

   46 {

   47     const string db_path = @”c:\temp\Db4oUserRepositoryTests.dbo”;

   48     private IObjectContainer database;

   49     private Db4oUserRepository repository;

   50 

   51     [SetUp]

   52     public void SetUp()

   53     {

   54         database = Db4oFactory.OpenFile(db_path);

   55         repository = new Db4oUserRepository(database);

   56     }

   57     [TearDown]

   58     public void TearDown()

   59     {

   60         database.Dispose();

   61         File.Delete(db_path);

   62     }

   63     [Test]

   64     public void Should_insert_user()

   65     {

   66         repository.Insert(new User { Username = “Foo” });

   67         Assert.AreEqual(

   68             “Foo”,

   69             (from User u in database select u).Single().Username);

   70     }

   71 }

Jeg vil ha en konkret UserRepository som jeg vil kalle Db4oUserRepository. Jeg vil sende en instans av en objektdatabase inn via konstruktøren (IObjectContainer er et db4objects interface). I setup-metoden oppretter jeg en ny filbasert database og selve repositorien, og i teardown disposer jeg databasen og sletter filen.

Selve testen lagrer en ny bruker i repositorien, og så bruker jeg Linq 2 Db4o for å hente ut alle brukere fra basen, sjekke at det finnes én-og-bare-én bruker (ved å kalle Single()), og kontrollere at brukernavnet er riktig. Vær oppmerksom på at du må legge til “using Db4objects.Db4o.Linq” for å få Linq-spørringen i linje 69 til å kompilere. Vil du vite mer om Db4objects kan du lese min blogpost om dette fantastiske rammeverket.

Følgende enkle implementasjon tilfredstiller testen over:

   27 public class Db4oUserRepository : UserRepository

   28 {

   29     private IObjectContainer _database;

   30     public Db4oUserRepository(IObjectContainer database)

   31     {

   32         _database = database;           

   33     }

   34     public void Insert(User userToInsert)

   35     {

   36         _database.Store(userToInsert);

   37     }

   38 }

Et konkret view

Det eneste som gjenstår nå for å ha en fungerende brukerregistrering er å lage et konkret view. Dette er den eneste biten jeg ikke bryr meg om å skrive tester for – det er somregel ikke verdt innsatsen, sålenge forretningslogikken er separert ut av viewet. Jeg oppretter en ny Web User Control, kaller den Registration.ascx, og dropper inn to TextBox’er og en Button.

    1 <%@ Control Language=”C#” AutoEventWireup=”true” CodeBehind=”Registration.ascx.cs”

    2     Inherits=”WebApplication1.Registration” %>

    3 User name: <asp:TextBox ID=”txtUsername” runat=”server”></asp:TextBox><br />

    4 Password: <asp:TextBox ID=”txtPassword” runat=”server”></asp:TextBox><br />

    5 <asp:Button ID=”createUserButton” runat=”server” Text=”Create user”

    6     onclick=”createUserButton_Click” />

Og her er code-behind, hvor jeg lar Registration arve fra UserRegistrationView:

   11 public partial class Registration : UserControl, UserRegistrationView

   12 {

   13     private UserRegistrationController _controller;

   14     protected void Page_Load(object sender, EventArgs e)

   15     {

   16         _controller = new UserRegistrationController(

   17             this,

   18             Application["UserRespoitory"] as UserRepository,

   19             new MD5Hasher());

   20     }

   21 

   22     // Inherited from UserRegistrationView

   23     public event EventHandler RegistrationRequested;

   24     public string Username { get { return txtUsername.Text; } }

   25     public string Password { get { return txtPassword.Text; } }

   26     public void SetRegistrationResult(bool success)

   27     {

   28         if (success)

   29         {

   30             createUserButton.Text = “Success!”;

   31             createUserButton.Enabled = false;

   32         }

   33     }

   34     // end UserRegistrationView Members

   35     protected void createUserButton_Click(object sender, EventArgs e)

   36     {

   37         if (RegistrationRequested != null)

   38             RegistrationRequested(this, e);

   39     }

   40 }

Denne approchen er opprinnelig inspiret av Phil Haack’s blogpost om Supervising Controller i ASP.NET. Initaliseringen av kontrolleren som foregår i Page_Load knytter sammen alle objektene som trengs for å registrere en bruker, og når brukeren fyrer av RegistrationRequested eventet ved å klikke på knappen vil controlleren ta over og gjennomføre registreringen.

Om du drar inn Registration-kontrollen i Default.aspx så kan du teste at det fungerer som det skal. Foreløpig får du tilbakemelding ved at knappen blir disablet og teksten sier “Success!”, men her kan man tenke seg at vi skjuler hele kontrollen og viser login-kontrollen (som fortsatt gjenstår å lage) i stedet.

Jeg har valgt å plassere repositorien i Application state – jeg vet ikke hvor bra dette fungerer i praksis, og det er ikke sånn jeg ville har gjort det “i virkeligheten”, men det fungerer greit her og nå. ASP.NET og state er ikke det viktige temaet i denne artikkelen, så ikke gi meg flak for denne fremgangsmåten. Initaliseringen skjer i Global.asax:

    6 public class Global : System.Web.HttpApplication

    7 {

    8     protected void Application_Start(object sender, EventArgs e)

    9     {

   10         var database = Db4oFactory.OpenFile(@”c:\temp\Db4oUserRepository.dbo”);

   11         Application["Db4oUserRepository.dbo"] = database;

   12         Application["UserRespoitory"] = new Db4oUserRepository(database);

   13     }

   14 

   15     protected void Application_End(object sender, EventArgs e)

   16     {

   17         var database = Application["Db4oUserRepository.dbo"] as IDisposable;

   18         database.Dispose();

   19     }

   20 }

Innlogging

For å implementere innlogging går jeg frem som jeg gjorde da jeg startet på brukerregistrering. Innlogging er et eget ansvar som trenger egne tester, eget view og egen controller. Controlleren vil bruke UserRepository, som man gjennom testene må utvide med logikk for å hente opp en bruker basert på navn. Den vil også gjenbruke PasswordHashService, fordi passordet må hashes før man kan sammenligne med det lagrede passordet.

Gjennom denne blogposten har du allerede sett og lært alt du trenger for å fullføre oppgaven, så jeg stopper her. Om du har lyst til å øve deg litt selv så anbefaler jeg at du fortsetter på egenhånd. Skriv tester først, og ikke lag en eneste klasse eller metode før du har kode (test eller annen kode) som allerede refererer klassen/metoden.

Jeg håper du likte denne TDD-tutorialen. I en ny blogpost som kommer veldig snart vil jeg analysere designet jeg har kommet opp med, og snakke litt om best practises rundt smidig arkitektur. På gjensyn!

Knagger: , , , , , , , , , ,

Ren kode

cleancode

Robert C. Martins Clean Code: A Handbook of Agile Software Craftsmanship er en viktig og spennende bok. Mange mener mye om hva ren kode er for noe, men det har vært vanskelig å finne konkrete og komplette kilder. Her har Uncle Bob samlet og forklart motivasjonen bak et godt sett med regler for hvordan man skriver kode med høy kvalitet i de små detaljene.

Så hvorfor skal vi skrive ren kode? Er det ikke nok at programvaren gjør det den skal? Jo, kanskje, hvis det er 100% bugfritt, og aldri skal endres på noen som helst måte. Og hvor sansynlig er det? Ufattelig mye energi brukes på å forstå og navigere i dårlig skrevet kode, og utviklingsteam som jobber i slike systemer er langt fra så effektive som de kunne ha vært. Ren kode kan endre dette ganske drastisk.

Boken har tre deler: Første del beskriver prinsipper og praksiser for å skrive kode som kommuniserer godt, og argumenterer for hvorfor en profesjonell utvikler skriver ren kode. Andre del inneholder en rekke studier av eksisterende kode, som Uncle Bob fikser på for å gjøre den bedre. Og siste del er mer eller mindre en liste over “regler” som ren kode ifølge forfatteren skal følge.

Clean Code er en bok som fungerer både for ferske og erfarne utviklere. Folk med flere års erfaring vil tenke at de kjenner alle disse prinsippene fra før, men min erfaring tilsier at de fleset av oss ikke følger så mange av dem likevel. Alle har godt av å minnes på hva ren kode er for noe, og hvor viktig det er – og tradisjonelt har det vært alt for lite fokus på dette i bransjen. Får du hele teamet ditt til å lese boken, og så følge prinsippene i den, vil kvaliteten på det dere produserer garantert øke, noe dere vil nyte godt av i hele levetiden til produktet/produktene.

Uncle Bob advarer at boken er krevende, men jeg synes i grunnen den var ganske lettlest. Den inneholder ganske mye kode, men det leser vi jo hver dag likevel. Han har derimot rett i at det vil kreve noe av deg å lære deg å følge prinsippene i boken – det holder ikke bare å lese den, for så å gjøre som du vanligvis gjør. Du må øve på disse tingene, og praktisere dem hver dag.

Boken vil ikke lære deg alt det du trenger for å lage bra software – langt der i fra – men er en fin liten byggekloss på veien til å bli en Drivende Dyktig Utvikler. Jeg ble litt skuffet over noen av de refaktureringene Bob gjorde i del 2, jeg tror koden kunne ha blitt enda renere. Men jeg anbefaler gjerne denne boken likevel!

PS: Kodeekksemplene i boken er hentet fra Java, men det bør ikke være noe problem for en seriøs utvikler, samme hvilket språk du jobber i til vanlig – og spesielt ikke om du er en C#-utvikler som meg. Prinsippene Uncle Bob presenterer er universelle.

Knagger: , , ,

Dekorer klassene dine med funksjonalitet

CropperCapture[18]

Decorator pattern er et objektorientert designmønster som lar deg legge ny funksjonalitet til et eksisterende objekt uten å modifisere det. Funksjonalitet implementert som decorators kan “plugges inn” dynamisk når man måtte ønske det, og holder grunn-klassen fri for forstyrrende kode.

I denne artikkelen illustrerer jeg et tenkt tilfelle hvor vi blir bedt om å legge til ny funksjonalitet i et eksisterende system, og hvor vi kan velge å gjøre det enkelt – noe som kan føre til større problemer på sikt – eller vi kan velge å bruke decorators, som vil la oss beholde en ren og fleksibel løsning. Jeg viser hvordan jeg dekorerer den eksisterende klassen med ny funkjonalitet, og snakker til slutt litt om motivasjonen for å gjøre det på denne måten.

I det tenkte systemet vårt har vi et abstrakt begrep vi kaller for en Service. Du kan tenke på det som en typisk windows service. For å administrere en service har vi laget en klasse vi kaller ServiceRunner – den har metoder for å starte, stoppe, pause og re-starte en service.

    1     public class ServiceRunner

    2     {

    3         private Service _service;

    4         public ServiceRunner(Service service)

    5         {

    6             _service = service;           

    7         }

    8         public void Start()

    9         {

   10             if (_service.State == ServiceState.Suspended)

   11                 _service.Resume();

   12             else

   13                 _service.Run();

   14         }

   15         public void Stop() { /* details omitted */ }

   16         public void Pause() { /* details omitted */ }

   17         public void Restart() { /* details omitted */ }

   18     }

ServiceRunner skjuler brukerne av koden vår for alle detaljene som inngår i de fire vanlige operasjonene vi ønsker å tilby. Implementasjonen ev Start() er vist over. Den er ikke spesielt kompleks, men det kunne tenkes at Service hadde et grensesnitt som det var vanskeligere å forholde seg til, og du aner ingenting om hva som skjuler seg bak de andre metodene – koden der kan godt være mere innviklet.

Systemet vårt fungerer ypperlig. Servicer starter og stopper mer eller mindre som de skal. Men så får vi inn et nytt krav fra kunden: Systemet skal logge hver gang en service starter, stopper, pauses eller re-startes.

Ok, det var jo ikke så vanskelig. Vi har allerede laget et abstrakt konsept vi har kalt Logger, som i praksis kan skrive til event-loggen, databasen, en logfil o.l. Vi kunne latt koden som kalte de ulike metoden på ServiceRunner logge før kallene, men hvordan skulle vi håndheve det? Nye klienter av ServiceRunner kan komme til, og da måtte vi ha duplisert kallene til Logger. Nei, vi kan gjøre noe mye enklere, Vi kan gi ServiceRunner tilgang til en Logger (gjennom dependency injection selvsagt), og implementere logging i hver av de fire metodene – illustrert her ved logging av Start i linje 12 nedenfor:

    1     public class ServiceRunner

    2     {

    3         private Logger _logger;

    4         private Service _service;

    5         public ServiceRunner(Service service, Logger logger)

    6         {

    7             _logger = logger;

    8             _service = service;

    9         }

   10         public void Start()

   11         {

   12             _logger.Log(String.Format(“Starting service {0}”, _service.Name));

   13             if (_service.State == ServiceState.Suspended)

   14                 _service.Resume();

   15             else

   16                 _service.Run();

   17         }

   18         /* Stop, Pause, and Restart omitted */

   19     }

Supert, dette fungerte som en drøm. Vi måtte riktignok endre alle steder ServiceRunner opprettes (vi burde ha innført en factory, slik at det kun var et sted konstruktøren til ServiceRunner ble kalt), men det var en smal sak. Nå vil alle operasjoner på en Service bli logget.

Like etter at vi releaser den nye ServiceRunneren får vi inn et nytt ønske. Brukere av systemet har ulike rettigheter, og ikke alle har lov til å starte en gitt service, stoppe en gitt service etc. Ok, vi bestemmer oss for å bruke mer eller mindre samme teknikk; vi implementerer et ServiceAuthorization objekt som vi kan spørre om den påloggede brukeren har lov til å foreta bestemte operasjoner på servicen. ServiceAuthorization sendes inn i konstruktøren, og brukes i hver av metodene, som i linje 15 nedenfor:

    1     public class ServiceRunner

    2     {

    3         private Logger _logger;

    4         private ServiceAuthorization _authorization;

    5         private Service _service;

    6         public ServiceRunner(Service service, Logger logger,

    7             ServiceAuthorization authorization)

    8         {

    9             _authorization = authorization;

   10             _logger = logger;

   11             _service = service;

   12         }

   13         public void Start()

   14         {

   15             if (!_authorization.CanStart(_service))

   16                 throw new NotAllowedToStartServiceException(_service);

   17 

   18             _logger.Log(String.Format(“Starting service {0}”, _service.Name));

   19             if (_service.State == ServiceState.Suspended)

   20                 _service.Resume();

   21             else

   22                 _service.Run();

   23         }

   24         /* Stop, Pause, and Restart omitted */

   25     }

Ser dere hva som skjer her? En veldig enkel klasse begynner å vokse etterhvert som vi trenger mer funksjonalitet. Denne naive måten å legge til adferd på fører til prosedyrebasert kode som blir vanskeligere og vanskeligere å holde orden på. Klassen får flere og flere dependencies til andre deler av systemet. Dette er begynnelsen på noe som om kort tid vil bli omtalt som et legacy-system av utviklerne som må vedlikeholde det.

Problemet er at vi må endre ServiceRunner hver gang vi skal legge til noe nytt som har med kjøring av servicer å gjøre. Vi bryter dermed med Open/Closed-prinsippet innenfor objektorientert design. Det er også vanskelig å argumentere for at ServiceRunner tilfredstiller Single Responsibility Principle – om vi vil endre hva vi logger så må vi endre ServiceRunner, om vi vil endre autorisasjonsprosessen må vi endre ServiceRunner, og om måten en Service fungerer på endres må vi også endre ServiceRunner.

Decorator Pattern

Decorator pattern er en alternativ løsning vi kan bruke for å implementere den nødvendige loggingen, autoriseringen, og en rekke andre, nye krav som måtte komme til. Første steg er å bruke ReSharper, Refactor! Pro eller manuell refakturering til å trekke ut interfacet fra den konkrete ServiceRunner-klassen. Dette vil gjøre det enklere å implementere dekoratørene.

Jeg velger å gi interfacet navnet ServiceRunner, mens jeg endrer den konkrete implementasjonen til å hete ServiceRunnerImplementor (alternativt kan du kalle interfacet for IServiceRunner og den konkrete klassen for ServiceRunner – en konvensjon du sikkert er mer komfortabel med). Med min metode behøver vi ikke endre alle eksisterende referanser til ServiceRunner – de vil nå referere interfacet, som er det vi ønsker.

Jeg har også bestemt at ServiceRunner skal ha en Service property som eksponerer instansen gitt gjennom konstructøren. Grunnen vil bli åpenbar om litt.

    1     public interface ServiceRunner

    2     {

    3         Service Service { get; }

    4         void Start();

    5         void Stop();

    6         void Pause();

    7         void Restart();

    8     }

    9 

   10     // Original ServiceRunner is renamed…

   11     public class ServiceRunnerImplementor : ServiceRunner

Steg 2 er å lage en abstrakt klasse som arver fra ServiceRunner: ServiceRunnerDecorator. Dette blir baseklassen for alle de ulike funksjonene vi vil dekorere ServiceRunner med. ServiceRunnerDecorator tar inn en instans av en ServiceRunner i konstruktøren, og delegerer så alle kall til denne instansen. Den abstrakte dekoratøren er altså ikke annet enn en enkel wrapper rundt en ServiceRunner.

    1     public abstract class ServiceRunnerDecorator : ServiceRunner

    2     {

    3         private ServiceRunner _runner;

    4         public ServiceRunnerDecorator(ServiceRunner runner) {

    5             _runner = runner;           

    6         }

    7         public Service Service

    8         {

    9             get { return _runner.Service; }

   10         }

   11         public virtual void Start() { _runner.Start(); }

   12         public virtual void Stop() { _runner.Stop(); }

   13         public virtual void Pause() { _runner.Pause(); }

   14         public virtual void Restart() { _runner.Restart(); }

   15     }

Og nå kan vi implementere logge-funksjonaliteten. Vi lager er konkret ServiceRunnerDecorator som vi kaller for LoggingServiceRunner. I tillegg til å ta inn en instans av en ServiceRunner trenger den selvsagt også en Logger. I Start-metoden kan vi gjøre loggingen på samme måte som vi tidligere gjorde i den orginale ServiceRunner’en. Deretter delegeres resten av start-logikken til baseklassen, som til syvende og sist kaller Start-metoden på den orginale klassen.

    1     public class LoggingServiceRunner : ServiceRunnerDecorator

    2     {

    3         private Logger _logger;

    4         public LoggingServiceRunner(ServiceRunner runner, Logger logger)

    5             : base(runner) {

    6             _logger = logger;

    7         }

    8         public override void Start()

    9         {

   10             _logger.Log(String.Format(“Starting service {0}”, Service.Name));

   11             base.Start();

   12         }

   13         /* Logging for other methods omitted */

   14     }

Nå ser du også grunnen til at jeg ønsket en Service property i ServiceRunner, slik at dekoratørene kan få tak Servicen de skal gjøre noe med (i dette tilfelle logges navnet).

Videre kan vi implementere en ny dekoratør som tar seg av å sjekke om brukeren har lov til å bruke tjenesten. Jeg kaller den StrictServiceRunner:

    1     public class StrictServiceRunner : ServiceRunnerDecorator

    2     {

    3         private ServiceAuthorization _authorization;

    4         public StrictServiceRunner(ServiceRunner runner,

    5             ServiceAuthorization authorization) : base(runner)

    6         {

    7             _authorization = authorization;

    8         }

    9         public override void Start()

   10         {

   11             if (!_authorization.CanStart(Service))

   12                 throw new NotAllowedToStartServiceException(Service);

   13             base.Start();

   14         }

   15         /* Authorization for other methods omitted */

   16     }

Som du ser har hver av klassene vi nå står igjen med hvert sitt ansvar: StrictServiceRunner handler kun om autorisasjon, LoggingServiceRunner handler kun om logging, ServiceRunnerImplementor bryr seg kun om å utføre de nødvendige operasjonene på Service-instansen. Hver av disse klassene kan så komponeres sammen, slik at når brukeren kaller Start() på sin instans av ServiceRunner så kalles Start-metoden etter tur i alle klassene, som nå utgjør en “pipeline” med relatert funksjonalitet. Du kan se for deg hvordan dekoratørene pakker inn den orginale ServiceRunner’en lag for lag, som en løk:

DecoratorOnion

Nå bør vi til slutt implementere en factory for å opprette ServiceRunnere, slik som jeg hintet til helt i starten. Gitt en Service kan vi komponere en ServiceRunner med ønsket funksjonalitet (eller “dekor” om du vil).

    1         public ServiceRunner CreateServiceRunner(Service service)

    2         {

    3             return

    4                 new StrictServiceRunner(

    5                     new LoggingServiceRunner(

    6                         new ServiceRunnerImplementor(service)

    7                         , _eventLogger)

    8                     , _userServiceAuthorization);

    9         }

Eller vi kan går for en mer dynamisk factory som komponerer ServiceRunnere basert på konfigurasjon, slik som dette:

    1         public ServiceRunner CreateServiceRunner(Service service)

    2         {

    3             ServiceRunner runner = new ServiceRunnerImplementor(service);

    4 

    5             if (_configuration.Service_ACL_Enabled)

    6                 runner = new StrictServiceRunner(runner, GetServiceAuthorizationService());

    7 

    8             if (_configuration.Should_log_to_EventLog)

    9                 runner = new LoggingServiceRunner(runner, GetEventLogLogger());

   10 

   11             if (_configuration.Should_log_to_DataBase)

   12                 runner = new LoggingServiceRunner(runner, GetDataBaseLogger());

   13 

   14             //TODO: add additional decorators..

   15 

   16             return runner;

   17         }

Legg merke til hvordan jeg her potensielt legger til to LoggingServiceRunnere, en for logging til eventloggen i linje 9, og en for logging til databasen i linje 12.

Når skal man bruke decorators?

I Working Effectively with Legacy Code (kapittel 6: “I Don’t Have Much Time and I Have to Change It”) snakker Michael Feathers om hvordan Decorator pattern kan være et nyttig hjelpemiddel når man skal implementere ny funksjonalitet i et komplisert system man ikke er trygg på. La oss tenke oss at den orginale ServiceRunner-klassen er mye større, og allerede har mange avhengigheter som gjør det svært vanskelig å lage enhetstester for den. Vi ønsker å være trygg på den nye funksjonaliteten vi skal implementere, og vil derfor skrive tester, men det koster rett og slett for mye å få ServiceRunner inn i et testing harness.

Da kan man velge å implementere den nye funksjonaliteten som en dekoratør. Den kan testes uavhengig av ServiceRunner. ServiceRunner er i seg selv fortsatt Legacy Code, men alt nytt vi legger til er enhetstestet og til å stole på. Dette er ikke ønskedrømmen – aller helst burde vi klare å få testet ServiceRunner også – men noen ganger må man ta snarveier, og da er Decorator pattern et bra verktøy.

Robert C. Martin forteller også om Decorator i boken Agile Principles, Patterns, and Practices in C#, hvor han klassifiserer mønsteret sammen med Visitor pattern og Extension pattern. Han har følgende å si om hvordan disse teknikkene støtter opp under gode OO-prinsipper:

“The Visitor family of patterns provides us with a number of ways to modify the behaviour of a hierarchy of classes without having to change them. Thus, they help us maintain the Open/Closed Prinsiple. They also provide mechanisms for segregating various kinds of funktionality, keeping classes from getting cluttered with many different functions. As such, they help us maintain the Common Closure Prinsiple. It should be clear that LSP [Liskov's Substitution Principle] and DIP [Dependency Invertion Principle] are also applied to the structure of the Visitor family.”

Uncle Bob gir videre følgende råd om bruken av Visitor, Decorator og Extension:

“The Visitor patterns are seductive. It is easy to get carried away with them. Use them when they help, but maintain a healthy skepticism about their necessity. Often, something that can be solved with a Visitor can also be solved by something simpler.”

For mer informasjon anbefaler jeg at du tar en titt på Decorator Pattern på wikipedia, og så finner du en video kalt Learning the Decorator Pattern som er under 10 minutter lang på DimeCasts.NET.

Knagger: , , , , , , , ,

null-referanser

NullReferenceExceptionPå flytoget etter NDC 2009 hadde mine kollegaer og jeg en nokså heftig diskusjon om bruk av null. Det var et nokså uskyldig spørsmål som startet det hele:

“Sjekker du alltid om parametrene som sendes inn til en metode er null, og kaster ArgumentNullException om så er tilfelle?”

Mitt svar var ganske bestemt: Nei, det gjør jeg stort sett aldri! For meg er det å sjekke for null noe man gjør i frykt, noe man gjør for å kompansere for dårlig kode. I de fleste tilfeller er det en bug å sende inn null, og jeg ønsker ikke å legge til rette for sloppy kode ved å sette på støttehjulene og teste for dette.

Kode som forventer en instans men får null vil kaste en NullReferenceException, som er mer enn godt nok for kode som er godt skrevet – den energien en eventuelt putter inn i å lage mer detaljerte feilmeldinger er ikke verdt det i dette tilfellet.

Videre hevdet jeg at null i seg selv er en uting. Kode som er skrevet slik at man gjentatte ganger må teste for null er ikke god kode. Å teste for null er en code smell. Bruk av null hører ikke hjemme i kode som befinner seg andre steder enn på det laveste abstraksjonsnivået – null er ikke noe forretningsfolk kan snakke om, og bør derfor etter min mening heller ikke eksistere i kode som beskriver forretningskonsepter.

Null Object pattern

Problemet jeg snakker om kan illustreres med følgende kodesnutt:

Employee e = DB.GetEmployee(“T-Man”);

if (e != null && e.IsTimeToPay(DateTime.Today))

    e.Pay();

Siden databasen kan returnere null om “T-Man” ikke eksisterer benytter vi oss av det faktum at det første uttrykket i if-testen evalueres først, og det andre uttrykket kun om det første er sant. Vi har alle skrevet kode som dette her. Vi har også alle glemt å teste for null fra tid til annen – selv om dette er et vanlig mønster så er det både uelegant og noe som fører til mange feil.

I stedet for å returnere en null-referanse kunne vi la databasen kaste en exception, men da måtte vi ha brukt try/catch ved alle kall, noe som er enda mindre elegang.

Et alternativ til å bruke null-referanser er å implementere et objekt som har det forventede grensesnittet, men som ikke gjør noen ting. Dette kaller vi for Null Object. Min kollega hevdet at dette er veldig tungvindt å implementere, men etter å ha tenkt meg litt om så finner jeg mange steder hvor jeg har brukt det selv, og hvor det er langt fra vanskelig å implementere, samtidig som det gjør koden som bruker objektene langt enklere.

Her er et eksempel fra prosjektet jeg holder på med akkurat nå. Brukeren skriver inn noe tekst som blir gjort om til en UserCommand. Poenget er å unngå å returnere null, så hvis teksten som sendes inn er gibberish så returnerer jeg en UnknownCommand:

public UserCommand GetCommand(FirstPerson person, string[] commandArguments)

{

    if (ValidCommandArguments(commandArguments))

    {

        return _commandDictionary 

            .GetParser(commandArguments[0])

            .GetCommand(person, commandArguments);

    }

    return new UnknownCommand();

}

UnknownCommand er et null-objekt: Det implementerer UserCommand-interfacet, men gjør ingen ting. Klienter av UserCommand kan bruke dette objektet på samme måte som alle andre commands – jeg trenger ikke kaste noen exceptions, og aldri teste for null.

public class UnknownCommand : UserCommand

{

    public UserCommandResult Execute()

    {

        return new UserCommandResult

        {

            Success = false,

            Text = “Did not compute!”,

        };

    }

}

Jeg har flere eksempler som ligner på dette. Der hvor jeg bruker strategy pattern i kombinasjon med en eller annen form for en factory, implementerer jeg nesten alltid også et null-objekt, en dummy-klasse som kan brukes i stedet for en virkelig strategi.

Dette beviser ikke at null objects kan brukes over alt, ei heller at det er trivielt, men jeg er ganske sikker på at bruk av dette mønsteret vil føre til kode som er enklere å lese og vedlikeholde, og som er mer feilfri.

Kan vi erstatte != null testen?

Men det er ikke til å komme bort ifra at man av og til må bruke null-referanser. Det er også et faktum at jeg hater å skrive != og == null. Jeg ønsker derfor en mer deskriptiv måte å teste på – en som ikke bryter abstraksjonsnivået. Dette er hva jeg har kommet frem til:

[Test]

public void Test()

{

    object o = null;

 

    if (o.IsNotSetYet())

        Assert.That(o == null);

        //Cool, I called a method on a null reference

    else

        Assert.Fail(“o was not null”);          

}

Denne testen feiler ikke! Men hvordan er det mulig å kalle en metode på en null-referanse? Jo, det er mulig takket være extension methods:

public static class ObjectExtensions

{

    public static bool IsNotSetYet(this object o)

    {

        return o == null;

    }

    public static bool IsSet(this object o)

    {

        return o != null;

    }

}

Er det ikke vakkert?

En generisk state machine

TurnstileFinite state machines (FSM), tilstandsmaskiner på norsk, er blant de mest kraftige abstraksjonene vi programmerere har tilgjengelig, og har et stort og variert bruksområde. FSM gir oss en enkel og elegant måte å utforske og definere oppførselen til komplekse systemer, og implementasjonen er både enkel å fortså og enkel å modifisere. I boken sin Agile Principles, Patterns, and Practises in C# sier Robert C. Martin følgende om dette:

“Finite state machines are underutilized. In many scenarios their use would help to create cleaner, simpler, more flexible, and more accurate code.”

I den enkleste formen er en state machine gjerne implementert som en eller flere switch/case statements – i mer komplekse løsninger kan man bruke STATE pattern. I .NET 3.0 fikk vi også Windows Workflow Foundation, hvor vi kan designe State Machine Workflows.

Men alle disse alternativene er mere komplekse enn å lage en FSM basert på en overgangstabell. I denne artikkelen designer jeg en generisk FSM-motor som kan gjenbrukes i mange scenarier. For å illustrere hvordan den virker vil jeg implementere tilstandsmaskinen som finnes i en typisk passeringsautomat – slike som man finner på T-banestasjoner, flytoget osv.

I diagrammet under ser du tilstandene (states), overgangene (transitions), hendelsene som kan inntreffe (transition conditions) og det systemet skal utføre når det går fra en tilstand til en annen (actions) i en slik passeringsautomat.

FSM_turnstile

Ta for eksempel en automat som i utgangspunktet er låst. Hvis noen putter en mynt på automaten, vil systemet kalle en rutine for å låse opp porten, og endre statusen til å være ulåst. Når den så registrerer at noen passerer porten, vil den kalle rutinen for å låse porten, og returnere til den låste tilstanden. Hvis noen passerer mens systemet er i låst tilstand, vil automaten kalle alarm-rutinen og forbli i låst tilstand.

Skulle noen finne på å putte på penger mens systemet er ulåst, vil automaten si “tusen takk”, og forbli i ulåst tilstand.

Det første jeg vil gjøre er å opprette et par enumerasjoner som beskriver systemets tilstander og hendelser:

    9 public enum State { LOCKED, UNLOCKED }

   10 public enum Event { COIN, PASS }

Jeg utviklet selvsagt automaten vha. TDD, og her følger oppsettet av testklassen, som viser hvordan en StateMashine opprettes og konfigureres:

    9 using StateMachine = Marosoft.Generics.StateMachine<State, Event>;

   10 

   11 [TestFixture]

   12 public class TestGenericStateMachine

   13 {

   14     private StateMachine _stateMachine;

   15 

   16     static bool _unlockCalled = false;

   17     static bool _alarmCalled = false;

   18     static bool _thankYouCalled = false;

   19     static bool _lockActionCalled = false;

   20 

   21     private Action unlock = () => _unlockCalled = true;

   22     private Action alarm = () => _alarmCalled = true;

   23     private Action thankYou = () => _thankYouCalled = true;

   24     private Action lockAction = () => _lockActionCalled = true;

   25 

   26     [SetUp]

   27     public void SetUp()

   28     {

   29         _stateMachine = new StateMachine(State.LOCKED);

   30         // Equivalent to generic specification due to using statement

   31 

   32         _stateMachine.AddTransition(State.LOCKED, Event.COIN, State.UNLOCKED, unlock);

   33         _stateMachine.AddTransition(State.LOCKED, Event.PASS, State.LOCKED, alarm);

   34         _stateMachine.AddTransition(State.UNLOCKED, Event.COIN, State.UNLOCKED, thankYou);

   35         _stateMachine.AddTransition(State.UNLOCKED, Event.PASS, State.LOCKED, lockAction);

   36     }

Legg merke til det spesielle using direktivet på linje 9. Dette er ikke et krav, men lar meg definere en StateMachine-variabel på linje 14 og initiere den på linje 29 uten å spesifisere hvilke enumerasjoner som skal brukes. Dette er et generelt tips om hvordan man kan gjøre kode som bruker mye generics mer lesbar.

Linje 16 til 24 setter opp fire dummy-actions som skal brukes når jeg tester tilstandsmaskinen. Det mest interessante kommer først på linje 32, hvor jeg begynner å definere tilstandsovergangene.

Du kan lese linje 32 på følgende måte: Hvis nåværende tilstand er LOCKED, og noen putter på en COIN, så skal tilstanden endres til UNLOCKED, og “unlock” skal kjøres.

Den første testen jeg trenger sjekker bare den initielle tilstanden, som jeg satte i konstruktøren. Testen er ikke mer enn én linje:

   40 [Test]

   41 public void InitialConditions()

   42 {

   43     Assert.AreEqual(State.LOCKED, _stateMachine.State);

   44 }

Deretter skriver jeg tester for hver av de fire mulige overgangene. I hver test setter jeg først gjeldende status, noe som bare er aktuelt i enhetstestene – i virkelig bruk vil man ikke overstyre status på den måten. Deretter sender jeg inn et event til tilstandsmaskinen, før jeg sjekker ny status og om riktig action har blitt kalt (ref oppsettet av test-klassen). Her er testene:

   46 [Test]

   47 public void CoinInLockedState()

   48 {

   49     _stateMachine.State = State.LOCKED;

   50     _stateMachine.HandleEvent(Event.COIN);

   51     Assert.AreEqual(State.UNLOCKED, _stateMachine.State);

   52     Assert.IsTrue(_unlockCalled);

   53 }

   54 [Test]

   55 public void CoinInUnlockedState()

   56 {

   57     _stateMachine.State = State.UNLOCKED;

   58     _stateMachine.HandleEvent(Event.COIN);

   59     Assert.AreEqual(State.UNLOCKED, _stateMachine.State);

   60     Assert.IsTrue(_thankYouCalled);

   61 }

   62 [Test]

   63 public void PassInLockedState()

   64 {

   65     _stateMachine.State = State.LOCKED;

   66     _stateMachine.HandleEvent(Event.PASS);

   67     Assert.AreEqual(State.LOCKED, _stateMachine.State);

   68     Assert.IsTrue(_alarmCalled);

   69 }

   70 [Test]

   71 public void PassInUnlockedState()

   72 {

   73     _stateMachine.State = State.UNLOCKED;

   74     _stateMachine.HandleEvent(Event.PASS);

   75     Assert.AreEqual(State.LOCKED, _stateMachine.State);

   76     Assert.IsTrue(_lockActionCalled);

   77 }

Nå bør du skjønne hvordan StateMachine skal brukes, og jeg er klar for å vise hvordan den er implementert. Her er første bit; StateMachine har en generisk definisjon som forventer to typer som skal representere henholdsvis statuser og eventer: Jeg har kalt dem TState og TEvent. Construktøren tar inn initiell status:

    9 public class StateMachine<TState, TEvent>

   10 {

   11     ///

   12     /// Setter is left internal for testing purposes

   13     ///

   14     public TState State { get; internal set; }

   15 

   16     public StateMachine(TState initialState)

   17     {

   18         State = initialState;

   19     }

Videre inneholder klassen en liste med overganger (linje 21). En overgang – Transition – er en intern klasse i StateMachine (linje 23). Det er ikke så ofte jeg bruker private klasser i andre klasser, men her passet det fint. AddTransition-metoden (linje 31) som jeg brukte i SetUp i testklassen oppretter en ny Transition og legger den til i listen.

   21 private List<Transition> _transitions = new List<Transition>();

   22 

   23 private class Transition

   24 {

   25     public TState StartState;

   26     public TEvent Trigger;

   27     public TState EndState;

   28     public Action Action;

   29 }

   30 

   31 public void AddTransition( TState startState,

   32                             TEvent trigger,

   33                             TState endState,

   34                             Action action)

   35 {

   36     _transitions.Add(new Transition

   37     {

   38         StartState = startState,

   39         Trigger = trigger,

   40         EndState = endState,

   41         Action = action,

   42     });

   43 }

Og da gjenstår bare HandleEvent-metoden, som er gjengitt nedenfor. Den finner riktig overgang basert på event-parameteren og gjenldende status. Til dette bruker jeg Find-metoden til List. Hvis en overgang finnes, brukes den til å sette ny status, og riktig action kalles (linje 55 og 56).

Hvis et ugyldig event sendes inn for gitt status (ikke mulig for denne passeringsautomaten, men kan tenkes for andre tilstandsmaskiner), så kaster jeg et ArgumentException.

   45 public void HandleEvent(TEvent e)

   46 {

   47     var transitionToPerform

   48         = _transitions.Find((transition)

   49         => transition.StartState.Equals(State) && transition.Trigger.Equals(e));

   50 

   51     if (transitionToPerform == null)

   52         throw new ArgumentException(

   53             string.Format(“Can not handle event {0} in state {1}.”, e, State));

   54 

   55     State = transitionToPerform.EndState;

   56     transitionToPerform.Action.Invoke();

   57 }

Min StateMachine er en forbedring av Robert C. Martins Transition Tables-eksempel fra boken jeg refererte. Ved å bruke generics har jeg gjort den gjenbrukbar. Når du trenger en tilstandsmaskin behøver du bare definere tilstandene og eventene, og konfigurere StateMachine med de riktige overgangene.

Det er også verdt å merke seg at du ikke er begrenset til å bruke enumerasjoner til å definere tilstander og/eller eventer; du kan like gjenre bruke klasser, slik som jeg har gjort her:

   12 public abstract class TurnstileState

   13 {

   14     public override bool Equals(object obj)

   15     {

   16         return base.GetType().Equals(obj.GetType());

   17     }

   18     public override int GetHashCode()

   19     {

   20         return base.GetType().GetHashCode();

   21     }

   22 }

   23 public class LockedState : TurnstileState { }

   24 public class UnlockedState : TurnstileState { }

Den abstrakte klassen TurnstileState (turnstile er engelsk for passeringsautomat) definerer et interface (et tomt et sådan) for en tilstand. LockedState og UnlockedState arver fra TurnstileState, og erstatter Event.LOCKED og Event.UNLOCKED. Jeg har overstyrt Equals fordi HandleEvent bruker Equals til å finne riktig overgang (merk at man også alltid skal overstyre GetHashCode når man overstyrer Equals).

Flytende konfigurering..

Til slutt gjorde jeg en liten forbedring i hvordan jeg oppretter overgangene. Flytende interfacer er veldig populært for tiden, og kan gjøre kode mer lesbar. Sammenlign SetUp-metoden nedenfor med den jeg presenterte tidligere, og se om du er enig i at dette er bedre.

   24 [SetUp]

   25 public void SetUp()

   26 {

   27     _stateMachine = new StateMachine<State, Event>(State.LOCKED);

   28 

   29     _stateMachine.Configure()

   30         .Given(State.LOCKED)

   31             .When(Event.COIN)

   32                 .ThenSetState(State.UNLOCKED).AndRun(unlock)

   33             .When(Event.PASS)

   34                 .ThenSetState(State.LOCKED).AndRun(alarm)

   35         .Given(State.UNLOCKED)

   36             .When(Event.COIN)

   37                 .ThenSetState(State.UNLOCKED).AndRun(thankYou)

   38             .When(Event.PASS)

   39                 .ThenSetState(State.LOCKED).AndRun(lockAction);

   40 }

Da jeg implementerte det som trengtes for å få denne setup-metoden til å kjøre oppdaget jeg noe ganske morsomt. Konfigurerings-objektet jeg opprettet er faktisk en tilstandmaskin – en Finite State Machine. Begynner du først å tenke på denne måten så oppdager du stadig nye tilfeller hvor FSM-teknikken kan benyttes.

Tilstandsmaskiner kan gjøre koden din enklere å forstå, vedlikeholde og endre, og bør ikke undervurderes. De fjerner “spagetti”, og lar dine øvrige klasser konsentrere seg om sine primære ansvar. Tenk gjennom hvilke tilstander koden din kan ha neste gang du programmerer, og vurder å bruk en tilstandsmaskin for å forenkle koden.

Knagger: , ,

Min nye bibel innen Smidig Design

Agile Principles, Patterns, and Practices in C#Robert C. Martin’s Agile Principles, Patterns, and Practices in C# er DEN BESTE programmeringsboken jeg noen gang har lest!!!

Det er ikke så ofte man kan begynne en blogpost med en setning som det der, og bruke både all-CAPS og tre utropstegn uten å få en flau smak i munnen. Men denne boken fortjener det virkelig. Den gir deg både lyst og kunnskapen du behøver for å omforme deg selv til en ideell, smidig systemutvikler.

Boken er full av anekdoter som virkelig får deg til å fortså hva som er galt med mange prosesser i dag, og hva som kan gjøres bedre. Og den utgjør bortimot et komplett sett med teknologi-uavhengig kunnskap om hvordan du skal gå fra en diffus problemstilling til et fungerende produkt.

Section I: Agile development

Den første delen av boken handler om smidig utvikling. Bob presenterer hovedtrekkene i eXtreme Programming, og går spesielt i dybden på planlgging, testing og refakturering. Han presenterer også en veldig bra historie som viser hvordan parprogrammering kan fungere i praksis:

“In order to demonstrate XP practices, Bob Koss and Bob Martin will pair program a simple application while you watch like a fly on the wall. We will use test first design and a lot of refactoring to create our application. What follows is a faithful re-enactment of a programming episode that the two Bob’s actually did.”

Historien er også tilgjengelig online, riktignok implementert med Java-kode – du kan lese den her.

Section II: Agile design

Her begynner boken virkelig å ta av. Bob snakker først om hvorfor design “rotner” over tid, og bruker resten av del 2 til å forklare hvordan man skal unngå at dette skjer. Han går i dybden på en rekke prinsipper innen objektorientert design, de såkalte SOLID prinsippene, og selv om jeg hadde hørt og lest en god del om dem fra før var det først med denne boken at jeg virkelig begynte å forstå.

Han forklarer hvorfor prinsippene er viktige, hvordan man skal kode for å innfri dem, og ikke minst hvilken tradeoff det innebærer å følge dem. Dette siste inpirerte meg til å skrive en liten blogpost jeg kalte når skal man følge SOLID prinsippene.

Bob bruker også mye av del 2 til å snakke om UML, og bruk av design-diagrammer innenfor smidig utvikling. Mange er av den oppfatning at modellering er noe man ikke skal gjøre når man er agile, men det kan være både feil og farlig. Bob lærer deg all den UML’en som er nødvendig for en smidig utvikler, og mener mye om hvordan man skal bruke dette “verktøyet”.

Section III: The Payroll case study

I del 3 demonstrerer Bob hvordan man går fra noen diffuse krav til et komplett program. Han bruker TDD, og viser deg all koden han produserer – inkludert testene. Han forteller grundig hvordan han tenker, og vi får se hvordan han følger OO-prinsippene og hvordan koden utvikler seg over tid.

I denne delen presenteres et hav av design patterns / mønstre, den mest forståelige forklaringen av design patterns jeg har vært borti faktisk, og vi lærer hvordan de brukes for å tilfredstille de grunnleggende prinsipene. Med fare for at den som leser boken blir pattern happy lærer han bort Command, Template Method, Strategy, Adapter, Monostate, Null object, Visitor, Decorator, Gateway, Proxy, State, osv., osv.

Denne delen av boken inspirerte meg til å skrive om hvordan man bryter avhengigheter mellom klasser, hvor jeg benyttet Strategy og Adapter.

Section IV: Packaging the Payroll System

I del 4 fortsetter man med applikasjonen fra del 3. Bob forteller oss om hvordan man bryter opp et prosjekt i moduler basert på Cohesion og Coupling. Han presenterer flere prinsipper, og patterns for å understøtte disse prinsippene. Og han fortsetter å presentere enhetstester og kode.

Denne delen inpirerte meg til å kode og blogge om hvordan man kan bruke en tilstandsmaskin til å gjøre kode mer presis og fleksibel: En generisk state machine i C#.

Mot slutten implementerer han også et grafisk brukergrensesnitt, og bruker da WinForms og Model View Presenter-mønsteret (MVP). Dette er det grundigste og mest forståelige eksempelet på MVP jeg noen gang har sett.

Det kan tenkes at jeg leste denne boken på akkurat det riktige tidspunktet i min egen utvikling, og at den ikke vil passe like godt for alle. Jeg tør likevel å anbefale den på det sterkeste til alle utviklere som ikke allerede praktiserer best practices innen smidig utvikling (TDD, parprogrammering, etc.) og som ikke allerede er guruer innen objektorientert design og vedlikeholdbar kode.

Andre Uncle Bob-relaterte blogposter: En karriære innen programmering | For mange utviklere som ikke bryr seg

 

Å tenke objekter

Object ThinkingObject Thinking er en bok skrevet av forfatter Dr. David West i 2004. Den er overraskende nok utgitt av Microsoft, men hører egentlig hjemme i eXtreme programming / Domain-Driven Design / Smalltalk-universet, og har potensiale til å totalt endre ditt syn på softwareutvikling.

Dette er en historiebok om utviklingen innen programmeringsspråk. Det er også en politisk diskusjon, og ikke minst et filosofisk studie av hva programmering er, og hvordan det bør gjøres. Hovedtemaet er objektorientering, og forfatteren hevder at på tross av at mange sier at de driver med OO, så gjør de langt fra akkurat det. Han sier at vi har misforstått, eller eventuelt glemt, bakgrunnen for objektorientering. For å få til alt det som OO gjør tilgjengelig for oss så må vi tenke objekter på den riktige måten.

Nesten med en gang jeg startet å lese merket jeg at boken provoserte meg. Den grep tak i noen av mine fundamentale oppfatninger – ikke bare rundt programmering, men i verden generelt – og snudde dem på hodet. Og så merket jeg hvordan teksten begynte å forandre mine egne meninger…!

Det var faktisk ganske skummelt. Men også veldig spennende. Noe av det mest besynderlige var hvordan han klarte å introdusere meg til smidig utviklingsfilosofi på en helt ny måte. Han presenterer en lang avhandling om den skarpe kontrasten mellom Formalisme og Hermeneutikk.

Formalisme er sterkt knyttet til rasjonalisme, et mekanisk og deterministisk verdensbilde. Her følge man formelle metoder og teknikker. Hermeneutikk i den motsatte enden av skalaen er et ikke-deterministisk verdensbilde, og i følge Dr. West i samsvar med ting som eXtreme programming og objekt-tenking. TDD er for eksempel et glimrende eksempel på ikke-formalisme. TDD er en praktisk metode som det viser seg kan gi et veldig bra resultat, uten at det kan bevises rasjonelt.

Desverre, sier han, er det få XP/agile-praktikanter som er klar over dette filosofiske skillet. Og han har helt rett; selv har jeg blitt presentert smidig utvikling i veldig formelle rammer, og det er først nå, etter å ha lest Object Thinking, at jeg begynner å forstå det jeg tidligere bare glimtvis har sett – at smidig utvikling helt grunnleggende er en annen filosofi enn det som råder i softwarebransjen i dag.

Ken Auer sier det slik:

“Too many software professionals who use object-oriented tools aren’t object thinkers. Every developer Dave West reaches with this book takes us a step closer to improving software – and the industry.”

Jeg merker at det er vanskelig, eller snarere umulig, å formidle denne oppdagelsen på en god måte. Jeg tror dette er en oppdagelse hver og en må gjøre i sitt eget tempo. La meg i stedet fortelle litt mer om boken..

Objekt-tenking går rett og slett ut på å tenke som et objekt. Tradisjonelle utviklere tenker som datamaskiner. De konsentrerer seg om den tekniske løsningen, om implementasjon. OO-programmerere må lære seg å tenke mer som objekter – vi må tenke på problemet, og modellere det i stedet.

Nøkkelordet fra boken for meg er Behaviouradferd! Vi må designe programvaren vår rundt objekter, som igjen er definert i form av adferd. Dette i sterk kontrast til den data-sentriske modelleringen de fleste normalt bedriver. Rockford Lhotka, utvikleren bak CSLA.NET, anbefaler også Object Thinking, og sier det på denne måten:

“To me that’s the value in West’s book. The philosophy of each object having a single, clear responsibility. Of objects collaborating with other objects to perform complex tasks, without being complex themselves.”

Dr. David WestDen andre delen av boken..

Object Thinking har også en praktisk del. Mens den første halvdelen fokuserer på historie, filosofi, og hvordan man skal tenke, presenterer Dr. West i den andre halvdelen en del teknikker – modelleringsteknikker – som er i samsvar med objekt-tenking. Dette falt ikke så godt i smak for meg. Jeg vil foreslå at man hopper over noen av de mest praktiske delene, og i stedet leser noe sånt som Agile Principles, Patterns, and Practices in C# av Robert C. Martin, som har en litt annen tilnærming som er mer kjent for meg, men som fortsatt er i samsvar med det Dr. West sier.

Til slutt et par linker: Boken kan du selvsagt få tak i hos Amazon. Det finnes også et intervju med Dr. West på PolymorphicPodcast.

Knagger: , , , ,

Hvordan bryte avhengigheter mellom klasser

Tette koblinger er noe man snakker mye om innen utvikling av software – noe man bør unngå bl.a. fordi det gjør programmene vanskeligere å endre. Og at det vil komme endringer er det eneste sikre i ethvert softwareprosjekt.

I denne artikkelen forsøker jeg å illustrere et helt vanlig scenario med tett kobling mellom to klasser. Jeg snakker litt om hva to av SOLID-prinsippene sier om dette, og viser hvordan avhengighetene kan løses opp ved å benytte et par velkjente design patterns.

I dette fiktive eksempelet skal jeg implementere en del av programvaren til en mobiltelefon. Man må kunne taste inn et telefonnummer, og når nummeret er tastet skal telefonen ringe opp nummeret. Jeg ser da for meg at jeg behøver en klasse for å representere en knapp: Button. Når en knapp blir trykket på, sender den avgårde sifferet knappen representerer til en annen klasse som skal ringe nummeret; jeg kaller denne klassen for Dialer.

Dependencies1

Disse to klassene danner et typisk klient-tjener forhold: Button bruker Dialer som en tjeneste, og har en sterk knytning og avhengighet til den, f.eks. ved at den holder en referanse til et Dialer objekt, slik som dette:

public class Button

{

    private Dialer _dialer;

    public Button(Dialer dialer)

    {

        _dialer = dialer;

    }

}

Denne harde koblingen er lite fleksibel, og fører til kode som er vanskelig å endre eller utvide. Det finnes flere prinsipper innen objektorientering som kan brukes til å identifisere problemet. Ta for eksempel dette:

The Open/Close Principle (OCP)
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. (spec)

Om du ikke kjenner til OCP fra før er det ikke helt lett å skjønne hva man mener med “åpen for utvidelse men lukket for endringer”. Men se på Button-klassen. Den er som sagt hardt knyttet til Dialer, og skulle vi ønske å benytte en annen dialer, så må vi endre Button. Butten er altså ikke åpen for denne typen utvidelser.

Et annet prinsipp som sier noe om hva vi kan gjøre med dette er DIP:

The Dependency-Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend upon abstractions. (spec)

For å gjøre noe med dette kan vi bruke det vi kaller for STRATEGY PATTERN – vi definerer et interface som Button kan være avhengig av istedet. Mange ville f.eks. opprettet et IDialer interface, slik som dette:

public interface IDialer { /* some interface */ }

 

public class Dialer : IDialer { /* inplementation */ }

 

public class Button

{

    private IDialer _dialer;

    public Button(IDialer dialer)

    {

        _dialer = dialer;

    }

}

Nå kan man utvide med flere typer Dialers uten at man må endre Button-klassen. Men.., det er noe som skurrer litt. Button føles fortsatt for hardt knyttet mot dette Dialer-konseptet. Det kan jo godt tenkes knapper skal benyttes til helt andre ting enn å ringe (som å sende SMS, spille spill o.l.).

Løsningen er enkel men genial. Interfacer har nemlig mye mer med klienten i avhengighetsforholdet å gjøre (i dette eksempelet Button) enn med klassene som implementerer dem (Dialer). En Dialer er noe som er interessert i å få vite når en knapp blir trykket på. Hvis vi lar Button eie interfacet ved å døpe det om til å hete noe sånt som IButtonListener, og lar Dialer implementere dette, så åpner vi opp for å lage andre klasser som også kan lytte til knappetrykk – uten at de har noe med det å ringe å gjøre – og det uten at vi må modifisere hvordan knappen er implementert.

Dependencies2

public class Button

{

    private IButtonListener _buttonListener;

    public Button(IButtonListener buttonListener)

    {

        _buttonListener = buttonListener;           

    }

}

Nå er Button helt uavhengig av alt som har med Dialer å gjøre, selv om den under kjøring av programmet vil motta en referanse til et Dialer-objekt. Og fra knappens ståsted virker dette veldig fornuftig.

Dialer derimot er avhengig av å implementere IButtonListener. Vil vi ta løsningen vår et steg videre kan vi fjerne denne avhengigheten også. For å gjøre det kan vi bruke et annet mønster som kalles ADAPTER PATTERN. Det bruker man bl.a. om man vil skjule/konvertere et interface, f.eks. om man ikke har mulighet til å endre en klasse som Dialer, eller som i dette tilfellet når man ikke ønsker å rote til Dialer med referanser til Button-konseptet.

Dependencies3

Vi oppretter da en ButtonDialerAdapter-klasse. Denne adapteren lar vi arve fra IButtonListener, og det er den klassen Button nå vil få en instans av under kjøring. ButtonDialerAdapter vil i sin tur ha en referanse til Dialer, og oversetter knappetrykket til den korresponderende meldingen/funksjonskallet i Dialer.

public interface IButtonListener { /* some interface */ }

 

public class Dialer { /* implementation */ }

 

public class Button

{

    private IButtonListener _buttonListener;

    public Button(IButtonListener buttonListener)

    {

        _buttonListener = buttonListener;

    }

}

 

public class ButtonDialerAdapter : IButtonListener

{

    private Dialer _dialer;

    public ButtonDialerAdapter(Dialer dialer)

    {

        _dialer = dialer;

    }

}

Button har fortsatt bare en avhengighet mot IButtonListener, som gir fullstendig mening. Og Dialer har nå ingen avhengigheter mot noe som har med Button å gjøre. ButtonDialerAdapter avhenger av Dialer og IButtonListener. Ved å bruke strategy pattern og adapter pattern har vi tilfredstilt både Open/Close-prinsippet (OCP) og Dependendcy-Invertion (DIP).

OCP er på mange måter selve kjernen i objektorientert design. Ved å følge det kan man oppnå fleksibilitet, gjenbruk og vedlikeholdbar kode. Man sier også at å snu avhengighetene (DIP) er det som skiller objektorientert design fra prosedyre/transaksjonsbasert design.

Det er derimot ikke lurt å innføre abstraksjoner (interfacer og abstrakte klasser) over alt i koden. Utvikleren må passe på å bruke abstraksjoner de stedene i programmet som endres ofte. Robert C. Martin sier blant annet:

Confirming to OCP is expensive. It takes development time and effort to create the appropriate abstractions. These abstractions also increase the complexity of the software design. There is a limit to the amount of anstractions that the developers can afford. Clearly, we want to limit the application of OCP to changes that are likely.

Så hvordan bestemmer man seg for når man skal bruke prinsippene? Vi gjør nødvendige undersøkelser, vi stiller de nødvendige spørsmålene, og vi bruker vår erfaring og sunn fornuft. Og så venter vi til endringene inntreffer! Onkel Bob sier nemlig videre:

“Fool me once, shame on you. Fool me twice, shame on me.” This is a powerful attitude in software design. To keep from loading our software with needless complexity, we may permit ourselves to be fooled once. This means that we initially write our code expecting it not to change. When a change occurs, we implement the abstractions that protect us from future changes of that kind. In short, we take the first bullet and then make sure that we are protected from any more bullets coming from that particular gun.

Men jeg tror likevel man må øve på å bruke disse prinsippene om man skal se behovet når det dukker opp. Og man må også kunne overføre erfaring om endringer fra et utviklingsprosjekt til et annet. For flere synspunkter på akkurat dette kan du se kommentarene etter posten min om når man skal bruke SOLID prinsippene.

Les også: T-Man tipser om specification pattern

Denne artikkelen ble til under inspirasjon fra Robert C. Martins bok Agile Principles, Patterns, and Practices in C# (2006).

Når skal man bruke SOLID prinsippene?

SOLIDSOLID er benevnelsen på fem prisnipper innenfor objektorientert softwaredesign som vi har hørt mye om i løpet av den siste tiden. Blant annet holdt Mark Nijhof et innlegg på NNUG Bergen om SOLID i januar i år. Foredraget finnes også i blogpost-format her.

Siden det foredraget har jeg forsøkt å lære meg disse prinsippene, og brukt dem etter beste evne. Akkurat nå leser jeg Robert C. Martin’s Agile Prinsiples, Patterns, and Practices in C#, og da går det plutselig opp for meg at selv om mange har forsøkt å lære meg prinsippene, så er det ingen som sålangt har fortalt meg hvordan man skal bruke dem.

I boken sier Onkel Bob Martin at man skal bruke prinsippene når de trengs. Unødvendig bruk fører bare til unøvdendig kompleksitet. Jeg føler dette er et viktig poeng å få frem.

Ta for eksempel Single Responsibility Prinsiple (SRP). Dette prinsippet sier at en klasse kun skal ha ett ansvar, og ansvar defineres som én grunn til å endres. Men en grunn til å endres er kun en grunn til å endres om den endringen faktisk inntreffer. Onkel Bob sier at det er ikke lurt å innføre SRP – eller noe annet OO-prinsipp for den saks skyld – uten at man har symtomer som kan løses med prinsippet.

Dvs. at man ikke splitter opp en klasse i flere klasser basert på ansvarsområder før man får behov for å endre klassen. Når endringen kommer, og endringen kun gjelder for ett av ansvarsområdene, kan man bruke SRP og splitte ut det ansvarsområdet som endres til en ny klasse.

“Agile teams apply principles only to solve smells; they don’t apply principles when there are no smells. It would be a mistake to unconditionally confirm to a principle just because it is a principle. The principles are there to help us eliminate bad smells. They are not a perfume to be liberally scattered all over the system. Over-confirmance to the principles leads to the design smell of needless complexity.”

Knagger: , , ,

T-Man tipser om Specification pattern

Hver uke skriver jeg en artikkel for utviklerne i Contiki, hvor jeg forsøker å gi gode råd og tips først og fremst om ting som design og refakturering. Denne uken presenterte jeg Specification pattern, et mønster som isolerer business-regler og gjør kode som må ta mange avgjørelser enklere å lese og samtidig mere fleksibel.

Specification er et relativt avansert mønster som er en del av det som populært kalles Domenedrevet Design (DDD), som kan hjelpe oss (og da mener jeg spesielt oss i Contiki), til å eliminere duplisert kode. Jeg har alltid vært fasinert av konseptet om å lage en regelmotor, og dette mønsteret er én måte å implementere dette på.

Vi starter med litt tilfeldig kode..

For å fortelle deg om Specification vil jeg ta deg med på en liten reise, hvor vi starter med litt kode som har forbedringspotensiale, og som etterhvert vil endres til noe som er mer fleksibelt. Koden er ikke reell kode – bare noe jeg fant på i farten. SendIfPossible-metoden sender et dokument til en gitt bruker hvis noen kriterier tilfredstilles:

public class Step1

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        if (document.Status != DocumentStatus.Draft

            && document.Status != DocumentStatus.ReleaseCandidate

            && (document.Status != DocumentStatus.Final

            || !_config.ApprovalNeeded))

        {

            if (document.SecurityLevel == DocumentSecurityLevel.Public

                || (document.SecurityLevel == DocumentSecurityLevel.Restricted

                && (externalUser.TrustLevel >= 3

                || externalUser.YearlyContribution >= 10000)))

            {

                _sender.AddObject(document)

                    .ToEmail(externalUser.EmailAddress)

                    .Send();

            }

        }

    }

}

Desverre skriver de fleste av oss kode som dette fra tid til annen. Testene som gjøres her er vanskelige å lese, og reglene er vanskelige å forstå. Ok, la oss forsøke å forbedre dette litt da….

Trekk ut metoder

public class Step2

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        if (DocumentIsReadyToBeShown(document))

        {

            if (SecurityLevelOkForUser(document, externalUser))

            {

                _sender.AddObject(document)

                    .ToEmail(externalUser.EmailAddress)

                    .Send();

            }

        }

    }

 

    private bool DocumentIsReadyToBeShown(IDocument document)

    {

        return document.Status != DocumentStatus.Draft

                        && document.Status != DocumentStatus.ReleaseCandidate

                        && (document.Status != DocumentStatus.Final

                        || !_config.ApprovalNeeded);

    }

 

    private static bool SecurityLevelOkForUser(IDocument document, IUser externalUser)

    {

        return document.SecurityLevel == DocumentSecurityLevel.Public

                        || (document.SecurityLevel == DocumentSecurityLevel.Restricted

                        && (externalUser.TrustLevel >= 3

                        || externalUser.YearlyContribution >= 10000));

    }

}

Vi har nå trukket ut inneholdet i de to if-uttrykkene til separate metoder. Dette er en vanlig refaktureringsteknikk som add-ins som ReSharper, Refactor! og til og med Visual Studio kan hjelpe deg med. Gjennom å gi metodene gode navn er det klarere hvilke kriterier som må møtes for at dokumentet skal sendes til brukeren:

Dokumentet må være visningsklart, dvs. at statusen ikke må være DRAFT, ikke RELEASECANDIDATE, og hvis godkjenning av dokumenter er påkrevd så kan statusen heller ikke være FINAL. Og så må sikkerhetsnivået være ok for den gitte brukeren, dvs. at dokumentet enten må være PUBLIC, eller i tilfeller hvor det er RESTRICTED så må brukeren enten ha TRUST LEVEL 3 eller høyere, eller vi må tjene 10.000 eller mer på denne personen.

Ok, så kanskje det ikke var så lett å lese og forstå likevel. Dette bør vi kanskje splitte opp mere.

Men før vi gjør det, la oss først tenke gjennom om noe av denne logikken skulle vært flyttet til dokumentet eller bruker-objektet. For eksempel kunne vi ha opprettet en metode på IDocument for å spørre dokumentet om det var visningsklart. Men da måtte vi ha gitt IDocument en dependency mot IConfiguration, som jo holder på ApprovalNeeded-informasjonen. Eller i alle fall sende inn denne informasjonene til dokumentet på en eller annen måte. Og dette er noe dokumentet ikke burde behøve å forholde seg til. Det samme gjelder for å sjekke sikkerhetsnivå basert på bruker, hvor man må kombinere informasjon fra dokument og bruker – i tillegg til noen hardkodede konstanter som absolutt burde vært isolert bedre.

Trekk ut flere metoder

public class Step3

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        if (DocumentIsReadyToBeShown(document))

        {

            if (SecurityLevelOkForUser(document, externalUser))

            {

                _sender.AddObject(document)

                    .ToEmail(externalUser.EmailAddress)

                    .Send();

            }

        }

    }

 

    private bool DocumentIsReadyToBeShown(IDocument document)

    {

        return DocumentIsDone(document) && AllowedIfFinal(document);

    }

 

    private static bool DocumentIsDone(IDocument document)

    {

        return document.Status != DocumentStatus.Draft

            && document.Status != DocumentStatus.ReleaseCandidate;

    }

 

    private bool AllowedIfFinal(IDocument document)

    {

        return document.Status != DocumentStatus.Final

            || !_config.ApprovalNeeded;

    }

 

    private static bool SecurityLevelOkForUser(IDocument document, IUser externalUser)

    {

        return document.SecurityLevel == DocumentSecurityLevel.Public

            || (document.SecurityLevel == DocumentSecurityLevel.Restricted

            && IsUserVeryImportant(externalUser));

    }

 

    private static bool IsUserVeryImportant(IUser externalUser)

    {

        return externalUser.TrustLevel >= 3

            || externalUser.YearlyContribution >= 10000;

    }

}

Som du forhåpentlig vis ser blir koden mer lesevennlig jo mer vi splitter den opp, hovedsaklig fordi metodenavnene dokumenterer hva testene egentlig betyri domenemodellen vår. Men det gjenstår noen problemer. For det første begynner det å bli “trangt om plassen” i klassen vår – det blir rotete og uoversiktelig med mange, små metoder. For det andre er det svært sansynlig at testene som disse metodene våre utfører også vil være nyttige andre steder, sansynligvis i andre klasser. Jeg får derfor lyst til å trekke dem ut herfra…

Specification pattern (enkel variant)

Da er det endelig tid for å ta en titt på Specification mønsteret. Først trenger vi et enkelt interface:

public interface ISpecification<T>

{

    bool IsSatisfiedBy(T candidate);

}

Interfacet trenger bare én metode, og den kaller vi IsSatisfiedBy. Og nå kan vi implementere noen forretningsregler:

public class DocumentIsDoneSpecification : ISpecification<IDocument>

{

    private IConfiguration _config;

    public DocumentIsDoneSpecification(IConfiguration config)

    {

        _config = config;

    }

 

    public bool IsSatisfiedBy(IDocument document)

    {

        return DocumentIsDone(document) && AllowedIfFinal(document);

    }

 

    private static bool DocumentIsDone(IDocument document)

    {

        return document.Status != DocumentStatus.Draft

            && document.Status != DocumentStatus.ReleaseCandidate;

    }

 

    private bool AllowedIfFinal(IDocument document)

    {

        return document.Status != DocumentStatus.Final

            || !_config.ApprovalNeeded;

    }

}

 

public class DocumentHasOkSecurityLevelForUserSpecification

    : ISpecification<IDocument>

{

    private IUser _user;

    public DocumentHasOkSecurityLevelForUserSpecification(IUser user)

    {

        _user = user;

    }

 

    public bool IsSatisfiedBy(IDocument document)

    {

        return document.SecurityLevel == DocumentSecurityLevel.Public

            || (document.SecurityLevel == DocumentSecurityLevel.Restricted

            && IsVeryImportantUser);

    }

 

    private bool IsVeryImportantUser

    {

        get

        {

            return _user.TrustLevel >= 3

                || _user.YearlyContribution >= 10000;

        }

    }

}

I konstruktørene tar spesifikasjonene inn sine eksterne avhengigheter – all den informasjonen de måtte behøve for å avgjøre om et hvilket som helst dokument tilfredstiller forretningsregelen spesifikasjonen implementerer. Nå har vi et sentralt sted, en klasse, hvor regelen er definert, og som kan brukes hvor som helst i koden vår.

Ok, la oss forsøke å bruke disse reglene da:

public class Step4

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        IList<ISpecification<IDocument>> criterions = new List<ISpecification<IDocument>>();

        criterions.Add(new DocumentIsDoneSpecification(_config));

        criterions.Add(new DocumentHasOkSecurityLevelForUserSpecification(externalUser));

 

        foreach (var criterion in criterions)

            if (!criterion.IsSatisfiedBy(document))

                return;

 

        _sender.AddObject(document)

            .ToEmail(externalUser.EmailAddress)

            .Send();

 

    }

}

I denne versjonen av SendIfPossible-metoden lager vi først en liste av spesifikasjoner, og kontrollerer så hver av dem før vi sender dokumentet til brukeren. Dette er bare en måte å bruke spesifikasjoner på. Den faktiske listen kunne ha blitt konfigurert et annet sted, og for eksempel inneholdt ulike criterier i ulike situasjoner.

Komponerbare spesifikasjoner

La oss utvide konseptet noe. Vi ønsker nå å kunne kombinere ulike spesifikasjoner, og dermed lage nye, komponerte spesifikasjoner på et høyere nivå. La oss utvide ISpecification og i tillegg lage en abstrakt klasse om arver fra dette:

public interface ISpecification<T>

{

    bool IsSatisfiedBy(T candidate);

    ISpecification<T> And(ISpecification<T> other);

    ISpecification<T> Or(ISpecification<T> other);

    ISpecification<T> Not();

}

 

public abstract class CompositeSpecification<T> : ISpecification<T>

{

    public abstract bool IsSatisfiedBy(T candidate);

 

    public ISpecification<T> And(ISpecification<T> other)

    {

        return new AndSpecification<T>(this, other);

    }

 

    public ISpecification<T> Or(ISpecification<T> other)

    {

        return new OrSpecification<T>(this, other);

    }

 

    public ISpecification<T> Not()

    {

        return new NotSpecification<T>(this);

    }

}

Her har vi lagt til tre operasjoner på spesifikasjon: And, Or og Not. Den abstrakte klassen implementerer disse, en implementasjoner som vil være gjeldene for alle konkrete forretningsregler vi implementerer. Den abstrakte klassen benytter tre nye klasser som vi også må definere:

public class AndSpecification<T> : CompositeSpecification<T>

{

    private ISpecification<T> One;

    private ISpecification<T> Other;

 

    public AndSpecification(ISpecification<T> x, ISpecification<T> y)

    {

        One = x;

        Other = y;

    }

 

    public override bool IsSatisfiedBy(T candidate)

    {

        return One.IsSatisfiedBy(candidate) && Other.IsSatisfiedBy(candidate);

    }

}

 

public class OrSpecification<T> : CompositeSpecification<T>

{

    private ISpecification<T> One;

    private ISpecification<T> Other;

 

    public OrSpecification(ISpecification<T> x, ISpecification<T> y)

    {

        One = x;

        Other = y;

    }

 

    public override bool IsSatisfiedBy(T candidate)

    {

        return One.IsSatisfiedBy(candidate) || Other.IsSatisfiedBy(candidate);

    }

}

 

public class NotSpecification<T> : CompositeSpecification<T>

{

    private ISpecification<T> Wrapped;

 

    public NotSpecification(ISpecification<T> x)

    {

        Wrapped = x;

    }

 

    public override bool IsSatisfiedBy(T candidate)

    {

        return !Wrapped.IsSatisfiedBy(candidate);

    }

}

Vi kan nå implementere våre forretningsregler ved å arve fra CompositeSpecifications. Jeg overlater dette som en oppgave til leseren.., i stedet hopper vi direkte til slutten og ser hvordan dette kan brukes:

public class Step5

{

    private ISendStuff _sender;

    private IConfiguration _config;

 

    public void SendIfPossible(IDocument document, IUser externalUser)

    {

        var isDone = new DocumentIsDoneSpecification(_config);

        var okSecurity = new DocumentHasOkSecurityLevelForUserSpecification(externalUser);

 

        ISpecification<IDocument> sendCriterion = isDone.And(okSecurity);

 

        if (sendCriterion.IsSatisfiedBy(document))

        {

            _sender.AddObject(document)

                .ToEmail(externalUser.EmailAddress)

                .Send();

        }

    }

}

Nå har vi laget et slags “fluent interface” (eller mer presist brukt method chaining), og vi kan dermed kjede sammen forretningsregler slik som i dette banale eksempelet: isDone.And(okSecurity). Fantasien din har nok allerede begynt å jobbe med eksempler som har mange flere forretningsregler. Og for å sjekke om kriteriene er oppfylt trenger vi bare kalle IsSatisfiedBy på den komponerte spesifikasjonen.

Dette mønsteret åpner opp mange, spennende muligheter. Koden vår kan forholde seg til det enkle ISpecification interfacet for å sjekke kriterier, uten å vite noe om hvordan disse kriteriene har blitt konstruert. Spesifikasjoner kan bli sendt fra sted til sted, og nye spesifikasjoner kan kjedes til på ulike steg i prosessen – samtidig som koden holdes ren og ryddig.

Knagger: , , , ,

Inspirasjonskilder: DDD:Specification pattern blog post av Casey Charlton, Specification pattern på Wikipedia


Alf Kåre Lefdal: Distributed Podcast er også ganske interessant. De tar opp tema som fx. ...

Stian: +1 for 6er til This Developer's Life! Min definitive favoritt. Jeg trengte også...

Torbjørn: Takk for flere tips, Vegard. Deep Fried Bytes ligger på oversikten min fra 2009...

Vegar: Og glemte helt ios: Nsbrief og ideveloper live. Har du hørt på deep fried byt...

Vegar: Mye kjekt her. TDL, hanselminutes og .net rocks ligger i en klasse for seg. Suv...

Torbjørn: Helt enig, arkivet til Software Engineering Radio er en gullgruve om man vet hva...

Einar W. Høst: Jeg synes at det kuleste med se-radio er backloggen av intervjuer... det er noen...

arnab: fantastisk :)...

Olav: Glimrende blogg ! Modellen av hjernens arbeid passer ikke bare på nyskaping: ...

Torbjørn: Ja, flydesign trekkes ofte frem som et eksempel på dette fenomenet. Design av b...

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • NodeJS vs. ASP.NET
  • Pulten min..
  • No ifs and buts
  • Community-fiskebolle på ROOTS 2012
  • Kategorier

  • .net ninja (37)
  • Bøker (18)
  • Diverse prosjekter (37)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (78)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (34)
  • NDC (4)
  • NNUG / community (63)
  • O/RM & databaser (10)
  • Off topic (118)
  • OO-design/clean code (31)
  • Podcasts (15)
  • Polyglot (82)
  • Ruby (29)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (24)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (34)
  • WPF (9)
  • WTF (13)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta