Categories
Teknik

Cleaner Code

Agil systemutveckling måste vara agil på alla plan. Citerus konsult Jörgen Falk, med 10 års erfarenhet av agila metoder i bagaget, ger här några tips och råd kring det ädla hantverket att skriva kod i en agil miljö.

jorgen-falkAgil systemutveckling måste vara agil på alla plan. Dagliga stå-upper, retrospektiv, estimeringar, burn-downs, korta sprint:ar är mycket viktiga, men sättet vi skriver kod på måste också vara agil för att kunna möta kravet på snabba leveranser, hög kvalité och föränderliga krav.

 

number of WTF per minute

Bild 1. Antal WTF/minut: det enda mätbara kvalitetsmåttet för kod

I denna artikel tänkte jag försöka belysa tre aspekter av kodskrivandet som jag tycker är extra viktiga i en agil värld: begripbar kod, testbar kod och kod som är möjlig att återanvända. Vi kommer att titta på ett kodexempel som delvis är taget från verklig kod och delvis är påhittat. Vi kommer att ta den från att vara mycket svårläst och svårtestad till att bli en kod som är lättläst, som tydligare uttrycker intentionen med tydliga tester och som potentiellt1 kan återanvändas.

Status Quo

Traditionell systemutveckling har fokuserat på att uppnå ett oföränderligt läge för att säkerställa sin funktion. Frysta spec:ar, designfas och implementationsfas, fullständigt regressionstest, “rör inte den kod som fungerar” etc är ett litet potpurri av begrepp som alla uttrycker en idé om att kod är som stentavlor och ett mjukvarusystem är bara att lägga stentavlor ovanpå varandra. Sedan polerar man ytan genom att gnugga in mängder av mirakelsalvan Quality Assurance.

I en föränderlig värld måste vi kunna ändra oss ofta eller snabbt kunna utöka en funktion. Vi kan inte längre leva på att det som en gång har fungerat automatiskt fungerar i all framtid. En agil värld innebär ofta att man har täta leveranser och då har vi vare sig tid eller möjlighet att på förhand klura ut en perfekt design som skall stå sig för all framtid. Vi måste utgå från det vi vet idag, men vara öppna för eventuella förändringar i framtiden. I denna föränderliga värld ställs det helt andra krav på hur vi skriver kod och hur vi testar. Vi behöver framför allt skriva kod som är läsbar, testbar, flexibel och som inte upprepar sig.

I jämförelse med ett helt systems livstid är tiden för att skriva den initiala koden bara ett kort ögonblick. Under systemets livstid kommer många nya ansikten att behöva se och förstå koden och koden kommer att behöva omformas eller utökas många gånger. Det är när systemet är i drift som det genererar pengar, eller om vi har otur, som det kommer att kosta oss dyrt om det inte fungerar som det skall. Det är först efter att den har driftssatts som vår kod visar sitt rätta ansikte och börjar leva! Ditt jobb som systemutvecklare slutar alltså inte med att du checkar in din kod; tvärtom, det är då allvaret börjar och du måste vara beredd på att ta ansvar för koden under hela dess livstid.

Bild 2: Livscykel för ett mjukvarusystem. Tiden för att realisera en idé är förhållandevis kort jämfört med hela livscykeln. Om systemet är framgångsrikt och genererar höga intäkter finns det en risk att kostnaden för driftsstörningen och inkomstbortfallet är mycket mer än kostnaden för att skapa funktionen.

Ett litet exempel

Vårt påhittade exempel2 är skrivet i Java, dels för att det är ett förhållandevis populärt språk, dels för att behovet av välstrukturerad kod är förhållandevis stort i Java. För att till fullo förstå koden är det bra om du kan Java och lite om Dependency Injection3 och att du känner dig familjär med TDD och mockramverk. Systemet består av en fingerad AppService:

public class TheAppService implements AppService {
    private final CustomerRepository customerRepository;
    private final SenderRepository senderRepository = SenderRepository.instance();
   
    @Inject
    public TheAppService(final CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }
   
    public void onTimerEvent() {
        final List<Customer> customers = customerRepository.getNewCustomers();
        final Campaign campaign = new HalfOfCampaign();
        final Set<Long> ids = new HashSet<Long>();
        if (customers == null) {
            throw new IllegalArgumentException("No new customers found");
        } else {
            List<Customer> temp = new ArrayList<Customer>();
            for (Customer c : customers){
                Long id = c.id();
                ids.add(id);
            }
            for (Long id : ids) {
                for (Customer c : customers) {
                    if (c.id().equals(id)){
                        temp.add(c);
                        break;
                    }
                }
            }
            for (Customer c : temp) {
                if ((c.getType() == CustomerType.GOLD
                || c.getType() == CustomerType.VIP)
                && c.getBalance().over(1000.00)){
                    senderRepository.findFastestSenderService().
                                         send(campaign.message(), c);
                }
            }
        }
    }
}

