Home > Uncategorized > Wzorzec repozytorium (repository pattern) – jak w końcu ma on wyglądać ?

Wzorzec repozytorium (repository pattern) – jak w końcu ma on wyglądać ?

dobromirWzorce projektowe lubimy je (nad)używać. Jak często zastanawiałeś się w jaki sposób rozdzielić logikę bazy danych od swojego precious-code? Z pewnością użyłeś wzorca Repository – jak bardzo byłeś z tego rozwiązania zadowolony, czy wszystko się w nim Tobie podobało?

Na #-coda mi ten wzorzec?

Sam wykorzystuję go na co dzień – również inni go używają – jednak każdy używa go w inny sposób – powielając często praktyki od starszego kolegy lub z jakieś książki 101 wzorców w 15 minut lub z postów różnych portali – wykorzystując dodatkowo metodę Copyi’eg-Paste’a. Nie chcę tutaj podawać poprawnego sposobu tworzenia i wykorzystania wzorca repozytorium (bo idealnej odpowiedzi nie ma). Jedynie chcę przybliżyć parę rzeczy, które mi się nie podobają lub na które zwracam szczególną uwagę.

Dla niewtajemniczonych – sporo można na ten temat znaleźć w sieci – bardzo polecam http://bit.ly/1GzCQYw. Nie chcę tutaj wchodzić w dylemat – czy repozytorium ma sens, gdy używamy EntityFrameworka, czy też nie – szukając ciekawych odniesień, znalazłem dosyć fajnego posta Repozytorium stosować czy nie.

Precz z generycznością!

Lubię kod generyczny… wygląda tak pro, fajnie, jest (nie)reużywalny i wogóle. W pewnym stopniu wykorzystanie go we wzorcu repozytorium jest przesadą. Spójrzmy uwagę na to generyczne ‘międzymordzie’

interface IRepository<T> 
{ 
  T Get<T>(int id); 
  void Add<T>(T t);
  QueryResult Find(Query query);
  void Delete(T t);
  void DeleteById(int id);
  // i jeszcze parę innych metod
}

Fajnie, nie? Praktycznie w każdym ‘przewodniku programowania’ znajdziemy ten interfejs. Przecież są to zawsze używane metody CRUD(L)a. Zawsze jak coś dodajemy, to chcemy usunąć, zmodyfikować, pobrać etc… Interfejsy lubimy tworzyć, ale aby coś zadziałało, to definiujemy klasę – przeważnie jakąś abstrakcyjną typu:

public abstract class RepositoryBase<T> : IRepository<T>
{
  public T Get(int id)
  {
    // ściśle tajny kod
  }
}

Wow, mamy już sporo kodu, teraz możemy tworzyć dedykowane repozytoria z bomby dodając ‘puste’ klasy dla naszych obiektów (wzbogacone o interfejsy) Product, Category, Discounts, Details itd…

public interface ICategoryRepository : IRepository<Category> { }

public class CategoryRepository : RepositoryBase, ICategoryRepository
{
  // kodu, brak bo wszystko jest fajnie 'generyczne'
}

Czy coś tutaj się nie podoba? Mi się nie podoba np, że sporo klas jest ‘pustych’ – ta nasza generyczna funkcjonalność (a raczej klasa bazowa) spowodowała, że cała ‘logika’ znajduje się w klasie Base – spoko – to tylko taka pierdoła. Problem pojawia się gdzie indziej. Wyobraźmy sobie, że wykorzystujemy repozytorium Customer i jego metodę Delete

customerRepository.Delete(customer);

Dziwne wywołanie, mamy możliwość usunięcia klienta z bazy? Cóż, tak pewnie chciał programista, taki był wymóg, więc czemóż nie skorzystać. Ale dostajemy NotImplementedException – bo usuwanie nie jest dozwolone (a raczej nie powinno być zaimplementowane), a interfejs i klasa bazowa wymusiła implementację i udostępniła tą metodę. Podobny problem pojawia się w innych miejscach, gdzie jest ta ‘generyczność’ wykorzystana.

