Implementując warstwę biznesową za pomocą DomainModel lub ActiveRecord uzyskujemy bardzo rozdrobniony interfejs. Ponadto stworzone obiekty biznesowe zawierają logikę odnoszącą się tylko do konkretnej encji. W poprzednich postach pokazałem prostą klasę Client, implementującą wzorzec DomainModel. Przedstawiona klasa mogła wykonywać tylko operacje dla konkretnego, jednego klienta. W systemie jednak często będziemy musieli brać pod uwagę zbiór encji. Przykładowo może być potrzeba zwrócenia zbioru klientów, dla których suma dokonanych zamówień przekracza jakąś wartość. Ponadto analizując wzorce DM oraz AR łatwo zauważyć, że nie ma tam metod wstawiania nowych encji do bazy danych. Rozwiązaniem na przedstawione problemy jest wzorzec repozytorium. Zawiera on przede wszystkim operacje CRUD (Create, Read, Update, Deleted) oraz wszystkie inne operacje selekcji (np. GetById lub GetByQuery ). Przeważnie definiuje się jedno repozytorium na jeden obiekt biznesowy. Bardzo ważną cechą repozytorium jest niezależność od bazy danych. Użytkownik powinien korzystać z poprawnie zaimplementowanego repozytorium jak ze zwyklej klasy czy kolekcji danych. Dlatego dobrym zwyczajem jest stworzenie najpierw interfejsu a dopiero potem konkretnej klasy (implementacji):
public interface IClientRepository { Client GetById(Guid id); Client GetByQuery(Query query); void Add(Client client); void Remove(Client client); void Update(Client client); /*Inne specyficzne metody dla obiektu "Client". Może być to np. wyszukiwanie klientów po sumie zamówien, regionie pochodzenia, kupowanych towarach itp. */ }
Warto zauważyć, że większość metod w każdym repozytorium będzie się powtarzać. Operacje typu CRUD będzie zawierać zarówno repozytorium Client jak i każde inne. Z tego względu lepszym rozwiązaniem jest wprowadzenie generycznego interfejsu bazowego:
public interface IRepository<T> { T GetById(Guid id); T GetByQuery(Query query); void Add(T item); void Remove(T item); void Update(T item); }
Jeśli jakieś repozytorium musi zawierać metody niestandardowe to po prostu rozszerza interfejs:
public interface IOrderRepository:IRepository<Order> { Order[] GetOrdersByClientId(Guid idClient); }
Dobry zwyczajem jest również niezależność konkretnych implementacji repozytorium od bazy danych np:
public class OrderRepository:IOrderRepository) { private IDataContext m_DataContext=null; public OrdeRepository() { // inicjalizacja m_DataContext } public void Add(Order item) { m_DataContext.Add(item); } ... }
W powyższym przykładzie operacje związane z fizycznym zapisem w bazie danych przekazywane są do klasy z warstwy DAL. Ponadto konkretna implementacja IDataContext może być wstrzykiwana nawet w trakcie działania programu. W końcu jeśli chcemy zawrzeć w repozytorium logikę biznesową, nie ma mowy o jakiejkolwiek zależności klasy z bazą danych – repozytorium jest abstrakcyjne w przeciwieństwie do podobnych rozwiązań z DAL.
Wyobraźmy sobie, że założenia projekty wymagały implementacji repozytorium dla każdego obiektu biznesowego. Powstaje więc pytanie, w jaki sposób wygodnie tworzyć instancję repozytorium dla konkretnego obiektu biznesowego? Nie chcemy w kodzie operować wyłącznie na konkretnych klasach tzn:
OrderRepository orderRepository = new OrderRepository(); ProductRepository productRepository = new ProductRepository(); ClientRepository orderRepository = new ClientRepository();
Lepszym rozwiązaniem jest wykorzystywanie ogólnych interfejsów (np. IRepository). W następnym poście postaram się odpowiedzieć na te pytanie przedstawiając kilka dodatkowych wzorców projektowych (m.in factory, service locator).
a jaka jest roznica pomiedzy repositort a starym dobrym dao? jakbym nie znal tytulu art. to bym pomyslal ze opisywane jest dao…
Ja bym zaliczył repository to warstwy biznesowej zatem oprócz czystych operacji CRUD może zawierać jakąś logikę.
DAO to czysty DAL.
Piszę sobie od paru lat hobbystycznie w c# i mój ostatni programik do zarządzania licencjami wygląda właśnie tak. Tak więc jednak coś w tych wzorcach jest skoro wielu niezależnie od siebie dochodzi do tego samego 🙂 Wpis jak najbardziej jasny i zrozumiały. Czekam na następne 🙂
@Piotr Zieliński
Uściślając moim zdaniem do warstwy biznesowej powinien należeć jedynie interfejs repozytorium. Implementacja warstwę niżej.
Implementacja musi być również w BL. Powiedzmy, że mamy metodę zwracającą listę produktów, które powinny być przecenione – jest to czysta logika biznesowa więc nie może znajdować się w DAL.
Fizyczny odczyt z bazy danych powinien oczywiście znajdować się w DAL. Dlatego w przedstawionej implementacji (tzn. zarysie) repozytorium posiada referencje do IDataContext. Zatem IDataContext wykonuje operacje CRUD (zapytania SQL itp). Repozytorium nie zawiera implementacji DAL a wykorzystuje jedynie gotową klasę do tego.
Uuu, coś tu jest nie tam. Implementacja w BLL? Poważny błąd, ponieważ związujesz BLL z konkretnym sposobem dostępu do danych, w tym przypadku z IDataContext (Linq2Sql). A jakbyś chciał zmienić to na NHibernate’a po paru tygodniach (co jest dość częste, gdy wpada się w ograniczenia linq2sql)? Trzeba by wszędzie w _warstwie biznesowej_ zmienić IDataContext na ISession. A to znaczy jedno: bad design.
BLL powinien być tworzony jako osoby DLL BEZ ŻADNEJ referencji do bibliotek zewnętrznych. Wtedy ma się pewność, że jest to warstwa abstrakcyjna, niezależna od niczego poniżej (ani od DAL ani od warstwy prezentacji aka GUI). W tym miejscu mogę się powołać na rozdział 4 z “Applying Domain-Driven Design and Patterns”.
Jeżeli umieścisz część implementacji Repository w BLL to musi to być kod, który bezpośrednio NIE odwołuje się do bazy, ani do żadnej DLL powiązanej z bazą.
Oczywiście są “wyjątki”. Gdy np korzysta się z MS Entity Framework’a warstwa DAL praktycznie nie istnieje ponieważ EF udostępnia ci cały model. Opakowywanie tego w kolejną warstwę jest w małych do średnich projektów bezcelowe.
Jeszcze raz zaznaczam, że repozytorium musi być niezależne od źródła danych. Dlatego napisałem IDataContext a nie DataContext. IDataContext to interfejs dostępowy do bazy danych. A jego implementacja jest wstrzykiwana przy starcie aplikacji. W moich projektach IDataContext to własny interfejs dostępowy do bazy danych. Następnie w zależności od bazy danych piszę adaptery np. nHibernateAdapter.
Acha co do wspomnianej przez Ciebie referencji. To jakaś referencja przecież w końcu musi być (zawsze luźna).
A to ok, IDataContext to (przypadkowo) też nazwa interfejsu linq2sql…
Moim zdaniem, lepiej byłoby wykorzystać wzorzec stragegy w OrderRepository oraz użyć wstrzykiwania IDataContext przez konstruktor.
BTW Gratuluje ciekawych postów.
Zwykle IDataContext pobieram przez UnityContainer a konfiguracje UC umieszczam w pliku konfiguracyjnym aplikacji.Z kolei przez konstruktor jest o tyle dobrze, że łatwiej podczas testów wstrzyknąć mocka…
Czemu nie iść krok dalej 🙂
public class RepositoryFactory : IRepository, IRepository
{
IDataContext dataContext;
public RepositoryFactory(IDataContext dataContext)
{
this.dataContext = dataContext;
}
…..
public IList GetByExample(T example)
{
IEnumerable result = this.dataContext.GetByExample(example, typeof(T)).Cast();
return result.ToList();
}
public void Add(T item)
{
this.dataContext.Add(item);
}
}
Wykorzystanie:
RepositoryFactory emplRep = new RepositoryFactory(context);
IList employees = emplRep.GetByExample(e);
Pozjadało nawiasy kątowe… Zastąpię je [[ i ]]
public class RepositoryFactory[[T]] : IRepository[[T]]
{
IDataContext dataContext;
public RepositoryFactory(IDataContext dataContext)
{
this.dataContext = dataContext;
}
…..
public IList[[T]] GetByExample(T example)
{
IEnumerable[[T]] result = this.dataContext.GetByExample(example, typeof(T)).Cast();
return result.ToList[[T]]();
}
public void Add(T item)
{
this.dataContext.Add(item);
}
}
RepositoryFactory[[Employee]] emplRep = new RepositoryFactory[[Employee]](context);
IList employees[[Employee]] = emplRep.GetByExample(e);