En berättelse baserad på en sann historia

Föreställ dig att du är ny i teamet och skall fixa en bugg. “Ok, vad är det som händer här?” är din första tanke. onTimerEvent()4 hanterar någon slags händelse, men det säger bara när funktionen anropas, inte vad servicen gör. Först en null-check. Är null ett fel eller är det den tomma mängden man menar? Oj, mycket for och if här, bäst att hålla tungan rätt i mun. Varför lagrar man id:ena i ett nytt Set och sedan kopierar kunderna till en temp-lista? Safe-copy, eller ..? Ok, och till slut lite logik. Undra vad det är med VIP och guldkunderna?

De flesta utvecklare kan här enkelt följa flödet i for-looparna och if-satserna, men ingen kan i efterhand förstå intentionen med koden. Var det meningen att koden skulle göra så eller är det en bugg? Koden genererar bara många frågor och inga ledtrådar till hur det var tänkt att fungera.

Enhetstester kan ge dig både en trygghet när man refaktorerar kod men testerna kan, om de är välskrivna, även fungera som en beskrivning på hur enheten (dvs klassen) fungerar. Så du letar med ljus och lyckta efter något enhetstest. “Näe, vi kör inte med enhetstester, det blir bara massa jobb med dem. Vi satsar i stället på TLNFT (TheLatestNewFancyThing) Driven Development”, får du till svar när du frågar de som har jobbat ett tag med systemet. Det finns något litet test, men ingen tror inte det testar något vettigt. Efter lite letande hittar du “testet”:

public class AppTest{

    @Test

    public void onTimerEvent(){

        AppService service = new TheAppService(createInMemCustomerRepository());

        service.onTimerEvent();

    }



    private CustomerRepository createInMemCustomerRepository(){

        return new InMemCustomerRepository(newArrayList(

        new Customer(1L,"Mr X",new Money(987.65),CustomerType.BASIC),

        new Customer(2L,"Ms V",new Money(1234.56),CustomerType.BASIC),

        new Customer(3L,"Ms Y",new Money(2345.67),CustomerType.GOLD),

        new Customer(4L,"Mr Z",new Money(3456.78),CustomerType.VIP)));

    }

}

Du kör testet. Grönt ljus och lite brus på konsolen. Lysande! 100% testteckning, 0% nytta. Här var den som skrev testet tvungen att sätta upp ett mock:at repository med en mix av kunder för att testa olika fall i koden, men metoden returnerar inget värde så det enda vi kan fånga i detta test är om något går riktigt fel dvs ett undantag kastas. Vi kan inte verifiera att rätt händelser sker för de givna kunderna annat än att titta i loggfilen eller på konsolen.

Ta kontroll över beroendena

Ett av problemen med testbarheten i koden ovan är att vi inte kan verifiera något utfall. Inga returvärden, ingen trivial input eller output och klassen är nästan helt låst. Huvudorsaken till problemet är att skapandet av den statiska instansen av SenderRepository sker i vår AppService. Statiska instanser, likt den i vår AppService, skapar hårda beroenden till många delar av systemet. Det kanske känns enkelt i början med en statisk instans som alla delar av koden enkelt kan nå utan att behöva krångla. Nackdelen är att alla delar av koden som refererar till den statiska instansen blir mer eller mindre sammanfogade med den klassen. Ofta görs detta också i långa serier där en instans är beroende av en annan statisk instans som i sin tur är beroende av andra statiska instanser. Följden blir att man måste starta upp i princip hela systemet, inklusive tredjepartsbibliotek, databaser, webbservrar, meddelandeköer etc. för att kunna testa även den minsta enhet. I vårt fall, då vi vill kunna testa enheten AppService, är vi inte intresserade av om SenderService-klassen (som skapas av SenderRepository) fungerar eller inte.

För att kunna verifiera vårt utfall från AppService måste vi skapa en egen testversion av SenderService och SenderRepository, en sk. mock. Vi gör detta med hjälp av ett mockramverk5 där vi sätter en SenderRepository-mock genom Dependency Injection i konstruktorn till AppService:en. Notera att vi har extraherat ut ett interface ur SenderRepository för att undvika beroende till en konkret implementation.

