Wzorzec repozytorium

Odpowiedz Nowy wątek
2013-04-07 22:37
0

Witam

Przeglądam wzorce i jak zwykle mam wątpliwości jeżeli chodzi o moje pomysły. Sprawa prosta jak drut, w każdym tutorialu, opisie jest klasa i metody, które zwracają obiekty tej klasy:

http://patryknet.blogspot.com[...]pository-pattern-wzorzec.html

np. FakePersonRepository ( metoda GetById ) zwraca obiekt klasy Person, ale co w przypadku kiedy moje zapytanie złącza kilka tabel i zawiera kilka pól z tych tabel ( nie tylko pola z jednej), czy interfejs IPersonRepository może zwracać oprócz void-a i T, typ object? ( pewnie może, ale pytanie jak to ma się do dobrych zasad programowania)

Pozostało 580 znaków

2013-04-08 00:17
3
Ciekawski napisał(a):

co w przypadku kiedy moje zapytanie złącza kilka tabel i zawiera kilka pól z tych tabel ( nie tylko pola z jednej), czy interfejs IPersonRepository może zwracać oprócz void-a i T, typ object? ( pewnie może, ale pytanie jak to ma się do dobrych zasad programowania)

Ale czemu object? Przecież możesz zdefiniować sobie nową klasę, którą będzie zawierała interesujące Cię pola i ją zwracać z repozytorium.

Odrębną kwestią jest bezsens używania wzorca repozytorium w dzisiejszych czasach. To pewno był dobry wzorzec w czasach, gdy nie było ORMów. Wtedy można było za taką fasadą ukryć różne ręcznie sklejane SQLe i tego typu pierdoły. Ale ukrywanie w repozytorium ORMa jest tylko dokładaniem zbędnej warstwy, która nie wnosi chyba żadnej wartości dodanej.


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."
"nie wnosi chyba żadnej..." Chyba? Napisałeś Chyba? :) - jacek.placek 2018-01-10 23:49
5 lat temu mogło mi się chyba zdarzyć. A co? - somekind 2018-01-11 01:37

Pozostało 580 znaków

2013-04-08 08:08
1

To bezpośrednio na DataContext operujesz?

Pozostało 580 znaków

2013-04-08 08:16
micc
2

Nie zgodzę się, że wartwa z repozytoriami nie ma sensu. Ewentualnie można wyeliminować konkretne repozytoria i wykorzystywać repozytorium generyczne, tj. IRepository<product>, IRepository<order> itp, a przy skomplikowanych przypadkach dziedziczyć po IRepository<jakisobiekt> dodając własne, uszczegółowione metody. Według mnie zawsze uzasadnione jest ukrywanie skąd pochodzi źródło danych. Jeżeli zmieni się specyfika (dostęp będzie odbywał się np. poprzez czytanie plików z dysku/sieci) lub wystąpi wymiana ORM, wtedy w prosty sposób sobie poradzimy. Do tego dochodzi testowanie jednostkowego, które IMO nie powinno się odbywać, gdy operujemy bezpośrednio na IQueryable.

Pozostało 580 znaków

2013-04-08 08:44
0

Czyli jeżeli mam podstawowe klasy, które reprezentują encje i nagle mam widzimisie , że chce zwrócić coś nietypowego ( kilka pól z jednej tabeli i kilka pól z drugiej tabeli ) to muszę tworzyć ku temu osobną klasę tak? Swoją drogą ORM-a nie zawsze każdy używa ( przecież nie jest chyba super wymagany w każdym projekcie ). Ale jeżeli już ORM, to jak to rozwiązać skoro jako Model chcę zastosować mapowane klasy na tabele i każda klasa mapowana ma jakąś logikę.

Pozdrawiam

edytowany 3x, ostatnio: Ciekawski, 2013-04-08 10:16

Pozostało 580 znaków

2013-04-09 01:19
3
siararadek napisał(a):

To bezpośrednio na DataContext operujesz?

W przypadku Entity Frameworka owszem, w przypadku NHibernate korzystam z odpowiednika czyli Session.

micc napisał(a):

Według mnie zawsze uzasadnione jest ukrywanie skąd pochodzi źródło danych.

A ORM tego nie ukrywa wystarczająco? Z ORMem pracuje się z grubsza tak, jak ze zwykłymi kolekcjami obiektów w pamięci.

Jeżeli zmieni się specyfika (dostęp będzie odbywał się np. poprzez czytanie plików z dysku/sieci) lub wystąpi wymiana ORM, wtedy w prosty sposób sobie poradzimy.

