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? :))