Przybliżę dzisiaj koncept Value Objects, które w głównej mierze mogą kojarzyć się z Domain Driven Design, ale czy powinniśmy wrzucać je do tego samego worka? Dziś o tym dlaczego Value Object to nie coś dedykowanego jedynie DDD.
export class Money {
constructor(
public readonly amount: number,
public readonly currency: string
) { }
}
const myMoney = new Money(20, 'PLN');
Zgodnie z tym, co napisałem we wstępie, wszystkie podwartości są niemutowalne, dlatego są readonly. W czym taki model Money może nam pomóc? Chociażby w utrzymaniu spójności danych, udostępnieniu przyjemnego interfejsu do manipulacji i tworzeniu samoopisującego się kodu, a przede wszystkim transparentnego przestrzegania reguł biznesowych.export enum Currency {
PLN = 'PLN',
EUR = 'EUR',
USD = 'USD',
}
export class Money {
private static readonly allowedCurrencies = [Currency.PLN, Currency.USD];
constructor(
public readonly amount: number,
public readonly currency: Currency
) {
if (amount <= 0) {
throw new Error('Money amount needs to be greater than 0.');
}
if (!Money.allowedCurrencies.includes(currency)) {
throw new Error(`Currency ${currency} is not allowed.`);
}
}
static fromString(money: string): Money {
let [amount, currency] = money.split(' ');
if (!currency) {
throw new Error('Currency is missing.');
}
amount = Number.parseFloat(amount);
if (Number.isNaN(amount)) {
throw new Error('Wrong amount.');
}
return new Money(amount, currency);
}
add(money: Money): Money {
if (!this.currencyIs(money.currency)) {
throw new Error('Currency missmatch.');
}
return new Money(this.amount + money.amount, this.currency)
}
toString = (): string =>
`${this.getAmount().toFixed(2)} ${this.getCurrency()}`;
private currencyIs = (currency: Currency): boolean =>
this.currency === currency;
}
Podczas tworzenia nowej instancji sprawdzamy czy wartość jest większa od zera oraz czy podana waluta jest poprawna. W przypadku nieprawidłowości rzucamy wyjątkiem, może to być np. RuntimeException. Stworzyłem także metodę fromString(), która zamienia wartości zapisane w stringu do postaci obiektu Money. Może nie jest to do końca prawidłowe rozwiązanie, ponieważ lepsze wydaje się zwracanie całego obiektu, jednak przyjmijmy, że ktoś lub coś tego od nas wymaga. Mamy także metodę toString(), zamieniającą Money do adekwatnego ciągu znaków. Poza tym, prywatna metoda do porównywania dwóch walut. Oprócz tego widoczna jest metoda add(), która sumuje dwie wartości i zwraca nową (Value Objects są niemutowalne!), jednocześnie sprawdzając, czy ktoś nie próbuje dodać 20 PLN do 15 EUR.export class Transaction {
constructor(
private readonly id: TransactionId,
private readonly amount: Money,
private readonly createdAt: Date,
) { }
}
export class TransactionRepository {
constructor(
private readonly transactionDao: TransactionDao,
) { }
save = (transaction: Transaction): Promise<void> =>
this.dao.save(transaction);
async findOne(id: TransactionId): Promise<Transaction> {
const { amount, createdAt } = await this.transactionDao.findOne(id.toString());
return new Transaction(
id,
Money.fromString(amount),
new Date(createdAt)
);
}
}
export class Point {
constructor(
public readonly x: number,
public readonly y: number,
) {}
static fromString(coordinates: string): Point {
let [x, y] = coordinates.split(',');
x = Number.parseFloat(x);
y = Number.parseFloat(y);
if (Number.isNaN(x) || Number.isNaN(y)) {
throw new Error(`Incorrect coordinates: ${coordinates}`);
}
return new Point(x, y);
}
equals = (point: Point): boolean =>
point.x === this.x && point.y === this.y;
toString = (): string =>
`${this.x},${this.y}`
}
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.