's Picture

Mapper - stuprör ger ordning och reda

Postad av Johan Tell under javascript, promises

Promises i Javascript är fantastiska! Förutom att göra det möjligt att jobba asynkront utan röriga callbacks, skapar de dessutom en bättre översikt med tydligare flöden och enklare felhantering.

Som med det mesta annat är dom dock inte perfekta. Ett av de större problemen när det kommer till läsbarhet är att användandet av anonyma funktioner tvingar läsaren att läsa igenom hela funktionen för att förstå vad som händer. Som utvecklare väljer man ofta att extrahera och namnge funktionen vilket kan se ut såhär:

...
.then(transformBBCode)
...

function transformBBCode(value){  
  value.items = value.items.map((item) => {
      item.title = transformBBCode(item.title);
      return item;
  });
  return items;
}

Alla titlar i alla element får sin bbcode hanterad och hela världen ler av lycka, men är det verkligen en bra implementation?

En utvecklare som för första gången ser vår implementation skulle märka att operationerna saknar en hel del information. Frågor som rör transformBBCode dyker upp;
"Vad är det som transformeras?", "Vad returnerar egentligen funktionen?".

Var jag vill komma är att det är en stor del information som försvinner pga att funktionens namn inte tillräckligt bra beskriver vad den gör. Detta tvingar utvecklaren att ta reda på vad den gör på andra sätt, som ofta innebär att läsa dokumentation eller rent av ögna igenom implementationen. Om vi byter namn på funktionen transformBBCode till transformBBCodeWithTitleOnEachItemInItems kommer våra ögonen att gå i kors.

Så varför håller jag på att ranta om dessa till synes mindre problem? I device-teamet, där jag arbetar, har vi behovet av att hämta stora mängder data från olika källor som sedan bakas ihop, ändras lite och sen skickas vidare. All data exponeras i ett REST-API. Det finns ingen dedikerad utvecklare som underhåller API:et. Därför är det inte en ovanlighet att utvecklarna från andra team modifierar API:et vid behov. Vi vill förhindra att koden blir rörig och att utvecklarna måste spendera majoriteten av sin tid på att sätta sig in i koden varje gång. Vi behöver en lösning som är extremt tydlig och enkel men samtidigt väldigt modulär och robust.

I stället för långa semantiska namn har vi utvecklat ett verktyg vi kallar Mapper. Mapper utför strikt scopade asynkrona operationer på objekt i ett tydligt funktionellt flöde. Med Mapper kan vi bygga promise-chains som är mycket mer uttrycksfulla än de normalt sett brukar vara. På så sätt blir transformationer av data överskådliga och enkla att följa, även för någon som är ny i projektet. Genom att utföra operationer i scopes förhindras även i viss mån code smells som t.ex. orena (impure) funktioner.

Mapper är ett promise med flertalet funktioner för att underlätta operationer på ett data-objekt.

function mapFamily(family) {  
  return Mapper.pipe(family)
    .add("familyName", generateFamilyNameSync)
    .modify("address", fetchAddressForNewHome)
    .mapOn("members", increaseAgeAsync)
    .rename("members", "familyMembers")
    .select("familyName", "familyMembers", "address");
}

Det kan jämföras med hur en vanlig implementation av samma beteende kan se ut:

function mapFamily(family) {  
  return fetchAddressForNewHome(family)
    .then((address) => {
      family.address = address;
      return family;
    })
    .then(increaseAgeAsync)
    .then((familyMembers) => {
      return {
        familyMembers: familyMembers,
        familyName: generateFamilyNameSync(family),
        address: family.address
      };
    });
}

Det finns några viktiga saker att lägga märke till i de båda implementationerna: Mapper-implementationen är extremt tydlig.
De mer uttrycksfulla metoderna (add, modify, rename o.s.v.) ger oss en beskrivning av vad vi gör, med vad och hur vi gör det.
Vi behöver inte skriva anonyma metoder för att utföra enkla operationer på enstaka nycklar i våra objekt.
Vi göra allt detta genom en operation, med ett semantiskt namn, på nyckel i ett objekt och skicka en referens till metoden som utför operationen.

En annan positiv effekt är att varje operation även sätter ett scope som tydliggör inom vilka ramar en funktion förhåller sig.
Det skulle exempelvis vara uppenbart att man skapar en nytt operationsanrop i kedjan om man vill lägga till antalet hundar i familjen. I en vanlig implementation skulle en utvecklare instinktivt göra denna operation i en befintlig promise, utan tanke på scope eller läsbarhet

PS. Missa inte vår nästa bloggpost, följ oss på Twitter!

Till startsidan