Är ditt objekt verkligen inkapslat?
Häromdagen förde jag en diskussion med en kollega angående objektorientering och vilket som är den viktigaste aspekten i det objektorienterade konceptet. Utan att förringa arv och polymorfism så hävdade jag helt klart att inkapsling står högst på listan. Jag ska försöka förklara varför.
En allmän uppfattning är att inkapsling, och det närbesläktade ”information hiding”, i princip handlar om att göra alla fält i en klass till private och därefter skapa getters och setters för dessa fält. Finito, done, perfectum, klassen är inkapslad efter konstens alla regler.
Jag skulle vilja påstå att ingenting kan vara mer fel. Detta beteende ligger sannolikt bakom de flesta av de buggar som beror på att objekt har felaktiga tillstånd och gör dessutom system betydligt svårare att underhålla och förändra. Att då våra utvecklingsmiljöer erbjuder möjligheten att rutinmässigt generera getters och setters utifrån klassens fält borde snudd på rapporteras som en bugg. Vad den funktionaliteten gör är att uppmana till ett destruktivt beteende, ungefär som om bilhandlaren skulle bjuda på drinkar i samband med att man provkörde en bil. För vissa enstaka medpassagerare skulle det säkert ses som en trevlig service men för den stora massan mest fungera som ett försök att locka till en bedrägligt handling. Brottsprovokation är som bekant förbjudet för den svenska polisen men det är bevisligen tillåtet i Eclipse och IntelliJ.
För mig handlar istället inkapsling om en klass förmåga att gömma interna data och implementationsdetaljer, om att separera API från implementation. Hur självständigt ett objekt är och att reducera objektets beroenden till andra objekt.
En del i att uppnå detta är att alltid se till att ett objekt är färdigt att användas så fort det skapas och därefter erbjuda minsta tänkbara möjlighet att ändra objektets tillstånd. Ofta ser man dock kod som nedanstående:
Account account = new Account();
account.setId(accountId);
account.setType(accountType);
account.setOwner(owner);
listOfAccounts.add(account);
Det som utvecklaren bakom Account gjort här är dels att läcka implementationsdetaljer och dels att lämpa över ansvaret för att objektet skapas korrekt från sig själv till den som ska använda klassen. Detta trots att det med största sannolikhet är den som utvecklade klassen som borde veta vad som krävs för att objektet ska vara redo att användas. Finns det dessutom ytterligare några setters på Account så är det i princip omöjligt för en utvecklare att veta att exempelvis Owner är obligatoriskt. Missar han att sätta Owner så resulterar det om han har tur i ett ”nullpointer exception”. Har han otur slinker buggen igenom och ett halvår senare får han en felrapport i knät på att vissa konton saknar ägare.
En mycket bättre lösning på detta hade varit om Account hade tagit de fält som krävs som argument till konstruktorn. Dessutom hade sannolikt lite polymorfism suttit bra här:
Account savingsAccount = new SavingsAccount(accountId, owner);
listOfAccounts.add(savingsAccount);
Enklare, snyggare och definitivt mycket säkrare.
Ur detta perspektiv är det beklagligt att Java implicit skapar en ”no argument” konstruktor automatiskt om utvecklaren inte skriver en konstruktor själv. Det vore bättre att konstruktorlösa klasser gav kompileringsfel och att man i de undantagsfall där det faktiskt är befogat med en konstruktor utan argument skulle vara tvungen att skriva den själv. Det skulle om inte annat tvinga utvecklaren att tänka till angående implementationen, i alla fall till dess att han skulle upptäcka ”generera konstruktor”-funktionen.
Ett annat problem med såväl getters som setters är att de avslöjar klassens implementationsdetaljer. För att återgå till exemplet med ”savingsAccount”. Så här skulle det mycket väl kunna vara tänkt att räntan räknas ut:
//Calculate daily interest.
double amount = savingsAccount.getAmount();
int rate = savingsAccount.getRate();
double interest = (amount * rate / 100) / 365;
Här finns dessvärre möjligheten att ta del av implementationen av savingsAccounts och när någon plötsligt kommer på att det inte var så klokt att lagra rate som en integer och vill förändra detta så kommer det säkerligen att krävas förändringar på flertalet ställen i systemet. Är det dessutom så att det rör sig om ett publikt API som kunder använder så blir lösningen sannolikt att skapa ytterligare en getRate()-metod och därefter leva med en deprekerad getRate()-metod under resten av programmets livslängd.
Ovanstående exempel är tyvärr ett ganska vanligt tillvägagångssätt där utvecklaren väljer att fråga objektet efter information för att därefter själv processa den på lämpligt sätt. En smidigare lösning är att be objektet som sitter på informationen att även göra beräkningen:
double interest = savingsAccount.calculateDailyInterest();
Koden blir lättare att läsa och nu finns möjligheten att ändra ”rate” från en int till en double utan att det påverkar övrig kod eftersom detta är inkapslat inne i savingsAccount.
Något annat som kan utnyttjas för att kringgå inkapsling är arv. Det är en smal sak att ärva en klass och därefter överrida en metod så att den gör saker som klassen inte alls var designad för. En ändring i superklassen kan dessutom medföra fel i subklassen utan att subklassen ändras alls. Därför tycker jag att man rent generellt bör säkerställa att det inte går att ärva från klasser. Finns det goda skäl till att klassen trots allt ska vara möjlig att överridas så har man som utvecklare ett ansvar att designa sin klass så att inkapslingen äventyras så lite som möjligt.
Det absoluta drömobjektet är det som skapas av konstruktorn och där det sedan inte går att påverka objektets tillstånd alls. Dessa objekt kommer, så länge som de instansieras korrekt, aldrig att vara föremål för buggar som beror på felaktiga tillstånd. De kommer dessutom att vara trådsäkra och kan därmed delas hur mycket som helst mellan olika trådar och olika användare. Sträva därför efter att i möjligaste mån göra dina klasser oföränderliga.
Jag skulle vilja avsluta med en liten checklista för att säkerställa att ett objekt är inkapslat till en acceptabel grad. Följer du nedanstående så kommer du ett stort steg på vägen mot att bygga system som är lätta att underhålla och som sannolikt innehåller få tillståndsbuggar:
- Minimera synligheten på variabler, metoder och klasser (instansvariabler ska exempelvis självklart vara private).
- Generera inte getters och setters rutinmässigt.
- Se till att objektet är klart att använda då det skapas (erbjud ingen default-konstruktor).
- Sträva efter att göra klasserna oföränderliga.
- Se till att din klass inte kan överridas, förslagsvis genom att göra klassen ”final”.
- Erbjud inga metoder som påverkar klassens tillstånd.
- Låt klassen som besitter informationen erbjuda tjänster, inte ”rådata”.

Intressant läsning och kloka synpunkter. Jag delar dock inte synpunkten att utvecklingsverktyg ska vara mer polisiära, utan att brott mot t.ex. LSP minimeras genom medvetenhet (uppnådd t.ex. genom läsandet av såna här artiklar). Det skulle vara lite jobbigt att implementera mer komplexa konstruktionsmönster som t.ex. Builder i en miljö där man inte ens kan ta emot en bjuddrink efter man provkört.
Jag är egentligen inte säker på att jag tycker det själv heller. Det blir ju snudd på en filosofisk ”kollektiv bestraffning”-fråga huruvida det är rätt att alla straffas för att vissa missköter sig.
Min huvudpoäng var dock att jag tror att den sammanlagda tiden som sparas på att använda ”generate getters and setters” äts upp många gånger om av alla problem som det skapar. Men självklart har du rätt, det optimala vore naturligtvis om medvetenheten hos oss utvecklare ökade.