В горах Тибета, в уединенном монастыре, жил молодой Ученик, стремящийся постичь глубины программирования и достичь гармонии в своём коде. Он мечтал создать приложение, которое отражало бы принципы Чистой Архитектуры. Однажды он решил обратиться к мудрому Мастеру за советом.
Ученик подошёл к Мастеру и спросил:
Ученик: "О, мудрый Мастер, я создал приложение для управления покупками. Моя архитектура чиста?"
Мастер: "Покажи мне своё творение, и мы вместе узнаем истину."
Ученик продемонстрировал свой код, где база данных и сценарий использования были объединены.
Код Ученика:
// app.ts import sqlite3 from 'sqlite3'; import { open, Database } from 'sqlite'; interface Purchase { id: number; title: string; cost: number; } async function initializeDatabase(): Promise{ const db = await open({ filename: ':memory:', driver: sqlite3.Database, }); await db.exec(` CREATE TABLE purchases ( id INTEGER PRIMARY KEY, title TEXT, cost REAL ) `); return db; } async function addPurchaseIfCan(db: Database, purchase: Purchase): Promise { const { id, title, cost } = purchase; const row = await db.get( `SELECT SUM(cost) as totalCost FROM purchases WHERE title = ?`, [title] ); const totalCost = row?.totalCost || 0; const newTotalCost = totalCost cost; if (newTotalCost { const db = await initializeDatabase(); await addPurchaseIfCan(db, { id: 3, title: 'рис', cost: 2 }); })();
Мастер, после изучения кода, задумчиво произнес:
Мастер: "Твой код подобен реке, где смешаны чистые и мутные воды. Бизнес-логика и детали переплетены. Чтобы достичь истинной чистоты архитектуры, раздели их, как небо и землю."
Поняв наставление, Ученик решил разделить код на уровни, выделяя базу данных и сценарий использования в отдельные модули. Он также ввёл интерфейсы, чтобы следовать принципу инверсии зависимостей, который является краеугольным камнем Чистой Архитектуры. Теперь addPurchaseIfCan будет зависеть от интерфейса, а не от конкретной реализации репозитория.
// app.ts import { initializeDatabase } from './db/init'; import { PurchaseRepository } from './db/purchaseRepository'; import { addPurchaseIfCan } from './useCases/addPurchaseIfCan'; (async () => { const db = await initializeDatabase(); const purchaseRepository = new PurchaseRepository(db); await addPurchaseIfCan(purchaseRepository, { id: 3, title: 'рис', cost: 2 }); })();
// useCases/addPurchaseIfCan.ts import { IPurchaseRepository, Purchase } from './IPurchaseRepository'; export async function addPurchaseIfCan( purchaseRepository: IPurchaseRepository, purchase: Purchase ): Promise{ const { id, title, cost } = purchase; const totalCost = await purchaseRepository.getTotalCostByTitle(title); const newTotalCost = totalCost cost; if (newTotalCost // useCases/IPurchaseRepository.ts export interface IPurchaseRepository { add(purchase: Purchase): Promise; getTotalCostByTitle(title: string): Promise ; } export interface Purchase { id: number; title: string; cost: number; } // db/init.ts import sqlite3 from 'sqlite3'; import { open, Database } from 'sqlite'; export async function initializeDatabase(): Promise{ const db = await open({ filename: ':memory:', driver: sqlite3.Database, }); await db.exec(` CREATE TABLE purchases ( id INTEGER PRIMARY KEY, title TEXT, cost REAL ) `); return db; } // db/purchaseRepository.ts import { Database } from 'sqlite'; import { IPurchaseRepository, Purchase } from 'useCases/IPurchaseRepository'; export class PurchaseRepository implements IPurchaseRepository { private db: Database; constructor(db: Database) { this.db = db; } async add(purchase: Purchase): Promise{ const { id, title, cost } = purchase; await this.db.run( `INSERT INTO purchases (id, title, cost) VALUES (?, ?, ?)`, [id, title, cost] ); return purchase; } async getTotalCostByTitle(title: string): Promise { const row = await this.db.get( `SELECT SUM(cost) as totalCost FROM purchases WHERE title = ?`, [title] ); const totalCost = row?.totalCost || 0; return totalCost; } } Ученик вернулся к Мастеру и спросил:
Ученик: "Я разделил свой код на уровни, выделив базу данных и сценарий использования в отдельные модули, и использовал интерфейсы для репозитория. Моя архитектура стала чище?"
Мастер, глядя на код, ответил:
Мастер: "Ты сделал шаг вперёд, но вычисление totalCost всё ещё происходит в инфраструктурном слое. Однако totalCost относится больше к бизнес-логике твоего сценария использования. Перенеси это вычисление внутрь сценария использования, чтобы отделить бизнес-правила от деталей хранения данных."
Осознание разделения
Ученик осознал, что totalCost должен быть частью бизнес-логики. Он изменил код, чтобы получать список покупок и вычислять totalCost в сценарии использования.
// useCases/IPurchaseRepository.ts export interface IPurchaseRepository { add(purchase: Purchase): Promise; getPurchasesByTitle(title: string): Promise ; } ... // db/purchaseRepository.ts import { Database } from 'sqlite'; import { IPurchaseRepository } from './IPurchaseRepository'; export class PurchaseRepository implements IPurchaseRepository { ... async getPurchasesByTitle(title: string): Promise{ const rows = await this.db.all ( `SELECT * FROM purchases WHERE title = ?`, [title] ); return rows.map((row) => ({ id: row.id, title: row.title, cost: row.cost, })); } } // useCases/addPurchaseIfCan.ts import { IPurchaseRepository, Purchase } from './IPurchaseRepository'; export async function addPurchaseIfTotalCostLessThanLimit( purchaseRepository: IPurchaseRepository, purchaseData: Purchase, limit: number ): Promise{ const { id, title, cost } = purchaseData; const purchases = await purchaseRepository.getPurchasesByTitle(title); let totalCost = 0; for (const purchase of purchases) { totalCost = purchase.cost; } const newTotalCost = totalCost cost; if (newTotalCost Ученик снова подошёл к Мастеру:
Ученик: "Я перенёс вычисление totalCost в сценарий использования и отделил бизнес-логику от инфраструктуры. Моя архитектура стала чище?"
Мастер, с теплотой в голосе, сказал:
Мастер: "Ты сделал значительный прогресс, но арифметические операции могут приводить к неточностям. При работе с десятичными числами обычные операции JavaScript могут быть ненадёжными."
Встреча с деталями реализации
Ученик понял, что работа с числами в JavaScript может вызывать ошибки из-за особенностей представления чисел с плавающей точкой. Он обновил код, используя decimal.js для точных вычислений.
// useCases/addPurchaseIfCan.ts import Decimal from 'decimal.js'; import { IPurchaseRepository, Purchase } from './IPurchaseRepository'; export async function addPurchaseIfCan( purchaseRepository: IPurchaseRepository, purchaseData: Purchase, limit: number ): Promise{ const { id, title, cost } = purchaseData; const purchases = await purchaseRepository.getPurchasesByTitle(title); let totalCost = new Decimal(0); for (const purchase of purchases) { totalCost = totalCost.plus(purchase.cost); } const newTotalCost = totalCost.plus(cost); if (newTotalCost.greaterThanOrEqualTo(limit)) { console.log(`Общая стоимость превышает ${limit}.`); } else { await purchaseRepository.add(purchaseData); console.log('Покупка успешно добавлена.'); } } Ученик вернулся к Мастеру:
Ученик: "Я скорректировал арифметические операции с помощью decimal.js, чтобы избежать неточностей. Моя архитектура стала чище?"
Мастер ответил:
Мастер: "Ты проделал хорошую работу, но твой сценарий использования всё ещё содержит детали реализации. Прямая зависимость от decimal.js привязывает бизнес-логику к конкретной библиотеке. Если ты захочешь изменить библиотеку в будущем, тебе придётся менять бизнес-логику."
Инверсия зависимостей
Понимая проблему, Ученик решил абстрагировать арифметические операции, используя инверсию зависимостей, чтобы бизнес-логика не зависела от конкретной реализации.
// useCases/calculator.ts export abstract class Calculator { abstract add(a: string, b: string): string; abstract greaterThanOrEqual(a: string, b: string): boolean; }// decimalCalculator.ts import Decimal from 'decimal.js'; import { Calculator } from 'useCases/calculator'; export class DecimalCalculator extends Calculator { add(a: string, b: string): string { return new Decimal(a).plus(new Decimal(b)).toString(); } greaterThanOrEqual(a: string, b: string): boolean { return new Decimal(a).greaterThanOrEqualTo(new Decimal(b)); } }// addPurchaseIfCan.ts import { IPurchaseRepository, Purchase } from './IPurchaseRepository'; import { Calculator } from 'useCases/calculator'; export class addPurchaseIfCan { private purchaseRepository: IPurchaseRepository; private calculator: Calculator; private limit: string; constructor( purchaseRepository: IPurchaseRepository, calculator: Calculator, limit: number ) { this.purchaseRepository = purchaseRepository; this.calculator = calculator; this.limit = limit.toString(); } async execute(purchaseData: Purchase): Promise{ const { id, title, cost } = purchaseData; const purchases = await this.purchaseRepository.getPurchasesByTitle(title); let totalCost = '0'; for (const purchase of purchases) { totalCost = this.calculator.add(totalCost, purchase.cost.toString()); } const newTotalCost = this.calculator.add(totalCost, cost.toString()); if (this.calculator.greaterThanOrEqual(newTotalCost, this.limit)) { console.log(`Общая стоимость превышает ${this.limit}.`); } else { await this.purchaseRepository.add({ id, title, cost: parseFloat(cost.toString()), }); console.log('Покупка успешно добавлена.'); } } } // app.ts import { initializeDatabase } from './db/init'; import { PurchaseRepository } from './db/purchaseRepository'; import { AddPurchaseIfCan } from './AddPurchaseIfCan'; import { DecimalCalculator } from './decimalCalculator'; (async () => { const db = await initializeDatabase(); const purchaseRepository = new PurchaseRepository(db); const calculator = new DecimalCalculator(); const limit = 99999; const addPurchaseUseCase = new AddPurchaseIfCan( purchaseRepository, calculator, limit ); await addPurchaseUseCase.execute({ id: 3, title: 'рис', cost: 2 }); })();Ученик снова обратился к Мастеру:
Ученик: "Я абстрагировал арифметические операции с помощью инверсии зависимостей. Теперь моя архитектура чиста?"
Мастер ответил:
Мастер: "Ты сделал значительный прогресс. Но помни, что твои сценарии использования всё ещё зависят от деталей языка программирования. Ты используешь Javascript и Typescript, но если эти технологии перестанут быть актуальными, то тебе прийдется полностью все переписать на другой язык."
Принятие и понимание
Ученик в недоумении задумался, а потом спросил:
Ученик: "Мастер, как же мне достичь идеальной чистоты архитектуры, если мои сценарии использования всё ещё зависят от языка программирования?"
Мастер, улыбаясь, ответил:
Мастер: "Как птица не может отделиться от неба, так и архитектура не может быть полностью независимой от среды. Полная независимость невозможна, но стремление к ней обогащает твою архитектуру. Цель Чистой Архитектуры — создать систему, где изменения могут быть внесены с минимальными усилиями, и где бизнес-логика отделена от деталей реализации. Понимание этого баланса — ключ к истинной мудрости."
Ученик почувствовал просветление и сказал:
Ученик: "Благодарю тебя, Мастер. Теперь я понимаю, что совершенство не в абсолютной изоляции, а в гармоничном разделении ответственности."
Мастер: "Иди с миром, Ученик. Твой путь только начинается, но ты уже нашёл направление."
Эпилог
Спустя некоторое время, Ученик заметил, что его приложение стало работать медленнее. Он был озадачен: почему программа, которая раньше работала быстро, теперь еле справляется со своей задачей?
Оказалось, что это вовсе не из-за того, что исходный код увеличился в 3 раза, а из-за того, что вычисление totalCost выполняется не в базе данных. Приложение тратило много ресурсов на пересылку больших объёмов данных из базы данных в приложение и на их обработку. Если бы расчёт происходил непосредственно в базе данных, не требовалось бы передавать тысячи строк между слоями, что значительно ускорило бы процесс.
Ученик хотел обсудить это с Мастером, но тот куда-то пропал, и вопрос остался без ответа.
Увидев пустой монастырь, Ученик достал новую книгу и сказал: "Похоже, мой путь к просветлению привёл меня к новому испытанию — оптимизации производительности."
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3