public class TheAppService implements AppService {

    private final CustomerRepository customerRepository;

    private final SenderRepository senderRepository;



    @Inject

    public AppService(

        final CustomerRepository customerRepository, final SenderRepository senderRepository) {

            this.customerRepository = customerRepository;

            this.senderRepository = senderRepository;

    }

...
Tillbaka till vårt testfall. Nu kan vi i alla fall skriva ett första test som verifierar att något händer i SenderService-klassen då metoden anropas. Metoden mock() skapar en mock:ad instans av vår klass. Med hjälp av when() kan vi sedan styra mock:ens beteende och med verify() kan vi sedan kontrollera att vår enhet (AppService) verkligen anropade de mock:ade instanserna på det sätt vi förväntade oss.
@Test

public void onTimerEvent(){

    SenderRepository senderRepository = mock(SenderRepository.class);

    SenderService sender = mock(SenderService.class);

    AppService service = new AppService(createInMemCustomerRepository(), senderRepository);



    when(senderRepository.findFastestSenderService()).thenReturn(sender);



    service.onTimerEvent();



    verify(sender, times(2)).send(

        eq(new HalfOfCampaign().message()),

        any(Customer.class));

}

Inte perfekt men en bra början. Som det är skrivet här är testet ganska svagt. Vi verifierar att send-metoden i sender anropas två gånger och att anropet innehåller ett specifikt meddelande och något som är av klassen Customer. Det finns många möjligheter att skapa felaktig kod som inte fångas av detta test. Som koden är skriven just nu finns det heller ingen enkel fix för att stärka testkoden. Vi skulle kunna gräva oss ned i knepiga och komplexa tricks i mockramverken, men vi väljer att inte göra det utan istället fokusera på att refaktorera om koden. Om vi känner behov av knepiga och komplexa tricks är det oftast en indikation på att det är något annat grundläggande fel med koden. Oftast är det då bättre att lägga tid på att fixa problemet än att stånga sig blodig mot exempelvis ett testramverk.

Genom att ha flyttat ut kontrollen över skapandet av de instanser som vår AppService är beroende av kan vi mycket enklare utöka och förändra vårt SenderRepository utan att påverka vår AppService. Vi “injicerar” våra beroenden till vår klass istället för att skapa dem internt i klassen, därav namnet Dependency Injection.

Begriplig kod

Vi skriver inte kod för att underlätta jobbet för kompilatorn. Vi skriver kod för att kommunicera med varandra, för att förklara för framtida läsare vad vår intention var, hur vi ansåg att verksamheten fungerade och hur vi realiserade problemet i kod.

I vårt nästa steg skall vi göra koden mer begriplig. Först måste vi knäcka nöten med varför man sparar id i ett Set. Efter lite efterforskning visar det sig att det inte är safe-copy utan att det fanns en bugg för länge sedan då CustomerRepository ibland returnerade dubbletter, så man ville garantera att listan var unik6. Suck! Det bästa vore ju att fixa buggen i CustomerRepository men den kommer från vårt outsourcing-team och vi har inte tillgång till koden och den här buggen har lägsta möjliga prio eftersom vi redan har “fixat” buggen i vår del av systemet. Dubbelsuck. Vårt första steg, som vid nästan all refaktorering av legacy code, är att vi bryter ut det knepiga kodblocket och introducerar en hjälpmetod och ger det ett begripligt namn:

List<Customer> getUniqueCustomers(List<Customer> customers) {

    final Set<Long> ids = new HashSet<Long>();

    List<Customer> temp = new ArrayList<Customer>();

    for (Customer c : customers){

        Long id = c.id();

        ids.add(id);

    }

    for (Long id : ids) {

        for (Customer c : customers) {

            if (c.id().equals(id)){

                temp.add(c);

                break;

            }

        }

    }

    return temp;

}

Dessutom visar det sig att null-checken görs för att det har hänt att CustomerRepository inte konfigurerats korrekt och då returnerar null som indikation på att det gått fel. Om repositoryt är konfigurerat korrekt men inga nya kunder hittas så returnerar den, helt korrekt, den tomma listan. Vi ersätter null-checken med en mer tydlig validering genom att använda Apache Commons Lang-biblioteket. Kvar blir en betydligt mer lättläst kod:

public void onTimerEvent() {

    final List<Customer> customers = customerRepository.getNewCustomers();

    final Campaign campaign = new HalfOfCampaign();



    Validate.notNull(customers, "Error initializing the CustomerRepository");



    for (Customer c : getUniqueCustomers(customers)) {

        if ((c.getType() == CustomerType.GOLD

        || c.getType() == CustomerType.VIP)

        && c.getBalance().over(1000.00)){

            senderRepository.findFastestSenderService().send(campaign.message(), c);

        }

    }

}

Eftersom verksamhetsexperten är på plats idag passar du på att fråga vad som är tanken med VIP och guldkunderna. Jo, vi skickar alltid ut ett 50%-på-allt-erbjudande till alla våra mest värdefulla kunder varje jul. Aha, “våra mest värdefulla kunder” var precis det vi ville höra och vi bryter genast ut logiken med jämförelserna på kundobjektet till en egen metod:

public void onTimerEvent() {

    final List<Customer> customers = customerRepository.getNewCustomers();

    final Campaign campaign = new HalfOfCampaign();



    Validate.notNull(customers, "Error initializing the CustomerRepository");



    for (Customer c : getUniqueCustomers(customers)) {

        if (customerOfGreatValue(c)){

            senderRepository.findFastestSenderService().send(campaign.message(), c);

        }

    }

}



private boolean customerOfGreatValue(Customer c) {

    return (c.getType() == CustomerType.GOLD

    || c.getType() == CustomerType.VIP)

    && c.getBalance().over(1000.00);

}

Nu är koden ganska läsbar och intentionen är mycket tydligare än tidigare. Dock är det fortfarande lite jobbigt att testa och klassen är ganska sluten, dvs. svårt att modifiera och utöka.

Återanvändingsbarhet och testbarhet

Ett problem som är kvar i koden är att logiken och anropen till SenderService är inlindade i for-loopen och if-satsen. Detta gör att vi när vi skall testa måste skapa upp en massa olika Customer-objekt. Det gör det också svårt att återanvända delar av koden och att kunna utöka funktionaliteten utan att behöva förändra stora delar av koden.

Ett sätt att lösa detta på är att använda funktionsobjekt som Closure och Predicate. Tyvärr är stödet för detta mycket begränsat i Java, men med lite olika enkla hjälpmedel så kan man få ned nivån av java-brus och öka läsbarheten. Några utvecklare väljer att helt avstå från Closures och Predicates i Java på grund av bruset och att det känns ovant. Vårt lilla exempel är i mycket bättre skick nu än tidigare så vi kanske skulle kunna klara oss utan att introducera nya konstruktioner som kanske inte alla utvecklare känner till. Men för att kunna möta en föränderlig framtid behöver vi enklare kunna förändra och utöka koden, utan att behöva stöka runt allt för mycket, och vi måste också kunna friktionsfritt underhålla våra tester. Förhoppningsvis kan nästa steg i vår lilla refaktorering visa att det inte är så svårt med funktionell programmering och att inspirera till att prova på Closures och Predicate7.

I detta exempel kan vi något förenklat säga att vår kod har två domänregler: unika kunder och värdefulla kunder. Vi börjar med unika kunder. Customer ärver av Entity8, så vi kan tryggt anta att det finns en unik identitet på alla kunder. Vi skapar ett UniqueEntityPredicate. Ett predikat är här en funktion som givet ett objekt av viss typ returnerar sant eller falskt (Boolska värden)

public class UniqueEntityPredicate<T extends Entity<T>> implements Predicate<T> {