Wiedziałem, że ktoś użyje tego argumentu. :)
A zmieniła się kiedykolwiek w jakimkolwiek projekcie? Ktoś kiedyś zmienił bazę danych na pliki? Zaczynając projekt wybieramy języki, technologie i bazy danych, a potem się tego trzymamy.

Ale nawet jeśli, to co za problem? Wystarczy użyć innego providera do ORMa, i już możemy zmienić bazę danych. Nawet na pliki, jak się uprzemy.

Do tego dochodzi testowanie jednostkowego, które IMO nie powinno się odbywać, gdy operujemy bezpośrednio na IQueryable.

Dlatego powinniśmy pisać tak kod, żeby logikę biznesową dało się przetestować bez udziału bazy danych. Chociaż testy integracyjne nie są niczym złym, wręcz przeciwnie.

Co jest złego w Repository?

Zaczyna się od CustomerRepository z metodami GetCustomerById, GetCustomerByName, GetCustomerByNIP. Wszystko jest fajnie. Potem okazuje się, że trzeba pobrać klienta razem z fakturami, więc ktoś zmienia zawartość tych metod tak, że zachłannie pobierane są faktury. Oczywiście nigdzie w kodzie tego nie widać, do repozytorium nikt inny nie zagląda (bo po co?), ale ludzie dziwią się, że w wielu miejscach aplikacja zwolniła... Na szczęście znalezienie przyczyny nie trwa długo, i trzeba to naprawić. Więc co się robi? Nowe metody o więcej mówiących nazwach: GetCustomerWithInvoicesById, GetCustomerWithInvoicesByName, GetCustomerWithInvoicesByNIP. I jest wszystko dobrze!
Przynajmniej na razie, bo z czasem pojawia się potrzeba pobrania klientów wg adresu wraz z jego pracownikami, ale bez faktur: GetCustomersWithEmployeesButWithoutInvoicesByAddress, a potem potrzeba pobrania wszystkich klientów, z fakturami, bez pracowników, z ulubionymi filmami, bez kochanek wg grupy krwi żony i imienia kota córki prezesa: GetCustomersWithInvoicesWithoutEmployeesWithFavouriteMoviesWithoutMistressesByWifeBloodTypeAndChairmanDaughterCatName.
Im bardziej skomplikowana domena tym więcej potrzeb kombinacji warunków i wyciągania powiązanych encji, tym więcej metod w klasie Repozytorium. Kończymy z klasą z milionem metod... i to ma być dobre?

http://martinfowler.com/eaaCatalog/repository.html

A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.

A teraz zamieńmy w tej definicji "Repository" z "ORM", i zobaczmy, że nadal będzie ona prawdziwa. Repozytorium ORMowi nie daje absolutnie nic. To tylko kolejna warstwa kodu, która może spowodować błędy.

Ciekawski napisał(a):

Czyli jeżeli mam podstawowe klasy, które reprezentują encje i nagle mam widzimisie , że chce zwrócić coś nietypowego ( kilka pól z jednej tabeli i kilka pól z drugiej tabeli ) to muszę tworzyć ku temu osobną klasę tak?

No raczej... Chyba, że masz inny pomysł, ale to jest generalnie najprostsze. Porządny ORM bez problemu Ci pozwoli wypełnić taką klasę danymi z bazy.

Swoją drogą ORM-a nie zawsze każdy używa ( przecież nie jest chyba super wymagany w każdym projekcie ).

No nie jest wymagany. Ale w zależności od tego, czy płacą nam za godzinę, czy nie, to nie używając ORMa okradamy albo swojego pracodawcę/klienta z pieniędzy, albo siebie z czasu.

Ale jeżeli już ORM, to jak to rozwiązać skoro jako Model chcę zastosować mapowane klasy na tabele i każda klasa mapowana ma jakąś logikę.

Klasy mapowane na tabele to raczej encje, ale mniejsza z tym. Jeśli chcesz zaszywać logikę w tych klasach, to też nie ma problemu, tylko niech to nie będzie logika operacji bazodanowych, jak pytałeś w innym wątku.


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."

Pozostało 580 znaków

2013-04-09 09:42
micc
1

Te wszystkie GetCustomerByCosTam można zastąpic w prosty sposób repozytorium generycznym.
Ja w wiekszosci projektów uzywam takiego interfejsu, który pozwala tworzyć różne "where'y" i includować navigation properties, kolekcje. Bardzo rzadko (w wyjątkowych przypadkach) muszę tworzyć specyficzne repozytorium.