Inna sprawa – co w przypadku, gdy identyfikatorem nie jest int narzucony przez interfejs int, ale Guid? Pojawia się pytanie – utworzyć coś osobnego w stylu IGuidRepository czy dodać metodę Get(Guid id) – przeważnie wybierana jest bramka numer 2 i nasze generyczne repozytorium wzbogacone jest o dedykowaną metodę Get, a ta oryginalna otrzymuje miano NotImplementedException. Problem staje się poważniejszy przy większym systemie – wtedy okazuje się, że sporo generycznych metod otrzymało miano NotImplemented lub wogóle nie jest używana w aplikacji.

Jest na to jakaś recepta?

Na pewno jak z każdym innym wzorcem – rozwiązaniem jest umiar i nie branie wszystkiego dosłownie – niektórzy biorą dosłownie słowa świętych ksiąg i mamy na świecie to co mamy.

U mnie lekka modyfikacja sprawdza się na chwilę obecną całkiem sensownie i bez różnego rodzaju haków. Jest na to pewien przepis, który każdy może dostosować do swoich rozwiązań, przykładowo:

public interface ICustomerRepository
{
  Customer Get(int id);
}

public interface IProductRepository 
{
  Product Get(int id);
  Product GetByGuid(Guid guid);
  IEnumerable Latest()
}

klasa repozytorium natomiast wygląda następująco

internal sealed class CustomerRepository : RepositoryBase 
{
  // implementacja czegoś na prawdę wspólnego
  // lub implementacja metod pomocniczych
}

internal abstract class RepositoryBase 
{

}

Jaka jest różnica?

  • interfejs repozytorium (czy to Customer czy Product) udostępnia jedynie to co ma do udostępnia, klasa implementująca nie ma żadnych metod NotImplemented

  • wszystko co chcemy uwspólnić, możemy nadal uwspólnić w klasie bazowej – poprzez dodanie metod pomocniczych (przeważnie z modyfikatorem dostępu protected)
  • Po trzecie (o czym kiedy indziej wspomnę) – interfejsy są publiczne, czyli upubliczniamy wszystkie funkcjonalności, natomiast sama implementacja jest i powinna być znana w assembly zawierającym definicję – tutaj niech kontener IoC/DI zajmie się tworzeniem instancji – my posługujemy się przecież tylko interfejsami.

Nawiązując do ‘drugiej kropki’ – przykładem może być metoda Get, która jest prawie wszędzie wykorzystana. Nic nie stoi na przeszkodzie aby dodać interfejs (tutaj już bardziej taki rozdmuchany)

public interface IRepositoryGet<TEntity, TId>
{
  TEntity Get(TId id);
}

I wykorzystać go we wszystkich repozytoriach, gdzie zachodzi taka potrzeba:

public interface ICustomerRepository : IRepositoryGet<Customer,int> { }

(bo lepiej jest coś dodać gdy jest w danym momencie potrzebne, niż utworzyć na ‘zaś’ i póżniej modyfikować – należy też pamiętać, że jeżeli coś zmieniamy lub modyfikujemy trzeci raz, to warto zastanowić się nad tak zwanym i nie lubianym przez klientów procesem zwanym refaktoryzacją).

Nie poruszyłem tutaj jednak takich problemów jak możliwość wyszukiwania i zwracania listy wyników – np. dlaczego nie powinno używać się interfejsu IQueryable lub inne zagadnienie – czy np. w repozytorium Customer mamy doładowywać również zamówione produkty – jednak to może na inn raz.

Edit – komentarz przypomniał mi o jednej rzeczy, która nie podoba mi się jeszcze we wzorcu repository – często klasa lubi sobie spuchnąć – i zawierać wiele metod, często specyficznych dedykowanych dla jakiejś określonej funkcjonalności.

  1. wmekal
    April 12, 2015 at 19:41

    Marcinie, odrzuć Repozytorium, odrzuć niepotrzebne interfejsy.
    Polecam query/command (Ayende Rahien/Greg Young) lub mediator (Jimmy Bogard).

    • April 13, 2015 at 21:21

      Dzięki za hinta – sam jednak ten wzorzec o którym wspomiałeś wykorzystyję trochę w innej sytuacji, z pewnością kiedyś o nim wspomnę – na szybkiego zarzucę świeżym linkiem na do ciekawego miejsca: http://devtalk.pl/2015/04/14-cqrs-with-udi-dahan/#more-354
      Jednak Twój wpis przypomniał mi o jednej rzeczy, która nie podoba mi się w tym wzorcu (dopiszę ja za chwilę)

  1. April 9, 2016 at 00:17

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: