Vad är egentligen Unity?
Jag har tittat lite på Unity för att se hur det kan underätta skapandet av flexibla applikationer med hjälp av mönster som IoC och DC (Inversion Of Control och Dependency Injection), och än så länge tycker jag att det är riktigt trevlig och ganska enkelt att komma igång med. När jag också ser hur fler tekniker och ramverk från Microsoft nyttjar just Unity så känns det relevant att försöka mig på en förklaring, delvis för mig själv, men förhoppningsvis så kanske du som läser detta kommer till insikt också om det här passar dig?
För att kunna använda Unity så är det bra att känna till mönstrena som jag nämnde tidigare: Inversion of Control och specifikt Dependency Injection, och om du vill att jag ska ge mig in på att försöka förklara dem också så hör gärna av dig. Det tog lång tid innan jag insåg hur jag kan använda dem och vad de ger för fördelar, men det kan hända att du ändå inser nyttan av båda dessa mönster efter att ha läst det här inlägget.
Komma igång med Unity
Innan vi kan dra nytta av det här ramverket så behöver vi ladda hem ramverket från CodePlex. Primärt så behöver jag följande assemblys och namnrymder:
Microsoft.Practices.Unity
Microsoft.Practices.Unity.Configuration
Dessa assembly har också krav på att lägga till en ytterligare assembly som ursprungligen kommer ifrån Enterprise Library men som följer med Unity och det är:
Microsoft.Practices.ObjectBuilder2
Så efter att jag skapat ett nytt projekt i Visual Studio 2008, i det är fallet ett C# projekt så ser mina referenser ut som bilden här bredvid. Det kan vara värt att nämna att jag också har lagt till System.Configuration och tagit bort en del referenser som jag inte kommer att använda mig av i det här inlägget. Microsoft.Practices.Unity.Configuration är just för att erbjuda möjligheten att skapa en konfigurationsfil där vi kan beskriva hur Unity ska agera istället för att specificera detta i kod.
Lite klasser och gränssnitt som vi kommer att använda
Så låt mig nu ta och skriva lite kod som jag kommer att dra nytta av i det här exemplet. Jag lägger till en klass som jag kallar Customer som innehåller två publika egenskaper, ID och namn. Koden för klassen är helt enkelt:
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
}
Tanken är att jag ska hämta instanser av kunder från databasen men vill underlätta testningen av applikationen genom att vid debuggning inte gå mot databasen utan helt enkelt skapa en testklass eller som i det här fallet även kallat ett mock-objekt. För att skapa min arkitektur så definerar jag ett enkelt gränssnitt (interface) som jag döper till IDAL och sedan två stycken klasser som implementerar detta gränssnitt: SQLDataAccess och DALMock. Klassdiagrammet här bredvid beskriver och visar hur dessa ser ut i Visual Studio 2008 och hur de förhåller sig till varandra. Koden för gränssnittet är helt enkelt:
public interface IDAL
{
Customer GetCustomer(int id);
}
Koden för SQLDataAccess ser ut som följande, observera här nu också att datatåkomsten i sig är inte intressant i det här exemplet utan det är Unity som jag snart kommer att förklara, därför har jag inte heller i SQLDataAccess brytt mig om att skapa någon vettig datatåtkomst, hoppas att du har överseende med det.
public class SQLDataAccess : IDAL
{
public Customer GetCustomer(int id)
{
Console.WriteLine("Accessing database and fetching Customer {0}",id);
Customer customer = new Customer();
// Real data access here
return customer;
}
}
Mock-objektet som kommer att användas vid testning heter som jag nämnde DALMock och koden ser ut som följande, som du säkert ser påminner detta mycket om det "riktiga" dataåtkomst objektet men syftet är som sagt att visa Unity.
public class DALMock : IDAL
{
public Customer GetCustomer(int id)
{
Console.WriteLine("Fetching a fake customer from mock");
Customer fakeCustomer = new Customer { ID = 1, Name = "Johan" };
return fakeCustomer;
}
}
För att följa andra rekommendationer och mönster i min applikation så vill jag inte direkt i main-metoden hämta kunderna utan istället skapa ett så kallat Repository som inkapslar all funktionalitet mot och för kund-objekten. Det är också den här klassen som verkligen kommer att kunna dra nytta av Unity. Den här klassen består av en privat variabel som har typen IDAL, alltså en instans av gränssnittet som båda ovanstående klasser implementerar. CustomerRepository har också en publik konstruktor som tar en parameter av samma gränssnitt vilket ger mig möjlighet att instansiera klassen med en specifik instans av data-åtkomst för just det scenario som jag vill använda mig. Den sista metoden som CustomerReposityr har i mitt fall är en DoWork metod som får agera funktionalitet för att hämta en kund från databasen och sedan skriva ut dess namn på konsoll-fönstret. Koden för CustomerRepository ser ut som följande:
public class CustomerRepository
{
private IDAL _dal;
public CustomerRepository(IDAL dal)
{
this._dal = dal;
}
public void DoWork()
{
Customer customer = _dal.GetCustomer(1);
Console.WriteLine("Name: {0}", customer.Name);
}
}
Vad jag nu kan göra i min main-metod och som potentiellt kan ha varit sättet jag instansierat CustomerRepository-klassen innan jag använd mig av Unity är följande kod:
CustomerRepository customerRepository1 = new CustomerRepository(new DALMock());
customerRepository1.DoWork();
CustomerRepository customerRepository2 = new CustomerRepository(new SQLDataAccess());
customerRepository2.DoWork();
Den första instansen kommer att använda mock-objektet för att testa funktionaliteten och den andra kommer i det här fallet att gå mot databasen (eller borde ha gjort om jag hade valt att implementera det också).
Den här användningen av gränssnitts-instanser inkapslade i klasser som instansieras i konstruktor kallas alltså för "Dependency Injection" eller ännu mer specifikt "Constructor Injection" eftersom jag i konstruktorn skickar med en instans av den klass som min repository-klass beror på. Det finns ytterligare sätt att hantera detta på, men i det här fallet fungerar exemplet relativt tydligt. Men nu är det dags för att introducera Unity.
Ett första försök med Unity
Så vad kan då Unity bidra med. Jo istället för att skapa instansen av dataåtkomst-klassen explicit så kan jag låta Unity göra det åt mig, genom att berätta för Unity hur den ska mappa IDAL gränssnittet mot en specifik typ som jag vill instansiera. Detta görs genom att först skapa en instans av en UnityContainer.
IUnityContainer container = new UnityContainer();
container.RegisterType(typeof(IDAL), typeof(SQLDataAccess));
Den andra raden i koden ovan registrerar också att jag vill att alla variabler av typen IDAL ska instansieras med typen SQLDataAccess. Sedan ber jag Unity hämta en korrekt instans av CustomerRepository klassen, populerad med sina beroenden på korrekt sätt utan att jag själv behöver göra det i koden med något av följande alternativ som visar på en del av metoden Resolves överlagrade versioner.
CustomerRepository customerRepository = container.Resolve<CustomerRepository>();
CustomerRepository customerRepository = container.Resolve(typeof(CustomerRepository)) as CustomerRepository;
CustomerRepository customerRepository = container.Resolve<CustomerRepository>("CustomerRepository");
Själv gillar jag den första versionen (den generiska utan sträng-parameter) bäst för det här exemplet, men alla ger i det här fallet samma resultat, jag får en instans av min CustomerRepository klass som är populerad med den registrerade typen.
Resultatet på konsollen blir:
Accessing database and fetching Customer 1
Name:
-- Press any key to terminate --
Jag nu ändrar min registrering av datatypen IDAL till att istället registrera DALMock med följande kod:
container.RegisterType(typeof(IDAL), typeof(DALMock));
Och om jag då kompilerar och kör koden på nytt så blir resultatet:
Fetching a fake customer from mock
Name: Johan
-- Press any key to terminate --
Jag tror inte att du blir övertdrivet imponerad av detta, men betänk då flexibiliteten om min IDAL instans i sin tur hade beroenden som också behövde populeras på korrekt sätt, då blir Unity helt plötsligt oerhört attraktivt och flexibelt att använda.
Ytterligare flexibilitet med konfiguration
Men jag började också den här artiklen med att lägga till referenser till System.Configuration och även Microsoft.Practices.Unity.Configuration för att kunna lägga beteendet för Unity i konfigurationen. Så jag lägger nu till en App.Config fil till mitt projekt och börjar i den filen med att registrera sektionen för Unity genom att ersätta den generade koden med följande:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration,
Version=1.1.0.0,
Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
</configSections>
</configuration>
Nu kan jag också lägga till en sektion enligt följande (som alltså läggs till innan den avslutande "configuration"-taggen.
<unity>
<containers>
<container>
<types>
<type type="UnityTest.IDAL, UnityTest" mapTo="UnityTest.DALMock, UnityTest"/>
</types>
</container>
</containers>
</unity>
För att se till att min UnityContainer konfigureras med inställningarna i konfigurationen så uppdaterar jag instansiseringen av min UnityContainer till följande:
IUnityContainer container = new UnityContainer();
UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(container);
Det betyder också att jag kan ta bort kodraden som registrera min IDAL typ explicit i och helt enkelt sedan instansiera min CustomerRepository som tidigare, resultatet blir som väntat att jag hämtar kunden från just mock-objektet.
Använda flera "containers"
En användning som potentiellt kan vara intressant är att skapa olika "containers" i konfigurationen för specifik användning, exempelvis så kan jag tänka mig att när jag debuggar så vill jag använda mock-objektet men när jag testar koden i "release"-läge så vill jag använda den riktiga dataåtkomsten, så i mitt fall skapar jag två stycken "container"-sektioner i konfigurationen enligt följande:
<containers>
<container name="Debug">
<types>
<type type="UnityTest.IDAL, UnityTest" mapTo="UnityTest.DALMock, UnityTest"/>
</types>
</container>
<container name="Release">
<types>
<type type="UnityTest.IDAL, UnityTest" mapTo="UnityTest.SQLDataAccess, UnityTest"/>
</types>
</container>
</containers>
Sedan uppdaterar jag följande rad:
section.Containers.Default.Configure(container);
Den raden ersätter jag med följande kodsnutt:
#if DEBUG
section.Containers["Debug"].Configure(container);
#else
section.Containers["Release"].Configure(container);
#endif
Vad som kan nämna här är att Containers har alltså en Default-instans som inte har ett speficikt namn, alternativt så kan jag använda en "Dictionary" som hämtar en namngiven instans av min "container" från konfigurationen. Det bör också nämnas att om jag har "Debug" valt i Visual Studio 2008 som konfiguration i utvecklingsmiljön som hjälper det inte att bara trycka Ctrl-F5 för att få "Release", utan jag måste helt enkelt välja rätt konfiguration och sedan exekvera. Men det kände du säkert redan till.
Den här artikeln vart lite längre än vad jag ursprungligen hade räknat med, men jag hoppas att det var värt läsningen och att du nu ser lite av nyttan med Unity. Glöm inte att ge kommentarer, och om du vill bedöm gärna artikeln med "stjärnsystemet" som kan nyttjas om du går till inläggets riktiga sida och inte bara på framsidan av bloggen.
Comments
Anonymous
September 16, 2008
Hur vanligt är det att man använda en IoC komponent för mockning vs att använda ett Mocknings ramverkt som Rhino mock?Anonymous
September 17, 2008
Kimmen: Det är inte IoC vs. Mocks, utan snarare Ioc OCH Mocks. Mock-frameworks som t.ex. Rhinomocks förenklar implementationen av mock-objekten, alltså koden i "DalMock"-klassen i inlägget. IoC-ramverket ger dig möjlighet att konfigurera om det "riktiga" eller mock-implementation skall användas. Så de två teknikerna kompletterar varandra. Johan, finns något sätt att få konfigurationen mer typsäker i Unity, så att kompilatorn kan utföra mer kontroller? Tänker något i stil med structuremaps i StructureMap http://structuremap.sourceforge.net/Attributes.htm eller fluent NHibernate http://blog.jagregory.com/2008/08/08/introducing-fluent-nhibernate/Anonymous
September 21, 2008
Tänkte bara säga att det är precis den här typen av blogginlägg jag har saknat från er svenska MS evangelister, dvs exempel på hur man tex kan använda EntLib i sina egna tillämpningar. Snyggt jobbat! Keep it up.