    private Set<Long> ids = new HashSet<Long>();



    public boolean evaluate(T t) {

        return ids.add(t.id());

    }

}

Ett Set returnerar “true” om det gick bra att lägga till ett element, annars “false” som i fallet då man försöker lägga till en dubblett. Både detta predikat och värdefulla-kunder-filtreringen rör unikt kunder och kanske borde grupperas ihop med Customer9. Vi kan då skapa ett kedjat predikat i Customer-klassen:

public static Predicate<Customer> ofGreatValue() {

    return new AndPredicate<Customer>(

        new UniqueEntityPredicate<Customer>(),

        new Predicate<Customer>() {

            private static final double AMOUNT = 1000.00;



            public boolean evaluate(final Customer customer) {

                return (

                    customer.getType() == CustomerType.GOLD ||

                    customer.getType() == CustomerType.VIP) &&

                    customer.getBalance().over(AMOUNT);

                }

        });

}

Detta i sig är lite brusigt, men med en viss tillvänjning så blir det ganska naturligt att beskriva logiska, återanvändbara funktionsobjekt på detta vis och man kan enkelt kedja ihop fler predikat med logisk AND, OR eller NOT,vilket kan vara mycket kraftfullt. Det är mycket troligt att buggen med dubbletter inte var specifik för just en metod utan att även andra metoder i CustomerRepository har samma bugg. Då kan vi enkelt återanvända vårt UniqueEntityPredicate i andra delar av koden. Och om logiken ändras för vad som anses som värdefull kund så påverkar det inte AppServicen.

Vi kan nu enkelt testa enskilda fall i kundlogiken. För att friska upp vårt minne så var detta något som vi försökte göra i testet för AppServicen och det var något som gjorde att testet blev svagt samt att testkoden i sig blev svår att underhålla och helt obegriplig. Nu kan vi uttrycka domänlogiken med test direkt på Customer-klassen och dess speciella Predikat:

@Test

public void customersOfGreatValue(){

    Predicate<Customer> ofGreatValue = Customer.ofGreatValue();



    Assert.assertTrue(

        "GOLD customer with a good account balance should be selected",

        ofGreatValue.evaluate(new Customer(1L,"",new Money(1001.00),CustomerType.GOLD)));



    Assert.assertTrue(

        "Duplicate customers is not of great value",

        !ofGreatValue.evaluate(new Customer(1L,"",new Money(1001.00), CustomerType.GOLD)));

        // Note: we re-use the stafeful ofGreatValue predicate instance



    Assert.assertTrue(

        "GOLD customer who is low on cash should not be selected",

        !ofGreatValue.evaluate(new Customer(2L,"",new Money(999.00),CustomerType.GOLD)));



    Assert.assertTrue(

        "NORMAL customers should not be selected even if they are stinking rich",

        !ofGreatValue.evaluate(new Customer(3L,"",new Money(99999999.00),CustomerType.NORMAL)));

}

En sista fix blir att uttrycka publiceringen av erbjudandet som en Closure. En Closure är här en funktion som exekverar en sluten bit kod givet ett objekt av en viss typ utan returvärde:

public class PublishUtil {