public interface IRepository<TEntity> : IDisposable where TEntity : BaseEntity
    {
        int Count();
        bool Exists(Func<TEntity, bool> expression);
        int Count(Func<TEntity, bool> expression);
        void Delete(TEntity entity);
        void Add(TEntity entity);
        TEntity Get(int Id);
        IEnumerable<TEntity> FindWhere(Func<TEntity, bool> expression, Expression<Func<TEntity, object>> orderExpression, params Expression<Func<TEntity, object>>[] includeExpressions);
        TEntity Single(Func<TEntity, bool> expression);
        IEnumerable<TEntity> GetPaged(Expression<Func<TEntity, bool>> expression, string OrderField, OrderDirection orderDirection, int Take, int Skip, out int TotalFound, params Expression<Func<TEntity, object>>[] includeExpressions);
    }

Operowanie bezpośrednio na dbContext według mnie nie jest dobrym pomysłem, chociażby dlatego, że (pomijając już testy jednostkowe itp.) uzależnia nas od konkretnej technologii w tej warstwie, która powinna operować na abstrakcji. Logika biznesowa powinna operować na rzeczywistych kolekcjach, a nie na czymś, co nie wiemy, czy w ogóle się wykona. I chociaż tak jak wspomniałeś, zazwyczaj nie zmienia się technologii, to eliminacja tej warstwy śmierdzi mi pod względem architektonicznym. Jestem w tym względzie bardzo pedantyczny i na myśl o zrezygnowaniu z tej warstwy włącza mi się czerwone światełko - oho, nie łam zasad separations of concerns, ani abstraction.
Oczywiście dla małych, szybki projektów jestem w stanie zrezygnować z tej warstwy, przy dużych projektach raczej nie.

Pozostało 580 znaków

2013-04-14 16:02
2
micc napisał(a):

Te wszystkie GetCustomerByCosTam można zastąpic w prosty sposób repozytorium generycznym.

Generyczne repozytorium to antywzorzec. Żeby było śmieszniej sprzeczny z ideę repozytorium. :)
Bo repozytorium to kontrakt między logiką biznesową, a danymi, których ona potrzebuje. Ma zapewnić ścisły zestaw konkretnych (a więc nie generycznych!) metod dostępu do danych, tak aby logika biznesowa nie była świadoma szczegółów tego, co w bazie siedzi, ani jak o to pytać. Repozytorium to kolekcja z zestawem zaawansowanych metod odpytujących, a nie DAO z "Repository" w nazwie.
Repozytorium wystawia metodę GetCustomersByCity(string name), i logiki biznesowej nie obchodzi w tym momencie, czy nazwa miasta to kolumna w tabeli klientów, czy jest oddzielna tabela miast, czy może w ogóle oddzielna tabela adresów. Budując zapytania w warstwie biznesowej i przekazując je do repozytorium, sam pozbawiasz się abstrakcji, o którą tak walczysz.
Generyczne repozytorium może być co najwyżej wewnętrznym helperem dla konkretnych repozytoriów, ale nie interfejsem udostępnionym dla logiki biznesowej.

Logika biznesowa powinna operować na rzeczywistych kolekcjach, a nie na czymś, co nie wiemy, czy w ogóle się wykona.

Co to jest "rzeczywista kolekcja"? I co się nie wykona, bo nie rozumiem? Jak się nie wykona, to chyba aplikacja nie będzie działała.

I chociaż tak jak wspomniałeś, zazwyczaj nie zmienia się technologii, to eliminacja tej warstwy śmierdzi mi pod względem architektonicznym. Jestem w tym względzie bardzo pedantyczny i na myśl o zrezygnowaniu z tej warstwy włącza mi się czerwone światełko - oho, nie łam zasad separations of concerns, ani abstraction.

Tzn. dobra architektura i seperation of concerns polegają na tworzeniu warstw będących prostymi wrapperami i nadawaniu im modnych nazw? :)

I jeszcze takie pytanie - jak w tym generycznym repozytorium zarządzasz transakcjami? Bo nie zauważyłem metody Commit.


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."

Pozostało 580 znaków

2013-04-14 17:39
0

Mógłby mi ktoś powiedzieć co robią: bool Exists(Func<TEntity, bool> expression);. int Count(Func<TEntity, bool> expression);. Oczywiście czytałem o Func, ale chciałbym poznać zdanie autora czemu wybrał Func i co u niego to robi.

No to już pytanie stricte o C#. Po prostu wygodniej się używa Func niż delegatów. - somekind 2013-04-14 21:26

Pozostało 580 znaków

2013-04-15 11:06
micc
0

