"If a worker wants to do his job well, he must first sharpen his tools." - Confucius, "The Analects of Confucius. Lu Linggong"
Front page > Programming > Clean Architecture: The Unattainable Ideal

Clean Architecture: The Unattainable Ideal

Published on 2024-11-04
Browse:270

Чистая Архитектура: Недостижимый Идеал

Начало пути

В горах Тибета, в уединенном монастыре, жил молодой Ученик, стремящийся постичь глубины программирования и достичь гармонии в своём коде. Он мечтал создать приложение, которое отражало бы принципы Чистой Архитектуры. Однажды он решил обратиться к мудрому Мастеру за советом.

Ученик подошёл к Мастеру и спросил:

Ученик: "О, мудрый Мастер, я создал приложение для управления покупками. Моя архитектура чиста?"

Мастер: "Покажи мне своё творение, и мы вместе узнаем истину."
Ученик продемонстрировал свой код, где база данных и сценарий использования были объединены.

Код Ученика:

// 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 выполняется не в базе данных. Приложение тратило много ресурсов на пересылку больших объёмов данных из базы данных в приложение и на их обработку. Если бы расчёт происходил непосредственно в базе данных, не требовалось бы передавать тысячи строк между слоями, что значительно ускорило бы процесс.

Ученик хотел обсудить это с Мастером, но тот куда-то пропал, и вопрос остался без ответа.

Увидев пустой монастырь, Ученик достал новую книгу и сказал: "Похоже, мой путь к просветлению привёл меня к новому испытанию — оптимизации производительности."

Release Statement This article is reproduced at: https://dev.to/simprl/chistaia-arkhitiektura-niedostizhimyi-idieal-35bj?1 If there is any infringement, please contact [email protected] to delete it
Latest tutorial More>

Disclaimer: All resources provided are partly from the Internet. If there is any infringement of your copyright or other rights and interests, please explain the detailed reasons and provide proof of copyright or rights and interests and then send it to the email: [email protected] We will handle it for you as soon as possible.

Copyright© 2022 湘ICP备2022001581号-3