U poslovnim aplikacijama često se javlja potreba za sličnom funkcionalnošću nad entitetima različite vrste u sistemu. Na primer, u trgovini postoje različiti tipovi dokumenata, kalkulacije (ulaz robe), transferi (izlaz robe), nivelacije (promena cena), koji su po podacima koje sadrže vrlo slični. Nad ovim dokumentima se izvršavaju i istoimene akcije: import, eksport, knjiženje, posledice akcija zavise od tipa dokumenta. Kada se knjiži ulaz robe, pored kreiranja odgovarajućeg dokumenta, menjaju se količine robe na stanju, kada se knjiži promena cena, jasno je da se ne menjaju količine robe, ali se zato ažurira cenovnik. Verovatno u glavi već imate primer iz industrije kojom se trenutno bavite.
Cilj je rešenje poslovnog problema ovog tipa, ali i kôd organizovan tako tako da bude što lakši za održavanje i proširivanje. Idealno je rešenje koje dozvoljava da se doda novi slučaj, tj. u našem primeru novi tip dokumenta, ali i nova akcija, na primer izvoz u excel, bez ili sa što manje izmena na postojećem izvornom kôdu. Ovaj problem je poznat i dovoljno čest da je dobio i ime The Expression Problem.
Prikazaću primer rešenja u strukturnom, uz pomoć jedne klase i switch strukture, i objektno orijentisanom pristupu, koristeći polimorfizam i neku vrstu naivnog factory pattern-a. Da bi kôd bio jednostavniji neću koristiti inicijalni primer iz trgovine, već standardne kuce i mace. Dakle, pravimo softverski model koji ce opisati životinje, dovoljne su nam dve vrste Cat i Dog, koje imaju ime Name, i dve funkcije Walk i Talk.
Ukoliko želite da pratite priču kroz kôd, solution sa primerima u C# možete skinuti ovde, sadrži četiri projekta, svaki je samostalna konzolna aplikacija sa primerom. Potrebno je da na računaru imate instaliran Visual Studio 2010.
Strukturni pristup – klasa i switch
Najjednostavniji pristup je da napravimo jednu klasu Animal, koja će predstavljati sve životinje. Na osnovu tipa koji možemo da pamtimo u promenljivoj ili polju klase određujemo vrstu, odnosno da li je u pitanju kuca ili maca. Dakle u konstruktoru dajemo ime objektu, i imamo dve metode Walk i Talk, koje prihvataju tip, odnosno vrstu životinje kao parametar.
Listing 1. StructuredAnimals1 klasa Animal. Kompletan primer možete videti u projektu StructuredAnimals1.
Evo kako izgleda primer korišcenja ove klase:
[csharp]
var myCat = new Animal("Garfield");
myCat.Walk("Cat");
myCat.Talk("Cat");
[/csharp]
Vidite ponavljanje parametra, jer pri svakom pozivu metode šaljemo i podatak o vrsti životinje, ovo je moguće rešiti tako što bi se ovaj podatak pamtio kao polje klase, neka bude Name. Drugi nedostatak koji uočavam je da ukoliko napravimo grešku u kucanju i pozovemo metodu Talk sa vrednošću parametra „Cats“, umesto „Cat“, metoda Talk će se uredno izvršiti, međutim neće uraditi očekivano, tj. neće se izvršiti ni jedan slučaj u okviru Switch bloka. Naravno vrsta životinje može da postane enum, tako da iskoristimo prednosti strongly typed jezika i otkrijemo greške u kucanju u vreme kompajliranja.
Sa ova dva unapređenja dobijamo solidno rešenje, u stilu strukturnog programiranja, koje dopušta i da imamo listu (niz) objekata koji pripadaju našoj klasi a definišu različite vrste životinja.
Listing 2. StructuredAnimals2 klasa Animal. Kompletan primer u projektu StructuredAnimals2.
[csharp]
var myCat = new Animal("Garfield", AnimalType.Cat);
myCat.Walk();
myCat.Talk();
[/csharp]
Kada želimo da dodamo novu akciju, dovoljno je da to uradimo na jednom mestu, u klasi Animal. Medutim ukoliko želimo da dodamo novu vrstu životinja, u svakoj metodi potrebno je dodati novi case. Ovaj pristup zahteva izmenu klase i rebuild za dodavanje nove akcije i za dodavanje novog tipa, odnosno nove vrste životinja. Reusability je loš, dolazimo u iskušenje da kopiramo blok kôda u novi case sa malim izmenama, pored toga, kako code base raste nastaje prava šuma grananja u metodama što postaje noćna mora za održavanje. Da li možemo da smislimo bolje rešenje?
Polimorfizam
Objektno orijentisani jezici nam daju alat za rešavanje ovog problema polimorfizam, preciznije subtyping polymorphism. Ideja je da definišemo osnovni interfejs, eventualno i neko podrazumevano ponašanje u osnovnoj klasi, u našem slucaju Animal. Zatim svaku vrstu definišemo podklase koje nasleduju osnovnu klasu Animal.
Listing 3. OOPAnimals1 klasa Animal
Listing 3.1. OOPAnimals klasa Cat
Osnovnu klasu Animal da definišemo kao abstract, da bi podvukli da je to bazna klasa i da necemo instancirati objekte tog tipa, već klasa koje je nasleđuju, kao i da napravimo dogovor koje metode će imati naše kuce i mace.
Dalje, definišemo klase Cat i Dog koje nasleduju Animal i implementiraju apstraktne metode Walk i Talk. Sada naš primer izgleda ovako:
[csharp]
var myCat = new Cat("Garfield");
myCat.Walk();
myCat.Talk();
[/csharp]
Ukoliko želimo da kreiramo objekat koji predstavlja kucu, onda instanciramo klasu Dog, tj. vrstu životinje određuje tip objekta, tj. klasa, što je više u duhu objektno orijentisanog pristupa. Sada listu životinja možemo definisati ovako:
[csharp]
var animals = new List<Animal>
{
new Cat("Garfield"),
new Dog("Locko"),
new Dog("Pajko")
};
foreach (var animal in animals)
{
animal.Walk();
animal.Talk();
}
[/csharp]
Svaki objekat će pozvati odgovarajuću metodu Walk odnosno Talk, zavisno od tipa kome pripada.
Ovakav pristup omogućava jednostavno dodavanje nove vrste nasleđivanjem osnovne klase, mada dodavanje nove akcije zahteva izmenu bazne klase. Nismo našli sveti gral, ali je ovo rešenje dovoljno dobro za praktičnu upotrebu, pogotovu u poslovnim aplikacijama. Kroz praksu sam primetio da se često javljaju dva problema kada koristimo polimorfizam, pa da ih pojasnimo i rešimo.
Pre i post akcije
U praksi, često dolazi do ponavljanja delova koda u istoj metodi za različite tipove, najcešće postoji isti blok koda koji je potrebno izvršiti pre i posle same akcije. U osnovnoj klasi uvodimo novu metodu DoWalk koja će biti virtuelna i koju pozivamo iz metode Walk.
[csharp]
public void Walk()
{
//pre action
…
DoWalk();
//post action
…
}
virtual public void DoWalk()
{
}
[/csharp]
U podklasama i dalje pozivamo metodu Walk, medutim implementiramo metodu DoWalk. U projektu OOPAnimals2 metoda Walk izgleda ovako (analogno je izmenjena i metoda Talk):
[csharp]
public Animal Walk()
{
Console.Write("Prepare for walk. ");
DoWalk();
return this;
}
[/csharp]
Pre action je jednostavan Console.Write, a post action vraća sam objekat. Sada naše metode vraćaju referencu na svoju instancu, što nam omogućava method chaining. I dalje možemo da koristimo standardnu varijantu:
[csharp]
var myCat = new Cat("Garfield");
myCat.Walk();
myCat.Talk();
[/csharp]
Ali i one-liner:
[csharp]
new Cat("Garfield").Walk().Talk();
[/csharp]
Primetite da ovu funkcionalnost omogućava osnovna klasa, klase Dog i Cat su identične kao i u projektu OOPAnimals1 (osim naziva metoda).
Listing 4. OOPAnimals2 klasa Animal
Listing 4.1. OOPAnimals2 klasa Cat
Izbor vrste/podtipa u runtime
Ukoliko znamo vrste naših objekata u compile time, možemo lepo da baratamo i sa listama objekata:
[csharp]
var animals = new List
{
new Cat("Garfield"),
new Dog("Locko"),
new Dog("Pajko")
};
foreach (var animal in animals)
{
animal.Walk();
animal.Talk();
}
[/csharp]
Medutim naše rešenje nije idealno ukoliko imamo listu objekata čiju vrstu saznajemo u runtime, npr. radimo import iz nekog xml-a. Rešenje ipak postoji, napravićemo klasu AnimalFactory sa metodom Create koja će nam za odgovarajuce parametre, vrstu i naziv životinje vratiti odgovarajuci objekat:
[csharp]
public class AnimalFactory
{
public Animal Create(string type, string name)
{
switch (type)
{
case "Cat":
return new Cat(name);
case "Dog":
return new Dog(name);
}
return null;
}
}
[/csharp]
Sada možemo kreirati objekat čiji tip određujemo u runtime:
[csharp]
new AnimalFactory().Create("Cat", "Garfield");
[/csharp]
Ah ne, opet switch!? Ukoliko vam je potrebna mogućnosti izbor tipa u runtime, neko to mora da uradi, ipak, ako ste pripadnik anti switch pokreta, klasa AnimalFactory može da izgleda i malo drugačije:
[csharp]
public class AnimalFactory
{
private static readonly Dictionary<string, Func<string, Animal>> animalConstructors =
new Dictionary<string, Func<string, Animal>>()
{
{"Cat", n => new Cat(n)},
{"Dog", n => new Dog(n)}
};
public static Animal Create(string type, string name)
{
return animalConstructors[type](name);
}
}
[/csharp]
Funkcionalnost metode Create je ista kao i u prethodnom primeru, Switch smo zamenili rečnikom (Dictionary) čiji je ključ oznaka našeg tipa, a vrednost lambda izraz koji je sam poziv konstruktoru, tako da je pri kreiranju dovoljno da na osnovu ključa izaberemo funkciju koja ce kreirati odgovarajući tip objekta.
Za kraj, ako imamo listu koja sadrži parove naziv i tip objekta:
[csharp]
var animalsNameType = new Dictionary<string, string>
{
{"Garfield", "Cat"},
{"Locko", "Dog"},
{"Pajko", "Dog"}
};
[/csharp]
Možemo i ovako da kreiramo objekte i izvršimo akcije Walk i Talk:
[csharp]
animalsNameType.ToList().ForEach(
pair =>
AnimalFactory.Create(pair.Key, pair.Key)
.Walk()
.Talk()
);
[/csharp]
Šta je bolje, čitljivije i lepše je, uglavnom, stvar ličnog izbora, switch nije zao sam po sebi, međutim često se zloupotrebljava. Naravno, moguće je zloupotrebiti i polimorfizam, birajte odgovarajuće rešenje na osnovu problema koji rešavate. Nadam se da sam uspeo da zadržim pažnju i da dam neke ideje kako da pišete bolji kôd. Sve ideje i komentari su dobrodošli.
Da ponovim, izvorni kôd sa primerima možete skinuti ovde. Da bi otvorili projekte potrebano je da imate instaliran Visual Studio 2010, besplatnu express verziju možete skinuti sa ove adrese.
Odlicna tema!
Jedan bitan komentar je da kreiranje instanci moze da bude potpuno dinamicko preko “.NET Reflection” sto predstavlja osnovu “plugin arhitektura”. Onda funkcionalnost moze da se prosiruje bez izmena u osnovnoj aplikaciji, samo se doda novi dll sa implementacijom nove “zivotinje”. Onda lista tipova i konstruktora ne bi bila “hard coded”, vec bi se dinamicki ucitavala iz “plugin” foldera ili nekog config fajla.
Ovo na kraju moze da bude dobar primer za “Fluent Interface” koji je u nekim slucajevima veoma lepo resenje, kao na primer LINQ – enumerable.Where(_).OrderBy(_).Select(_).
Hvala Zvonko. Da “plugin arhitektura” koju si predložio bi bila pravi nastavak ove teme, sviđa mi se.
Pokušao sam da, ukratko, prođem celo rešenje čestog poslovnog problema, tako da sam uveo možda malo više pojmova nego što je idealno u jednom postu. Nadam se da sam pronašao neku meru i dao neke ideje kako bi moglo bolje… Možda se kasnije vratim na svaku od pomenutih tema da ih malo detaljnije istražimo 🙂