    public static Closure<Customer> publishTo(final SenderService sender, final String message) {

        return new Closure<Customer>() {

            public void execute(Customer customer) {

                sender.send(message, customer);

            }

        };

    }

}

och kvar i AppService blir den förhållandevis enkla och tydliga10

public void sendHalfOfOfferToCustomersOfGreatValue() {

    final Collection<Customer> customers = customerRepository.getNewCustomers();

    final SenderService fastestService = senderRepository.findFastestSenderService();

    final Campaign halfOfCampaign = new HalfOfCampaign();



    Validate.notNull(customers, "Error initializing the CustomerRepository");



    forAllDo(select(customers,ofGreatValue()),

             publishTo(fastestService, halfOfCampaign.message()));

}

Förutom att namnet på metoden i servicen äntligen uttrycker vad den gör, och inte som tidigare när den anropas, så kan man läsa intentionen i koden:

för alla utvalda kunder av stort värde,
skicka med hjälp av den “snabbaste tjänsten”
vårt halva-priset-erbjudande-meddelande 

Nu när vi har refaktorerat ut detaljerna till Customer-klassen så har vi också möjlighet att enklare testa flödet i AppService:n. En liten notis: detta passar nog bäst in i ett sammanhang av integrationstester där man testar ett större omfång av komponenter eller klasser och kanske inte är ett strikt enhetstest. Men tekniken kan vara densamma så därför visar vi detta exempel här.

@Test

public void publishToFastest(){

    CustomerRepository customerRepository = mock(CustomerRepository.class);

    SenderRepository senderRepository = mock(SenderRepository.class);

    SenderService sender = mock(SenderService.class);

    Customer customer = new Customer(3L,"Ms Y",new Money(2345.67),CustomerType.GOLD);



    AppService service = new AppService(customerRepository, senderRepository);



    when(customerRepository.getNewCustomers()).thenReturn(newArrayList(customer));

    when(senderRepository.findFastestSenderService()).thenReturn(sender);



    service.sendHalfOfOfferToCustomersOfGreatValue();



    verify(sender).send(new HalfOfCampaign().message(), customer);

}

Fortfarande inte övertygad om att funktionell programmering är användbart i Java? Ok, här kommer ett till exempel. Tänk dig att publicerings-teamet har ändrat så att man skall skicka med en Recipient istället för Customer till send-metoden. Vi kan hantera detta enkelt med en annan klass i Commons Collection, en så kallad Transformer:

public static Transformer<Customer, Recipient> asRecipients() {

    return new Transformer<Customer, Recipient>() {

        public Recipient transform(Customer customer) {

            return new Recipient(customer.getName());

        }

    };

}

och koden i vår AppService blir inte svårare att testa eller mindre begriplig:

public void sendHalfOfOfferToCustomersOfGreatValue() {

    final Collection<Customer> customers = customerRepository.getNewCustomers();

    final SenderService fastestService = senderRepository.findFastestSenderService();

    final Campaign halfOfCampaign = new HalfOfCampaign();



    Validate.notNull(customers, "Error initializing the CustomerRepository");



    forAllDo(

        collect(select(customers, ofGreatValue()), asRecipients()),

        publishTo(fastestService, halfOfCampaign.message()));

}

Allt vi lagt till är att vi omformar (collect) våra utvalda kunder som mottagare med hjälp av asRecipients()11. Vi har lyckats utöka funktionaliteten utan att försvåra testbarheten eller minska läsbarheten. Eftersom förändringen i publicerings-API:et är generellt, så är chansen också ganska stor att vi kan återanvända asRecipents-transformen i andra anrop.

Begripligt, testbart och återanvänt

Kärnan i vår AppService innehåller till slut i princip bara kod som beskriver intentionen, inget språktekniskt brus som new, throw, if-satser eller for-loopar. Kärnlogiken är utbruten från servicen till mindre enheter som enklare går att testa och återanvända. Vi kan enklare testa både detaljerna i enhetstester och flödet i servicen i integrationstester.

Det finns givetvis ytterligare förbättringar att göra, såsom att skicka in vårt predikat ofGreatValue och själva kampanjen som parametrar till metoden i AppService. Detta skulle troligtvis drastiskt öka möjligheterna med servicen. Vad skulle du vilja förändra i AppService om du fick två timmar över?

Nästa gång någon behöver ändra i denna kod så kommer man att mötas av ett “a-ha!” eller “ok!” och inte “wtf, vad är det här?” eller “åh, nej!”. Antalet WTF per minut är kanske ett lite humoristiskt sätt att mäta kvalitén på kod, men proaktiv kvalitet; där alla aktiviteter vi gör har hög kvalité från första sekunden, där koden är enkel att begripa, där det är enkelt att testa och där man enkelt kan förändra och utöka är det som verkligen skapar ett större värde under systemets livstid.


Fotnoter

1 Inget är återanvändningsbart innan det har återanvänts
2 Koden finns att klona på GitHub: https://github.com/jorgenfalk/cleanercode.git. För varje steg i refaktoreringen finns också en motsvarande branch: master, step1, step2, step3 och step4.
3 Dependency Injection här syftar inte på något speciellt ramverk, även om just dessa exempel är skrivna för att fungera med Guice. Man kan absolut implementera DI med vanliga egna klasser och om koden är liten och väl modulariserad är det att ofta att föredra än att dra in stora ramverk. Läs gärna Martin Fowler’s klassiska artikel om du vill veta mer.
4 onTimerEvent() är en påhittat namn och heter i verkligheten något helt annat.
5 Just detta exempel använder Mockito. Läs gärna denna utmärkta artikel om du vill veta lite mer.
6 Listor eller sekvenser är ordnade och kan innehålla dubbletter, medans elementen i ett set är utan inbördes ordning och det får inte finnas dubbletter. Att kopiera listans innehåll till ett set gör att man trollar bort dubbletterna. Bra om man vill trolla, mindre bra om man vill vara tydlig.
7  I detta exempel använder vi Apache Commons Collection fast med genericsstöd.
8 För mer information om Domändriven design och entiteter rekommenderas boken Domain Driven Design av Eric Evans.
9 Egentligen är kravet på unika kunder inte en domänregel i kundobjektet utan mer ett krav i servicen. Men för att visa på hur man enkelt kan kedja ihop logik så får vi se även detta som en domänregel för en kund.
10 Notera att vi här har använt static import i Java för att korta ned koden lite. Om man vill vara extra tydlig kan man ta bort all static import och då skall det stå Customer.ofGreatValue() och PublishingUtil.publishTo().
11 Både SenderService och PublishUtil är också ändrade, men det är mer en förutsättning än en del av refaktoreringen eftersom båda klasserna kommer från publiceringsteamet. Här har vi valt att lägga transformer-instansen i Customer, men det kan givetvis ändra sig i framtiden om vi inser att det finns andra beroenden.

Leave a Reply

Your email address will not be published.