C++-arkivet
|
Interfaceskolan del 1:Vi börjar med tipset:
Kort om interface innan vi kör igång
För att kunna lägga de anställda i samma lista måste alla i listan ha samma typ. Denna typ måste förstå anrop till funktionen calculateSalary(). Vad vi behöver här är en basklass (se Kodexempel 1):
Ett interface är en basklass med bara strikt virtuella funktioner (eng: pure virtual, funktionshuvudet avslutas med = 0). Vårt interface EmployeeInterface beskriver vad alla anställda måste kunna göra, dvs beräkna sin egen lön. Generellt kan man säga att ett interface beskriver vad man kan göra utan att beskriva hur man ska göra det! Eftersom vi inte talar om hur saker och ting ska göras finns inga implementationsdetaljer i ett interface: inga medlemsvariabler och inga funktionsdefinitioner (dvs implementation av funktion, känns igen på måsvingarna { } ). Det enda undantaget är destruktorn som måste ha en definition, annars får du en tillsägelse när du länkar ditt program. Basklassens destruktor anropas ju alltid sist, efter destruktorerna i nedärvda klasser. Vi sammanfattar egenskaperna hos ett interface i Tabell 1:
Vi låter vår chefsklass och vår programmerarklass ärva från EmployeeInterface. Sen låter vi listan innehålla pekare till EmployeeInterface. Vi låter pekarna peka på chefs- och programmerarobjekten och anropar calculateSalary() på varje pekare. Vi använder alltså samma anrop på alla pekare, men anropet slutar antingen i ett chefsobjekt eller ett programmerarobjekt. Detta kallas polymorfi som betyder "många former". Polymorfi är det som gör objektorienterad programmering så kraftfullt. Interface är polymorfi i dess renaste form (en hel klass skapad för polymorfi och inget annat). Tre exempel på användningsområdenMan kan använda interface till många olika saker. Jag har valt att fokusera på tre konkreta exempel:
Denna artikel kommer bara att behandla den första punkten av de tre: Du vill göra det svårt att använda dina klasser på fel sätt! De andra två punkterna kommer i en senare artikel. Nu kör vi. Användningsområde 1: Du vill göra det svårt att använda dina klasser på fel sätt!Låt säga att du har en klass med ett ganska stort gränssnitt. Din klass har många funktioner för olika användningsområden. Trots detta har din klass ett väldefinierat ansvar, så du är nöjd med dess design. Antag t.ex. att du håller på att skriva en databas som i Kodexempel 2:
Vi ser att gränssnittet för databasen består av två delar: en administratörsdel och en användardel. Det är inte helt oproblematiskt som vi snart ska se. Nu vill någon använda din databas. Han/hon vill skriva en funktion calculateSalary(const Key &employeeNumber, const Database &db) som slår upp den anställde i databasen och beräknar lönen. När vi skickar med databasen får den som implementerar calculateSalary() tillgång till både administratörsdelen och användardelen av gränssnittet. Det är här vi får problem. Naturligtvis är det olämpligt att calculateSalary() läser ut administratörsinformation med getAdminInfo() eller formaterar hårddisken med funktionen format() (om argumentet db inte hade varit const). Funktionen calculateSalary() får helt enkelt för mycket makt om den får tillgång till hela databasen! Det är direkt olämpligt och väldigt lätt att göra fel. Det kan också hända att calculateSalary() använder sig av delar av administratörsfunktionerna som sedan behöver byta gränssnitt. Dessa ändringar kommer att slå på oväntade ställen i koden med ökade kostnader och risker som följd. Stycka upp ditt gränssnitt i mindre delarSå hur ger jag calculateSalary() tillgång till användardelen av gränsnittet utan att ge den tillgång till administratörsgränssnittet? Det finns flera sätt att angripa detta problem, men här handlar det förstås om interface. Vi pratade om att ett interface beskriver vad som ska göras (men inte hur det ska göras). Alltså skapar vi ett interface som beskriver vad som finns tillgängligt när man som användare vill komma åt databasen (se Kodexempel 3):
Lägg märke till att alla funktioner i interfacet är strikt virtuella. Lägg också märke till att vi måste ha en virtuell destruktor om vi vill använda vårt interface som basklass. Annars är vårt program ogiltigt (standarden säger "undefined behavior" vid delete på nedärv klass). Det är knappt några ändringar alls som behövs i vår databasklass. Vi ärver bara in vårt nya interface (se Kodexempel 4). Vill vi vara extra tydliga kan vi ändra i Database och lägga till virtual på funktionerna writeData() och readData(), men det är valfritt. Även utan virtual blir writeData() och readData() virtuella eftersom de är virtuella i basklassen.
Exponera precis så lite du behöver av ditt gränssnittHur gör vi nu när vi vill använda vårt interface istället för hela databasklassen? Vi låter kompilatorn konvertera databasen till en referens till basklassen genom t.ex. Database db; DatabaseUserInterface &dbIf = db;. Utan att oroa oss kan vi sedan ge referensen dbIf till alla delar av koden som behöver användardelen av gränssnittet. Om man bara har tillgång till interfacet har man inte så mycket val när man ska skriva calculateSalary och skapar därför en calculateSalary(const Key &employeeNumber, const DatabaseUserInterface &db). Man tvingas använda användardelen av gränssnittet och calculateSalary() kan nu inte komma åt funktionerna getAdminInfo() och format(). Din kod blir lättare att förstå tack vare det smalare gränssnittet och det blir väldigt mycket svårare att göra fel. Vi sammanfattar hur vi gått tillväga i Tabell 2:
CalculateSalary är ett bra exempel på hur statisk typning samarbetar med dynamisk uppslagning. Den statiska typen i calculateSalary är DatabaseUserInterface. Därför finns bara användardelen av gränssnittet tillgängligt i calculateSalary. I och med att readData är virtuell i DatabaseUserInterface kommer man få en dynamisk uppslagning (polymorfi). Kompilatorn ser att objektet bakom referensen db är av typen Database och därför anropas funktionen redaData i Database. Om vi har delar av vår kod där vi bara får komma åt administratörsdelarna av databasen så går det att ordna förstås. Vi skapar ett interface DatabaseAdminInterface och ärver in även det (dvs vi får två basklasser till Database). Hoppas att detta hjälper dig skriva bättre program i framtiden. I nästa artikel pratar vi om hur man använder interface för att bryta beroenden, få bättre testbarhet och därmed bättre kvalitet på sin kod. Lycka till med dina interface! Relaterade artiklar:
|