Nie uważam, aby repozytorium generyczne było antywzorcem. Do prostych przypadków jest to dobre rozwiązanie.
Jeżeli muszę tworzyć konkretne repozytoria to wtedy faktycznie dziedziczą one po repozytorium generycznym.

Nie widzę różnicy pomiędzy zapisem GetCustomerByCityName("Warszawa") a zapytaniem FindWhere(a=>a.CityName.Equals("Warszawa")).

Zapis ten nie mówi nic o tym, że wewnątrz tej funkcji realizowany będzie dostęp do bazy danych. Jest to de facto zapytanie, które mówi: chcę kolekcję customerów, przefiltrowaną w następujący sposób. Co innego gdybym pisał FindWhere("CityName is Like 'Warszawa'"), wtedy faktycznie uwagi byłyby zasadne.
Zapytania nie buduję w warstwie biznesowej, ponieważ LINQ jest częścią wykorzystywanego języka programowania, a nie warstwy dostępu do danych (jako parametry do metod repozytoriów używane są tylko te wyrażenia, które są zgodne z LINQ to Objects).
Zawsze mogę przejść przykładowo z EF na nHibernate i wyłuskać wymagane search criteria z wyrażenia LINQ.

Mówiąc o "rzeczywistej kolekcji" miałem na myśli operowanie w warstwie biznesnowej na IEnumerable, a nie IQueryable.

Repozytorium u mnie używane jest na poziomie warstwy serwisów aplikacji. W związku z tym, że piszę aplikacje webowe, transakcje są commitowane po zakończeniu pojedynczego żądania http pod warunkiem, że nie został rzucony wyjątek lub transakcja została na którymś etapie odrzucona (pojedyncze żądanie http to jedna transakcja, która albo się wykona w całości, albo się nie wykona w ogóle). Odrzucanie, commitowanie transakcji oraz zarządzaniem cyklem życia DBContext zajmuje się u mnie IUnitOfWork, który jest wstrzykiwany razem z wymaganymi repozytoriami do warstwy serwisów aplikacji.

Pozostało 580 znaków

2013-04-15 12:00
1
micc napisał(a):

Nie widzę różnicy pomiędzy zapisem GetCustomerByCityName("Warszawa") a zapytaniem FindWhere(a=>a.CityName.Equals("Warszawa")).

A między FindWhere(a=>a.Addresses.Cities.Where(c => c.Name.Equals("Warszawa")) też nie?
W ten sposób, w warstwie logiki biznesowej zajmujesz się konstrukcją zapytań o dane. Te wszystkie lambdy to są właśnie zapytania o dane, kompletnie nieistotne z punktu widzenia logiki biznesowej.

Zapytania nie buduję w warstwie biznesowej, ponieważ LINQ jest częścią wykorzystywanego języka programowania, a nie warstwy dostępu do danych (jako parametry do metod repozytoriów używane są tylko te wyrażenia, które są zgodne z LINQ to Objects).

LINQ, jak sama nazwa wskazuje, to język zapytań. Używając go gdziekolwiek, budujesz zapytanie do źródła danych.

Zawsze mogę przejść przykładowo z EF na nHibernate i wyłuskać wymagane search criteria z wyrażenia LINQ.

Tylko kto używa LINQ w NHibernate?
Właśnie to jest ten problem - jakbyś miał prawdziwe repozytorium, to byś mógł faktycznie efektywnie podmienić źródło danych. Ty masz repozytorium ściśle związane z LINQ, więc możesz użyć tylko tego, co jest z tą technologią kompatybilne.
To właściwie dość dziwnie wygląda, gdy tyle piszesz o abstrakcji, elastyczności i SoC, jednocześnie ściśle wiążąc się z jedną konkretną biblioteką ORM.

Mówiąc o "rzeczywistej kolekcji" miałem na myśli operowanie w warstwie biznesnowej na IEnumerable, a nie IQueryable.

No to ja dokładnie tak robię, i nie mam repozytorium. (Zresztą, IQueryable w kontekście dostępu do danych w ogóle nie używam.)

http://www.ben-morris.com/why[...]y-is-just-a-lazy-anti-pattern
http://www.sapiensworks.com/b[...]itory-Is-An-Anti-Pattern.aspx
http://ayende.com/blog/3955/repository-is-the-new-singleton


"HUMAN BEINGS MAKE LIFE SO INTERESTING. DO YOU KNOW, THAT IN A UNIVERSE SO FULL OF WONDERS, THEY HAVE MANAGED TO INVENT BOREDOM."

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0