SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.
export class Post {
constructor(
private readonly id: PostId,
private readonly title: string,
private readonly content: string,
private readonly categoryName: string,
private readonly categoryDescription: string,
private readonly categoryId: string,
private readonly visible = true,
) {}
toggleVisibility() {
this.visible = !this.visible;
}
changeCategory(dto: CategoryDto) {
this.categoryName = dto.name;
this.categoryDescription= dto.description;
this.categoryId = dto.id;
}
categoryIs(categorySlug: string): boolean {
return this.categorySlug === categorySlug;
}
}
Jak to naprawić? Oczywiście poprzez rozdzielenie odpowiedzialności: post swoje, a kategoria swoje. Do samego postu możemy wstrzyknąć cały obiekt kategorii, bądź samo jej id - w zależności od problemu z jakim się spotykamy.export class Category {
constructor(
private readonly id: CategoryId,
private readonly name: string,
private readonly description: string,
) {}
update(dto: CategoryDto) {
this.name = dto.name;
this.description = dto.description;
}
}
export class Post {
constructor(
private readonly id: PostId,
private readonly title: string,
private readonly content: string,
private readonly categoryId: CategoryId,
private readonly visible = true,
) {}
toggleVisibility() {
this.visible = !this.visible;
}
changeCategory(categoryId: CategoryId) {
this.categoryId = categoryId;
}
categoryIs(categoryId: CategoryId): boolean {
return this.categoryId === categoryId;
}
}
export class Product {
constructor(
private readonly id: ProductId,
private readonly name: string,
private readonly basePrice: Price,
private readonly category: Category,
) {}
calculatePrice(season: PricingSeason): Price {
const price = this.basePrice;
if (this.category.giftable() && season.preGiftable()) {
price = price.percentageIncrease(15);
}
if (this.category.giftable() && season.giftable()) {
price = price.percentageDecrease(5);
}
if (this.category.giftable() && season.postGiftable()) {
price = price.percentageDecrease(10);
}
if (this.category.excisable()) {
price = price.percentageIncrease(5);
}
return price;
}
}
Jak naprawić kod, aby spełniał open/closed principle? Wystarczy wdrożenie wzorca strategii w jakiejkolwiek jego formie. Użyję jednak trochę innego podejścia. Skorzystajmy z czegoś na wzór policy pattern. Utwórzmy politykę na każdą okazję, a następnie użyjmy kolekcji takich polityk, aby poprawnie wyliczyć cenę.export interface ProductPricePolicy {
calculate(season: PricingSeason, category: Category, base: Price): Price;
}
export class PreGiftablePricePolicy {
calculate(season: PricingSeason, category: Category, base: Price): Price {
return category.giftable() && season.preGiftable()
? price.percentageIncrease(15)
: price;
}
}
export class Product {
constructor(
private readonly id: ProductId,
private readonly name: string,
private readonly basePrice: Price,
private readonly category: Category,
) {}
calculatePrice(season: PricingSeason, policies: ProductPricePolicy[]): Price {
return policies.reduce(
(price: Price, policy: ProductPricePolicy) => policy.calculate(season, this.category, price),
this.basePrice
);
}
}
W ten sposób, jeżeli w przyszłości dojdzie nowa reguła wyliczania ceny, to wystarczy, że dodamy nową politykę i umieścimy ją w przekazywanej kolekcji. Rozszerzanie, bez zmieniania kodu. Problem może pojawić się z argumentami przekazywanymi do polityki (np. cena może w przyszłości zależeć także od innych zmiennych produktu), wtedy możemy przekazać do polityki cały obiekt produktu, a jeżeli komuś wadzi takie przekazywanie, to polityki możemy wtedy iterować gdzieś wyżej (w serwisie?).export abstract class BlogMedia {
// other logic
abstract toPDF(): PDF;
}
export class Post extends BlogMedia {
toPDF(): PDF {
return this.pdfConverter.convert(
this.toPayload()
);
}
}
export class Movie extends BlogMedia {
toPDF(): PDF {
throw new Error('Movie cannot be converted to PDF file!');
}
}
export class BlogMediaService {
convertToPDF(mediaId: string): PDF {
const media: BlodMedia = this.mediaRepository.find(mediaId);
return media.toPDF();
}
}
Naprawa powyższego przykładu może odbyć się na wiele sposobów. Możemy np. wyciągnąć logike konwertującą do PDF poza BlogMedia i użyć interfejsu do wskazania, który BlogMedia może być skonwertowany do postaci pliku pdf.export abstract class BlogMedia {
// other logic
}
export interface class BlogMediaConvertableToPDF {
getBlogMediaPDFPayload(): BlogMediaPDFPayload;
}
export class Post extends BlogMedia implements ConvertableToPDF {
getBlogMediaPDFPayload(): BlogMediaPDFPayload {
return {
name: this.title,
language: this.language,
content: this.content,
};
}
}
export class Movie extends BlogMedia {
// other logic
}
export class BlogMediaService {
convertToPDF(media: BlogMediaConvertableToPDF, converter: PDFConverter): PDF {
return converter.convert(
media.getBlogMediaPDFPayload()
);
}
}
export interface Convertable {
toPDF(): PDF;
toDOC(): DOC;
toHTML(): HTML;
toTXT(): TXT;
}
export class Post implements Convertable {
// implementations
}
Co w sytuacji, jeżeli zapragniemy dodać byt, który jest konwertowalny jedynie do wyjścia bez formatowania, np. tylko czyste pliki tekstowe, ale metoda w serwisie przyjmuje całościowo obiekty klas implementujących Convertable. Rozwiązaniem są mniejsze interfejsy.export interface ConvertableToPDF {
toPDF(): PDF;
}
// ...
export interface ConvertableToTXT {
toTXT(): TXT;
}
export class Post implements ConvertableToPDF, ConvertableToDOC, ConvertableToHTML, ConvertableToTXT {
// implementations
}
export class AccessLog implements ConvertableToTXT {
// implementation
}
Z tą zasadą wiąże się pewien ból, mianowicie nie można tu przesadzić. Usilne tworzenie interfejsu dla każdej metody może spowodować spory bałagan z uwagi na ilość interfejsów, która powstanie, a później trzeba je jeszcze zaimplementować, prawda? Zawsze warto zastanowić się czy duża skrupulatność w granulacji interfejsów jest warta swojej ceny.export class BlogPostsService {
constructor(
private readonly repository: BlogPostRepository,
) {}
remove(id: string) {
const post = this.repository.findOrFail(id);
this.repository.remove(post)
}
}
Naprawa tego kodu będzie bajecznie prosta. Wystarcza wprowadzić dodatkowy interfejs, który zaimplementuje BlogPostRepository. Jak wspomniałem wcześniej, ilość korzyści płynących z takiego zabiegu jest ogromna, ale przed wszystkim wyróżnić należy możliwość łatwej podmiany zależności.export interface BlogPostRepositoryInterface {
findOrFail(id: string): BlogPost;
remove(post: BlogPost);
}
export class BlogPostsService {
constructor(
private readonly repository: BlogPostRepositoryInterface,
) {}
remove(id: string) {
const post = this.repository.findOrFail(id);
this.repository.remove(post)
}
}
Wzorzec projektowy budowniczy służy do dynamicznej budowy obiektów. Pozwala na kreowanie różnych typów obiektu w zależności od przesłanych wymagań. Dzięki temu posiadamy centralny punkt odpowiedzialny jedynie za tworzenie wariacji danego typu.
Sprawdź ten wpisJeżeli masz do czynienia z tworzeniem różnych obiektów bazujących na predefiniowanym modelu, to powinieneś/aś skorzystać z metody wytwórczej. Kreacyjny wzorzec projektowy factory method pozwala być fancy w tworzeniu obiektów.
Sprawdź ten wpisAby projektować dobrą architekturę oprogramowania trzeba najpierw zaznajomić się z jej przesłankami, rodzajami, a także sposobami wdrażania. Architektura nie jest przecież czymś stałym, co wszędzie implementowane jest w jednakowy sposób.
Sprawdź ten wpisCzym jest podejście composition over inheritance? Dzisiaj o tym, dlaczego każdy powinien przemyśleć czy dziedziczenie klas w jego projekcie jest naprawdę potrzebne. Prawdopodobnie powinieneś ograniczyć dziedziczenie klas na rzecz ich kompozycji.
Sprawdź ten wpisCommand-Query Separation, czyli zasada o rozdzielaniu zadań metod tak, aby były jedynie komendami lub zapytaniami. Jeżeli Twoja metoda realizuje logikę i zwraca wyniki, lub podczas pobierania danych wykonuje side taski, to łamie ona CQS.
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.