Fördjupande uppsats i datalogi Design Patterns: Elements of Reusable Object-Oriented Software Inledning Jag har läst boken Design Patterns: Elements of Reusable Object-Oriented Software. Boken, som myntade begreppet Design Pattern, kom först ut år 1995. Och den har sedan dess sålt i mer än 500000 exemplar världen över. Den är rankad på andra plats av Amazon Best Sellers inom datalogi[1]. Åsikter verkar gå isär när det gäller att kvantifiera bokens betydelse för mjukvaruindustrin i allmänhet. Vissa värderar den säkert högre än andra[4]. Dock är det nästintill omöjligt att idag vara mjukvaruingenjör någon längre tid utan att komma i kontakt med begrepp som Composite, Singelton och Abstract Factory. I de flesta fall är det begrepp man förväntas känna till, och fast de har studerats i en mängd olika böcker så var det i och med boken Design Patterns som begreppen först föddes. Bakgrund Uppsatsen vänder sig till mjukvaruutvecklare eller blivande utvecklare som tänkt läsa boken Design Patterns, eller någon liknande bok, men av någon anledning inte kommit till skott, och i allmänhet vill veta mer om konceptet Design Patterns. I och med detta antas målgruppen antas vara relativt duktiga programmerare. Bokens exempel är främst i C++ och Smalltalk. I mina exempel håller jag mig till Java. Av den anledning hjälper det om läsaren är bekant med Java, men eftersom språkkonstruktionerna som används är så pass simpla räcker det troligtvis med att kunna ett godtyckligt objektorienterat språk för att förstå exemplen. Eftersom antalet ord som jag har till mitt förfogande är betydligt färre än vad boken hade kommer jag gå igenom en Desing Pattern i detalj, nämligen den så kallade Composite. Jag valde Composite eftersom den enligt mig är den Design Pattern som mest inbegriper det författarna kallar the second principle of object-oriented design : Favor object composition over class inheritance. För mig var citatet ovan ganska förvånande. Jag hade alltid sett arv som något mycket fint. Dock så hade jag också stundtals brottats med problem när det gäller arv. Först nu efter att ha läst boken kan jag tydligt formulera problemet. Och det är något som boken påpekar om och om igen, nämligen den statiska sammansättningen som arv medför. Ett uppenbart exempel är Javas klassbibliotek för att hantera in och utmatning. Det finns en bas-klass för att hantera alla typer av inläsning av inmatningsströmmar som vi kallar. En mängd olika klasser som specialiserar sig på att läsa olika typer av strömmar ärver från : CharArray, String, File, och så vidare. Ofta när man läser från strömmar är det i ens intresse att buffra inläsningen. Det är dock en egenskap som ska vara valfri för varje instans av en skapad läsare. En arvsbaserad lösning är att skapa en ny klass, Buffered, och låta varje specialiserad typ av ärva från den, det vill säga: BufferedCharArray, BufferedString, BufferedFile, och så vidare. Klassdiagrammet ser då ut på följande sätt: 1
CharArray String File Buffered BufferedChar Array Buffered String InputStream Figur 1: Klassdiagram för lösningen som föreslogs ovan. Pilen symboliserar arv. Buffered File Strukturen som visas i figur 1 är, som läsaren säkert redan uppmärksammat, katastrofal ur upprepningssynpunkt. Funktionaliteten som skiljer de olika läsarna åt behöver upprepas i två separata klasser per specialisering (en buffrad och en icke-buffrad). En annan lösning, som också tar hjälp av arv, vore att låta en Buffered-subklass ärva från varje specialisering. Strukturen blir då: CharArray String File BufferedChar Array Figur 2: Den andra lösningen som föreslogs ovan. Buffered String InputStream BufferedFile Inte heller lösningen ovan är särskilt fördelaktig ur upprepningssynpunkt. Visserligen känns denna lösning mer naturlig än den föregående, men den medför ändå en upprepning av koden som möjliggör buffring för varje ny typ av inmatningsström. Istället löser man det i Java med composition. Det vill säga en Buffered ärver från (som i lösning 1), men istället för att låta olika specialiseringar ärva från Buffered låter man Buffered innehålla (compose) en. Buffered är en tillämping av en särskild Design Pattern, nämligen Decorator (sida 175 i boken). Buffered dekorerar en valfri given med buffringsfunktionalitet. En Buffered kan instansieras med en File: reader = new Buffered(new File(new File("foo.txt"))); Lika väl som den kan instansieras med en String: reader = new Buffered(new String("foo")); Den valda strukturen ser ut på följande sätt: 2
CharArray String File Buffered Figur 3: Lösningen med Decorator som finns i Java. Diamanten symboliserar composition. Resultatet är att man slipper implementera ytterligare tre klasser och det finns ingen uppenbar upprepning av logik. Lösningen blir desto mer överlägsen när man vill implementera ytterligare någon valfri egenskap i likhet med buffring. Av en slump så hände det mig bara några veckor sedan. Jag ville implementera en läsare som bara läste rader som matchade ett givet regular expression. Jag kallade klassen Grep, och på samma sätt som Buffered innehåller (compose) den en. Det gör att vi kan skapa en buffrad Grep som läser från fil: reader = new Grep(new Buffered(new File(new File("foo.txt")))); och en obuffrad Grep som laser från en sträng: reader = new Grep(new String("foo")); Hade strukturen från början sett ut som den i figur 2, och jag hade fortsatt på samma spår, hade jag behövt implementera GrepBufferedFile, GrepFile, och så vidare. I längden är en sådan lösning ohållbar om man vill kunna lägga till valfri funktionalitet till varje klass. När man läst redogörelsen ovan får man nästan känslan av att författarna förespråkar composition framför arv i alla situationer och att arv aldrig borde tillämpas. Så är absolut inte fallet, tvärtom så innehåller många Design Patterns arv i någon form. Men författarna tycker sig se ett överutnyttjande av arv när istället compositon skulle ge en mer dynamisk klasstruktur som lämpar sig bättre för eventuella utökningar. Resterande delar av den här uppsatsen är ordnade på följande sätt: först redogörs Composite för, sedan följer några exempel på Design Patterns (mer moderna än de som nämns i boken) som jag själv stött på, och sist kommer en summering och avslutning. Composite Composite heter den Design Pattern som jag valt att fokusera på. Composite, i likhet med Decorator, involverar som sagt composition. Huvudidén med Composite är att låta enstaka objekt behandlas på samma sätt som mängder eller snarare hierarkier av objekt. Det hela uppnås med att alla klasser som deltar implementerar en abstrakt klass som vi kallar Component. Klasserna kan delas in i två grupper Leaf och Composite, klasser utan barn respektive klasser med barn (som också implementerar Component). Oavsett vilken av de två grupperna en klass tillhör, så måste den implementera de operationerna som deklareras i Component på ett meningsfullt sätt. Ofta betyder det att en Component med barn kallar operationen på samtliga sina barn. Ett typiskt exempel är att man vill bygga en hierarki av vy-element. Exemplet i boken bygger på en Component klass: Graphic, och fyra implementerande klasser: Line, Rectangle, Text och Picture. Line, Rectagle och Text är Leaf och Picture är en Composite. Figuren nedan visar klassdiagrammet: 3
Graphic add(graphic) remove(graphic) getchild(integer) Line Rectangle Text Picture add(graphic) remove(graphic) getchild(integer) Figur 4: Ett exempel på vy-element som tillsammans bildar Composite. Tanken är alltså att Picture innehåller (composes) en eller flera Graphics. Implementationen av i Picture kallar draw för alla Graphic-objekt som lagts till med add(graphic). Det går därmed att bygga bilder (Pictures) som innehåller ett godtyckligt antal linjer, rektanglar och texter. Kanske det finaste är att det även går att bygga bilder som innehåller en eller flera andra bilder som innehåller ett godtyckligt antal linjer, rektanglar, texter, och så vidare. Klienten (den delen av koden som använder sig av objekten) vet i regel inget mer än att ett objekt är en Component, eller Graphic i exemplet. Det gör att strukturen enkelt kan utökas till att inkludera fler Component klasser utan att några ändringar behövs i klienten. En implementeringsdetalj som diskuteras i boken är huruvida Graphic ska deklarera metoderna som hanterar eventuella barn. Fördelen med att implementera den koden i Graphic, alltså Component-basklassen, är transparens. Med transparens menas att alla Graphic-objekt, både klasser av Leaf och Composite typ, kan behandlas på samma sätt. Nackdelen är att klienter kan kalla operationer på Leaf som saknar innebörd. Det är inte uppenbart vad add(graphic) innebär för en Line. Man kan låta den inte ha någon effekt alls, men det faktum att en klient kallar add på en Leaf tyder på att något inte står riktigt rätt till, och då kanske det är bättre att signalera det (kasta en Exception). Nackdelen med Composite som tas upp i boken är att det kan göra ens design för generell. Ofta är generellt bra, men låt säga att vi vill begränsa vissa Composite-klasser så att de bara kan innehålla vissa typer av Component. Eftersom alla Composite-klasser implementerar metoder för att hantera barn av valfri Component-typ är det inget som man kan låta kompilatorn sköta. Istället måste sådana kontroller göras vid run-time. Moderna exempel Jag tyckte att det vore intressant att fundera över vilka olika Design Patterns jag stött på i mjukvara som jag själv arbetat med. Boken ger exempel för varje Design Pattern under titeln Known Uses, och ET++ i all ära, men det är här som det faktum att boken kom ut för 15 år sedan märks mest. Nedan följer ett antal riktiga exempel på några av de 23 Design Patterns som listas i boken. Den som går vidare till att läsa boken kan använda listan som komplement till de exempel som redan finns. iphone SDK[1] o Composite Samtliga vy-element ärver från klassen UIView. Bland annat knappar, textfält och bilder ärver från UIView. Man kan lägga till sub-vyer till en UIView med 4
metoden addsubview(uiview), vilket motsvarar add(graphic) från det föregående exemplet. o Chain of Responsiblity Composite, lämpar sig, som boken tar upp, mycket väl till Chain of Responsiblity. Här delegeras ansvar att svara på en interaktion högre upp i hierarkin. Det vill säga om användaren trycker på ett textfält ges textfältet först möjlighet att svara på tryckning innan den skickas uppåt i vy-hierarkin till den UIView som är förälder. Wicket[6] o Composite Component är bas-klassen för alla typer av komponenter som tillsammans utgör en webbsida. Enkla Leaf klasser ärver från WebCompoenent som i sin tur ärver från Component. Composite klasser ärver från MarkupContainer som i sin tur ärver från Component. Här har man alltså valt att inte implementera metoderna som hanterar barn i bas-klassen. o Visitor Hierarkin av komponenter som tillsammans utgör en webbsida kan itereras över genom att kalla visitchildren(visitor) på en MarkupContainer. o Decorator JavaScript strängar som generas av de inbyggda komponenterna för att hantera AJAX kan dekoreras med hjälp av IAjaxCallDecorator. Spring[5] o Singelton En Spring bean är oftast en Singleton (om den inte är en Prototype). o Prototype En Spring bean kan också finnas i flera instanser, då används Prototype för att skapa nya instanser varje gång en särskild bean efterfrågas. o Facade Det finns en mängd exempel på Facade inom Spring-ramverket. Ett exempel är JdbcTemplate som gömmer alla mer eller mindre komplicerade operationer som det innebär att skapa Connection-objekt till en JDBC-databas. Apache Camel[3] o Proxy Bland annat kan man skapa en Proxy för Spring Remoting. Man exporterar då ett Interface till en Proxy-instans på klienten. Klienten kallar metoder på Proxy-objektet ovetandes om att den i bakgrunden kommunicerar via JMS med en server som har den implementerande koden för interfacet. Slutsats Uppsatsen fokuserade konceptet att föredra composition framför arv. En Design Pattern, Composite, som kanske bäst demonstrerar konceptet har gåtts igenom. Förhoppningsvis har det gett läsaren en grundläggande förståelse för varför det i många fall kan vara en fördelaktig design. Jag vet att jag själv gått tillbaka till kod som jag skrivit innan jag läst boken. I flera fall har jag kunna omstrukturera kod som förlitat sig på arv till att använda sig mer av composition. 5
Referenser 1. Amazon.com. http://www.amazon.com/gp/bestsellers/books/280309/ref=pd_zg_hrsr_b_3_4. Senast besökt 2011-12-15. 2. Apple. http://developer.apple.com/devcenter/ios. Senast besökt 2011-12-25. 3. Camel. http://camel.apache.org/tutorial-jmsremoting.html. Senast besökt 2011-12-25. 4. InfoQ. 2007. http://www.infoq.com/news/2007/07/gofcriticism. Senast besökt 2011-12-15. 5. Spring. http://www.springsource.org. Senast besökt 2011-12-25. 6. Wicket. http://wicket.apache.org. Senast besökt 2011-12-25. 6