W dzisiejszym wpisie wkraczamy do warstwy dostępu do danych i przy tej okazji omówię znany i lubiany wzorzec projektowy: Repository. O tym, czym jest repozytorium, jak powinno wyglądać i czym tak naprawdę powinno się zajmować.
export class TagRepository extends Repository<TagEntity> {
private static QUERY_ALIAS = 'tags';
findOneById = (id: string): Promise<TagEntity> =>
this.createQueryBuilder(TagRepository.QUERY_ALIAS)
.leftJoinAndSelect(`${TagRepository.QUERY_ALIAS}.site`, 'site')
.leftJoinAndSelect('site.pages', 'pages')
.where(`${TagRepository.QUERY_ALIAS}.id = :id`, { id })
.getOne();
findAll = (): Promise<TagEntity[]> =>
this.createQueryBuilder(TagRepository.QUERY_ALIAS)
.leftJoinAndSelect(`${TagRepository.QUERY_ALIAS}.site`, 'site')
.leftJoinAndSelect('site.pages', 'pages')
.getMany();
}
Kolejny, bardzo powszechny błąd to brak atomowości. Samo używanie DAO nie rozwiąże wszystkich problemów. Repozytorium powinno dotyczyć tylko jednego typu danych (podobnie, jak kolekcja). Nie możemy w jednym repozytorium mieć metod dla encji A oraz encji B. Cała taka klasa powinna być dedykowana konkretnemu typowi danych.export class UserRepository {
constructor(
private readonly userDAO: UserDAO,
private readonly accountDAO: AccountDAO,
) {}
findUserById = (id: string): Promise<UserEntity> =>
this.userDAO.findOneById(id);
findAccountByUserId = (id: string): Promise<AccountEntity> =>
this.accountDAO.findOneByUserId(id);
}
export class UserRepository {
constructor(
private readonly userDAO: UserDAO
) {}
findOneById = (id: string): Promise<UserEntity> =>
this.userDAO.findOneById(id);
findMany = (): Promise<UserEntity[]> =>
this.userDAO.findMany();
async add(user: UserEntity): Promise<void> {
await this.userDAO.save(user);
}
async remove(user: UserEntity): Promise<void> {
await this.userDAO.delete({ id: user.id });
}
}
Rygorystyczne postrzeganie tych klas, jako wspomnianych zbiorów, pozwala nam na posiadanie łatwego interfejsu do manipulacji na konkretnych informacjach. Pojawić się jednak może kilka pytań, co do słuszności tworzenia takich repozytoriów. Po pierwsze, co w przypadku, jeśli musimy udostępnić wiele metod do wybierania danych, które będą obsługiwały różne warunki, np. getByName(), getById(), getByAge() itp. Możemy wtedy posiadać tylko jedną metodę do wybierania rekordów, np. findMany(), która posiadać będzie opcjonalny parametr criteria, będący zbiorem warunków dla naszego zapytania.export class UserRepository {
constructor(
private readonly userDAO: UserDAO
) {}
findMany = (criteria: Criteria): Promise<UserEntity[]> =>
this.userDAO.findMany(criteria);
}
Kolejny temat to konieczność dokonywania mapowań na poziomie repozytorium. Wyobraźmy sobie, że posiadamy zagnieżdżone dane w polu naszej encji. Niech to będzie user, który posiada pole address, będące również obiektem. Zdecydowaliśmy się, że zapiszemy te dane, jako json w pojedynczej kolumnie tabeli users. Po wybraniu danych z bazy, chcemy w obiekcie użytkownika posiadać obiekt adresu, zamiast ciągu znaków json. Całkowicie dozwolone jest dokonywanie takich mapowań przed zwróceniem danych. Trzeba jednak pamiętać, że nie chodzi tutaj o mapowanie do form porządanych przez wyższe warstwe, a jedynie doprowadzenie surowych danych z bazy do postaci poprawnej encji.export class UserRepository {
constructor(
private readonly userDAO: UserDAO
) {}
async findOneById(id: string): Promise<UserEntity> {
const user = await this.userDAO.findOneById(id);
const address = JSON.parse(user.address);
user.setAddress(address);
return user;
}
}
Dociekliwi zapytają też, a co w przypadku kiedy nie używam ORM i moje DAO zwraca surowe dane? Wtedy musimy utworzyć obiekt encji i jej wszystkie zależności manualnie. Zadanie ułatwi np. zastosowanie konstruktora encji, fabryki lub mappera. Wtedy nasze repozytorium musi użyć zastosowanego rozwiązania przed zwróceniem danych. export class UserRepository {
constructor(
private readonly userDAO: UserDAO
) {}
async findOneById(id: string): Promise<UserEntity> {
const userData = await this.userDAO.findOneById(id);
return new UserEntity({
...userData,
address: JSON.parse(userData.address)
});
}
}
export class ExchangeRateRepository {
constructor(
private readonly exchangeRateApiClient: ExchangeRateApiClientInterface
) {}
getExchangeRate = (from: CurrencyCodeEnum, to: CurrencyCodeEnum): Promise<number> =>
this.exchangeRateApiClient.getExchangeRate({
sourceCurrencyCode: from,
destinationCurrencyCode: to
});
}
Zasada DRY, to po przełożeniu na język polski: nie powtarzaj się. Brzmi bardzo banalnie, jednak dosyć często okazuje się, że mamy problem z jej stosowaniem w kodzie. Dziś dowiesz się, jak nie łamać tej reguły, a co za tym idzie nie powtarzać się.
Sprawdź ten wpisYAGNI to kwintesencja zasad clean code. Dotyczy ona bezużyteczności kodu, a dokładniej, konieczności usuwania tych fragmentów, które nie są potrzebne. W myśl "You aren't gonna need it" nie powinniśmy tworzyć niczego więcej, niż to, co jest potrzebne.
Sprawdź ten wpisZasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.
Sprawdź ten wpisWzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.
Sprawdź ten wpisKolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.
Sprawdź ten wpisFanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.