Postanowiłem użyć w moim projekcie wzorca projektorowego: repozytorium (ang. repository). Myślę, że stworzenie samemu repozytorium nie jest takie trudne. Krok po kroku przedstawię jak można samemu do tego dojść.
Posiadam tabelę w bazie danych o nazwie Player, która jest odzwierciedlona w kodzie tak:
public class Player { public int ID { get; set; } public string Name { get; set; } public string Surname { get; set; } }
Posiadam również klasę dzięki której mam dostęp do bazy danych: DbContext. Aby pobrać wszystkie rekordy z bazy danych z informacjami o graczach wystarczy, że napiszę:
var dbContext = new DbContext(); var players = dbContext.Set<Player>();
Powiedzmy, że chcę napisać klasę, która będzie wykonywała potrzebne operacje pobierania rekordów z bazy danych i zwracała to co potrzebuję: czyli wszystkich graczy oraz wszystkich graczy w kolejności alfabetycznej.
public class Repository { private DbContext _dbContext; public Repository(DbContext dbContext) { _dbContext = dbContext; } public IEnumerable<Player> GetAllPlayers() { return _dbContext.Set<Player>(); } public IEnumerable<Player> GetAllPLayersOrderByName() { var players = GetAllPlayers(); var orderedPlayers = players.OrderBy(p => p.Name); return orderedPlayers; } }
Proste. Na potrzeby testowania stworzę do tej klasy interfejs.
public interface IRepository { IEnumerable<Player> GetAllOrderByName(); IEnumerable<Player> GetAllPlayers(); }
public class Repository : IRepository { private DbContext _dbContext; ... }
Po chwili jednak przyszło mi do głowy, że potrzebowałbym też wyciągnąć z bazy danych dane z tabeli Venue, której odzwierciedlenie w kodzie wygląda tak:
public class Venue { public int ID { get; set; } public string Name { get; set; } public string Address { get; set; } public DayOfWeek Day { get; set; } }
I również w tym przypadku chciałbym używać klasy, która zwracałaby to co potrzebuję i jest związane z Venue: Wszystkie dostępne miejsca oraz miejsca posortowane po dacie tygodnia w której grany jest turniej.Nie przejmując się wiele postanowiłem nową funkcjonalność wrzucić do istniejącej już klasy:
public class Repository { ... public IEnumerable<Venue> GetAllVenues() { return _dbContext.Set<Venue>(); } public IEnumerable<Venue> GetVenuesOrderByDay() { var venues = GetAllVenues(); var orderedVenues = venues.OrderBy(v => v.Day); return orderedVenues; } }
Rozszerzę interfejs:
public interface IRepository { IEnumerable<Player> GetAllOrderByName(); IEnumerable<Player> GetAllPlayers(); IEnumerable<Venue> GetAllVenues(); IEnumerable<Venue> GetVenuesOrderByDay(); }
Żyć nie umierać! Ale niestety łamię zasadę SOLID: Zasadę Jednej Odpowiedzialności – klasa Repository ma za dużo obowiązków: zajmuje się jednocześnie Graczami oraz Miejscami. No to naprawmy to. Stworzę osobną klasę, która będzie zwracać potrzebne rzeczy związane z klasą Venue oraz osobną klasę która będzie obsługiwać klasę Player.
public class PlayerRepository : IPlayerRepository { private DbContext _dbContext; public PlayerRepository(DbContext dbContext) { _dbContext = dbContext; } public IEnumerable<Player> GetAllPlayers() { return _dbContext.Set<Player>(); } public IEnumerable<Player> GetAllOrderByName() { var players = GetAllPlayers(); var orderedPlayers = players.OrderBy(p => p.Name); return orderedPlayers; } } public class VenueRepository : IVenueRepository { private DbContext _dbContext; public VenueRepository(DbContext dbContext) { _dbContext = dbContext; } public IEnumerable<Venue> GetAllVenues() { return _dbContext.Set<Venue>(); } public IEnumerable<Venue> GetVenuesOrderByDay() { var venues = GetAllVenues(); var orderedVenues = venues.OrderBy(v => v.Day); return orderedVenues; } }
Jak pewnie zauważyłeś od razu zaimplementowałem odpowiednie interfejsy. No bo po co klasa ma implementować coś z czego nie będzie korzystać (Zasada Segregacji Interfejsów)?
public interface IPlayerRepository { IEnumerable<Player> GetAllOrderByName(); IEnumerable<Player> GetAllPlayers(); } public interface IVenueRepository { IEnumerable<Venue> GetAllVenues(); IEnumerable<Venue> GetVenuesOrderByDay(); }
Rzucają mi się tu w oczy jeszcze dwie rzeczy: obydwie klasy mają jedno pole: DbContext oraz z obydwie klasy mają metodę której zadaniem jest wyciągniecie wszystkich elementów danego typu z bazy danych. Rozwiązaniem na to jest dziedziczenie klasy generycznej. Rozwiązanie podaję na talerzu:
public class Repository<T> { protected readonly DbContext DbContext; public Repository(DbContext dbContext) { DbContext = dbContext; } public IEnumerable<T> GetAll() { return DbContext.Set<T>(); } }
To teraz poprawmy klasy: VenueRepository oraz PlayerRepository, aby dziedziczyły po klasie Repository.
public class PlayerRepository : Repository<Player> { public PlayerRepository(DbContext dbContext) : base(dbContext) { } public IEnumerable<Player> GetAllOrderByName() { var players = GetAll(); var orderedPlayers = players.OrderBy(p => p.Name); return orderedPlayers; } } public class VenueRepository : Repository<Venue> { public VenueRepository(DbContext dbContext) : base(dbContext) { } public IEnumerable<Venue> GetVenuesOrderByDay() { var venues = GetAll(); var orderedVenues = venues.OrderBy(v => v.Day); return orderedVenues; } }
Jak widać teraz używam tylko metody GetAll(). Dzięki zastosowaniu klasy generycznej, metoda GetAll zwraca albo graczy albo miejsca. To zależy w której klasie wywołam tą metodę. Idźmy dalej… Aby móc mockować klasę Repository wypadałoby dodać interfejs do tej klasy:
public interface IRepository<T> where T : class { IEnumerable<T> GetAll(); } public class Repository<T> : IRepository<T> where T : class { protected readonly DbContext DbContext; ... }
Wróćmy do IPlayerRepository oraz IVenueRepository. Poprawmy te interfejsy, pozbądźmy się niepotrzebnych metod.
public interface IPlayerRepository { IEnumerable<Player> GetAllOrderByName(); } public interface IVenueRepository { IEnumerable<Venue> GetVenuesOrderByDay(); } public class PlayerRepository : Repository<Player>, IPlayerRepository { public PlayerRepository(DbContext dbContext) : base(dbContext) { } public IEnumerable<Player> GetAllOrderByName() { var players = GetAll(); var orderedPlayers = players.OrderBy(p => p.Name); return orderedPlayers; } } public class VenueRepository : Repository<Venue>, IVenueRepository { public VenueRepository(DbContext dbContext) : base(dbContext) { } public IEnumerable<Venue> GetVenuesOrderByDay() { var venues = GetAll(); var orderedVenues = venues.OrderBy(v => v.Day); return orderedVenues; } }
Z interfejsów usunąłem metody GetAllPlayers oraz GetAllVenues. One są zastąpione przez jedną generyczną: GetAll.
Tak widzę właśnie budowę repozytorium od początku. Jeżeli chcesz zobaczyć jak można stworzyć dobre repozytorium zapraszam do obejrzenia filmiku na youtubie (patrz niżej), który moim zdaniem wyjaśnia całe zagadnienie w bardzo przystępny i fajny sposób.
Repozytorium, Linki:
Film na YouTubie, który świetnie wyjaśnia Repository Pattern
Marcin Dembowski o repozytorium
CodingTV – czy warto stosować repozytorium
Czemu Repository jest public i czemu ma interfejs?
Cześć Dawid K.
Nie rozumiem dlaczego Repository nie miałby być public.
A co do drugiego pytania to tutaj wydaje mi się że chodzi o rozszerzalność. Osobiście do mnie trafia odpowiedź z tego posta: http://stackoverflow.com/questions/10616131/repository-pattern-why-exactly-do-we-need-interfaces .
Dawid, dzięki za pytania. Znowu coś do przodu jestem!
Kod kliencki powinien korzystać wyłącznie z konkretnych repozytoriów, i te faktycznie powinny mieć interfejsy i być publiczne. Zaś generyczne repozytorium bazowe powinno być internal, no i raczej nie potrzebuje interfejsu. (Zwłaszcza do rozszerzalności, ile nowych metod można jeszcze dopisać do generycznego repozytoruim? :))