Lektion 32 Övningar Korta punkter Jag vill ha en redovisning från alla grupper där ni går igenom person för person vad personen har ansvarat för och vad och vem personen har parprogrammerat på. Ta även med ickeprogrammeringsrelaterade uppgifter, t.ex. Har fungerat som projektledare, Har ansvarat för dokumentation eller liknande. Om någon av er eller hela gruppen vill lämna in ovanstående individuellt är det ok. Dock vill jag ha ett dokument från gruppen ifall inte hela gruppen väljer att lämna in individuellt. Klart: Torsdag nästa vecka. SOLID SOLID är en akronym för fem stycken designprinciper som används vid mjukvaruutveckling. SOLID står för: Single Responsibility Principle Open/Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle Nedan följer en definition av de olika principerna: Single Responsibility Principle: Varje enhet (objekt/metod/etc) skall endast ha ett ansvar. Dvs. istället för att skriva en metod FormatAndRenderHTML som innehåller implementationer för både formattering och rendering så bör metoden brytas isär i FormatHTML och RenderHTML. Ett annat vanligt sätt att uttrycka denna princip är An entity should have one, and only one reason to change. Denna princip medför att enheten kommer bli lättare att underhålla (en ändring av renderingen i exemplet ovan kommer inte påverka formatteringen), koden kommer bli mer lättläst och lättare att testa (Lägg märke till att man för det mesta pratar om klasser/objekt när det gäller denna princip motiveringen fungerar dock lika bra för metoder). Open/Closed Principle: Varje enhet bör vara Öppen för utökningar av funktionalitet, men stängd för modifiering av befintlig funktionalitet. T.ex. Bör du inte ändra implementationen av string-klassen i c# (modifiering), men du kan skriva en extension method för string-klassen om det skulle behövas (utökning). Denna princip gäller dock inte bara klassbibliotek utan även egna implementationer. Om du behöver utökad funktionalitet och åstadkommer det med en extension method så innebär det att den nya funktionaliteten endast kan medföra oönskat beteende på de ställen där extensionmetoden används. Om du istället ändrar en
definition av en befintlig metod i en mogen applikation så innebär det att det finns potentiella oönskade effekter på alla ställen klassen används (Lägg märke till att implementation av interface, arv och wrapper-funktioner är andra sätt att utöka funktionalitet utan att ändra basimplementationen). Denna princip medför att ändringar är mindre troliga att medföra oönskat beteende (t.ex. buggar). Koden kommer troligtvis också bli enklare att underhålla. Liskov Substitution Principle: Ett objekt i ett program bör kunna ersättas med objekt som ärver från detta objekt utan att programmet slutar fungera som tänkt. Dvs. om du implementerar en klass, 3D Chart som ärver utav klassen Chart så bör 3D Chart kunna användas på alla ställen i programmet där Chart används utan att programmet uppvisar oönskat beteende (Namnet på principen kommer f.ö. från namnet på personen som definierade principen). Denna princip medför att användning/implementation av en subtyp (arv) är mindre trolig att medföra oönskat beteende (t.ex. buggar). Koden kommer troligtvis också bli enklare att underhålla. Interface Segregation Principle: Man bör sträva efter att använda många, specifika, interface, snarare än ett interface som täcker flera saker eller allt. Om du t.ex. vill ha objekt som dels skall kunna sparas i en databas och dels skall kunna sparas till filsystemet så bör objektet implementera ett interface för objekt-i-databas-funktionaliteten (t.ex. IEntity som garanterar ett ID) och ett separat interface för objekt-i-filsystemet-funktionaliteten (t.ex. ISerializable som garanterar en metod för att serialisera objektet till filsystemet). Detta gör att ifall vi senare behöver implementera objekt som endast skall sparas till disk så har vi redan ett interface för det och implementationen behöver inte innehålla onödiga funktioner eller liknande för databas-funktionalitet. Denna princip medför att konkreta implementationer hålls rena (inga extra metoder behöver implementeras för att de finns i interfacet). Detta innebär i sin tur att koden blir enklare att underhålla och modifiera. Dependency Inversion Principle: Denna princip innebär att Högnivå-objekt (Förenklat: ett objekt som på något sätt hanterar flera andra objekt t.ex. ett repository) ej skall vara beroende av Lågnivå-objekt. Detta skiljer sig från äldre objektorientering där högnivåobjekt ofta är beroende av konkreta implementationer av lågnivåobjekt. Dependency Injection med interface är ett sätt att följa denna princip. Dvs. om ett UserRepository är implementerat för att hantera objekt som implementerar IEntity snarare än konkreta Userobjekt så följer vi denna princip. Ett annat sätt att uttrycka denna princip som man relativt ofta hör är Depend on abstractions, not concretions/implementation. Denna princip medför att förändringar och återanvändning av kod blir enklare att genomföra. Koden kommer troligtvis också bli enklare att underhålla.
Ursprungligen är de avsedda för objektorienterad programmering, men principerna kan, när man förstår dem, i väldigt stor utsträckning användas även i programspråk som inte har ett starkt stöd för objektorientering. Det skall också nämnas att man väldigt ofta använder SOLID som ett verktyg för analys inför refactoring. Personligen tycker jag inte det är så viktigt att lära sig exakt vad akronymen står för, det viktiga är att förstå anledningarna till att principerna används och att man kan applicera dessa tankesätt på sina implementationer (Om akronymen hjälper dig att komma ihåg principerna så är det ju dock bra ;) SOLID som helhet kan ta lite tid att få fullständig förståelse för (Se nästa stycke för en delförklaring till detta). Det kan därför vara bra att med jämna mellanrum fördjupa sig/repetera dessa principer. De är tänkta att appliceras på eller åtminstone övervägas för allt du implementerar, dvs. det är inte principer som är tänkta att plockas fram någon gång då och då eller för väldigt specifika ändamål. Lägg märke till att dessa principer i väldigt stor utsträckning syftar till förenklat underhåll av källkod (Detta är dock inte det enda syftet med dessa principer som ni kan utläsa ovan). Fördelarna med SOLID är därför ibland inte direkt uppenbara vid en implementation från scratch. Detta gör inte principerna mindre viktiga, som tidigare nämnt är underhåll av källkod normalt sett den överlägset mest kostsamma och tidskrävande delen av utveckling. Övningar Utgångspunkten för övningarna är v0.0 av Lektion32 repot. S Single Responsibility Principle (SRP) 1. Titta på klassen Models/Entities/User.cs Denna klass bryter mot SRP, då den har flera olika ansvarsområden. Vilka ansvarsområden har klassen? 2. Hur gör vi refactoring för att se till att klassen följer SRP? O Open/Closed Principle (OCP) 3. Ponera att Repository-klassen är testad, släppt till produktionsmiljö och anses stabil. Det har kommit in ett nytt krav att en metod, FindUserByEmail(string email) måste implementeras. Att implementera denna metod direkt i Repository-klassen hade brutit mot OCP. Finns det några andra anledningar till att det är en dålig idé att implementera denna metod direkt i Repository-klassen? 4. Hur implementerar vi denna metod samtidigt som vi följer OCP? (Tips: Låt en ny klass, UserRepository ärva från Repository<User> och implementera ett Interface, IUserRepository. Vad bör IUserRepository innehålla i det är fallet? Vad är det vi vill implementera?)
L Liskov Substitution Principle (LSP) Exempel för LSP blir lätt väldigt förenklade, till den grad att det är svårt att se någon riktig nytta med det (Se: A typical violation under http://en.wikipedia.org/wiki/liskov_substitution_principle ), eller så blir exemplen så avancerade att de är svåra att tränga in i. Vi använder här uppgift 3 + 4 för att exemplifiera LSP. 5. Antag att vi innan vi implementerade UserRepository använde oss av Repository<User> på flera ställen i källkoden. Vad hade hänt om vi ersatt alla förekomster av Repository<User> med UserRepository? Hade detta lett till några oönskade beteenden i programmet? Har detta några fördelar? Följer vi LSP? 6. Hur hade vi kunnat bryta mot LSP i vår implementation av UserRepository? I Interface Segregation Principle (ISP) Under Models/ExportableDataSet ligger PageHitStatistics.cs och UserStatistics.cs. Tanken är att dessa klasser skall representera dataset som är underlag för olika typer av statistik för vår sida. Denna statistik skall kunna tas ut i olika format, t.ex. som PDF, Excel-rapport eller som ren text. Statistiken innehåller olika typer av objekt och har olika komplexitet, vilket gör att det inte är lämpligt att exportera samtliga typer av dataset till samtliga typer av rapporter/filtyper. I detta fallet så antar vi att UserStatistics är väldigt komplex och ej lämpligt att ta ut i något annat format än Excel, medans PageHitStatistics är ganska enkel och lämplig att ta ut i graf-form, antingen som en bild eller PDF. När implementationen gjordes så bestämdes det att alla implementationer skall ske mot interface (IExportableSet.cs) för att göra det enkelt att senare utöka antalet rapporter. Förutom metoder för att exportera till Excel, Image och PDF så lades det även till metoder för att exportera till SQL, Text och CSV för att framtidssäkra applikationen. I PageHitStatistics.cs och UserStatistics.cs representerar metoder som är implementerade med new NotImplementedException(); exporter som ej skall vara möjligt att göra för det aktuella datasetet. Övriga metoder representerar fullständiga implementationer. 7. På vilket sätt bryter denna implementation mot ISP? Vilka problem innebär detta? 8. Hur kan vi göra refactoring av denna implementation för att följa ISP? ( int COUNT() - metoden är tänkt att vara implementerad för samtliga implementationer av dessa statistikset). 9. Är framtidssäkringen i form av abstrakta metoder för export till SQL, Text och CSV en bra idé? OBS! Lägg märke till att Implementationerna under ExportableSet potentiellt bryter mot SRP (Detta beror lite på hur den faktiska implementationen ser ut men spontant ser det ut som att klasserna både ansvarar för att representera data och exportera data). En lösning på detta (det finns fler) hade t.ex. kunnat vara att ta bort alla export-metoder både ur interfacen och klasserna i Exportable Dataset.
Vi bygger sedan en ny klass StatisticsExporter som innehåller ett antal statiska metoder för de olika typerna av exporter vi har. T.ex. StatisticsExporter.ExportToPDF(IPDFExportable statisticstoexport) Där de ExportableDataSet som skall kunna exporteras till PDF implementerar interfacet IPDFExportable. Man hade också kunna tänka sig något i stil med StatisticsExporter.Export(ExportStrategy.PDF, statisticstoexport) för en något mer dynamisk lösning. Jag ville dock hålla exemplet så enkelt som möjligt i förevisningssyfte, så jag har bortsett från att vi bryter mot SRP här. D Dependency Inversion Principle (DIP) DIP innebär helt enkelt i praktiken att klasser inte bör vara beroende på konkreta implementationer av andra klasser, utan istället av Interface. Vi har tidigare i kursen sett hur denna princip kan användas för att snabbt ändra vilken implementation av repository man vill använda (DB- eller testimplementation) m.h.a. Dependency Injection, Interface och Ninject. 10. Förklara på vilket sätt en Ninject-binding av Repository använder sig av DIP (Du kan titta på NinjectControllerFactory.AddBindings + Konstruktorn i HomeController för ett exempel på detta. Hur hade en liknande implementation, som ej använder Ninject och inte följer DIP sett ut? (Tips: den privata variabeln i HomeController hade varit en konkretimplementation ej en interface-variabel). 11. Om du hade implementerat ett TestRepository som implementerar interfacet IRepository och velat använda denna implementation istället vad hade du behövt ändra för att detta repository skall användas av samtliga Controllers? (Tips: Det hade räckt att ändra en rad i NinjectControllerFactory.AddBindings-metoden) Länkar/Vidare läsning: Om du utgår från Wikipedia-sidan ( http://en.wikipedia.org/wiki/solid ) så kan du fördjupa dig något, men framförallt hitta vidare länkar och lästips om samtliga principer i SOLID. Vidare kan det vara intressant att läsa artiklar/böcker av Robert C. Martin. Det var Robert som myntade akronymet SOLID (även om han nödvändigtvis inte uppfann alla del-principer). Robert (eller Uncle Bob) har väldigt mycket att säga om SOLID, Software Craftmanship, agila metoder, etc. Robert C. Martin har varit väldigt tongivande för stora delar av den agila rörelsen och var även en av de som lanserade den agila rörelsen iom. Manifesto for Agile Software Development 2001. Man skall dock vara medveten om att han ofta är extremt kompromisslös och fast i sina principer Personligen gillar jag detta eftersom det tvingar mig att värdera/överväga det han säger, men det finns naturligtvis varierande åsikter kring detta.