」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 清潔架構:遙不可及的理想

清潔架構:遙不可及的理想

發佈於2024-11-04
瀏覽:297

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

Начало пути

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

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

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

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

Код Ученика:

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

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

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

版本聲明 本文轉載於:https://dev.to/simprl/chistaia-arkhitiektura-niedostizhimyi-idieal-35bj?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 為什麼我的CSS背景圖像出現?
    為什麼我的CSS背景圖像出現?
    故障排除:CSS背景圖像未出現 ,您的背景圖像儘管遵循教程說明,但您的背景圖像仍未加載。圖像和样式表位於相同的目錄中,但背景仍然是空白的白色帆布。 而不是不棄用的,您已經使用了CSS樣式: bockent {背景:封閉圖像文件名:背景圖:url(nickcage.jpg); 如果您的html,cs...
    程式設計 發佈於2025-07-03
  • 為什麼不````''{margin:0; }`始終刪除CSS中的最高邊距?
    為什麼不````''{margin:0; }`始終刪除CSS中的最高邊距?
    在CSS 問題:不正確的代碼: 全球範圍將所有餘量重置為零,如提供的代碼所建議的,可能會導致意外的副作用。解決特定的保證金問題是更建議的。 例如,在提供的示例中,將以下代碼添加到CSS中,將解決餘量問題: body H1 { 保證金頂:-40px; } 此方法更精確,避免了由全局保證金重置...
    程式設計 發佈於2025-07-03
  • Java的Map.Entry和SimpleEntry如何簡化鍵值對管理?
    Java的Map.Entry和SimpleEntry如何簡化鍵值對管理?
    A Comprehensive Collection for Value Pairs: Introducing Java's Map.Entry and SimpleEntryIn Java, when defining a collection where each element com...
    程式設計 發佈於2025-07-03
  • 如何干淨地刪除匿名JavaScript事件處理程序?
    如何干淨地刪除匿名JavaScript事件處理程序?
    刪除匿名事件偵聽器將匿名事件偵聽器添加到元素中會提供靈活性和簡單性,但是當要刪除它們時,可以構成挑戰,而無需替換元素本身就可以替換一個問題。 element? element.addeventlistener(event,function(){/在這里工作/},false); 要解決此問題,請考...
    程式設計 發佈於2025-07-03
  • \“(1)vs.(;;):編譯器優化是否消除了性能差異?\”
    \“(1)vs.(;;):編譯器優化是否消除了性能差異?\”
    答案: 在大多數現代編譯器中,while(1)和(1)和(;;)之間沒有性能差異。編譯器: perl: 1 輸入 - > 2 2 NextState(Main 2 -E:1)V-> 3 9 Leaveloop VK/2-> A 3 toterloop(next-> 8 last-> 9 ...
    程式設計 發佈於2025-07-03
  • 在程序退出之前,我需要在C ++中明確刪除堆的堆分配嗎?
    在程序退出之前,我需要在C ++中明確刪除堆的堆分配嗎?
    在C中的顯式刪除 在C中的動態內存分配時,開發人員通常會想知道是否有必要在heap-procal extrable exit exit上進行手動調用“ delete”操作員,但開發人員通常會想知道是否需要手動調用“ delete”操作員。本文深入研究了這個主題。 在C主函數中,使用了動態分配變量(...
    程式設計 發佈於2025-07-03
  • 如何從PHP中的數組中提取隨機元素?
    如何從PHP中的數組中提取隨機元素?
    從陣列中的隨機選擇,可以輕鬆從數組中獲取隨機項目。考慮以下數組:; 從此數組中檢索一個隨機項目,利用array_rand( array_rand()函數從數組返回一個隨機鍵。通過將$項目數組索引使用此鍵,我們可以從數組中訪問一個隨機元素。這種方法為選擇隨機項目提供了一種直接且可靠的方法。
    程式設計 發佈於2025-07-03
  • Spark DataFrame添加常量列的妙招
    Spark DataFrame添加常量列的妙招
    在Spark Dataframe ,將常數列添加到Spark DataFrame,該列具有適用於所有行的任意值的Spark DataFrame,可以通過多種方式實現。使用文字值(SPARK 1.3)在嘗試提供直接值時,用於此問題時,旨在為此目的的column方法可能會導致錯誤。 df.withCo...
    程式設計 發佈於2025-07-03
  • Java中假喚醒真的會發生嗎?
    Java中假喚醒真的會發生嗎?
    在Java中的浪費喚醒:真實性或神話? 在Java同步中偽裝喚醒的概念已經是討論的主題。儘管存在這種行為的潛力,但問題仍然存在:它們實際上是在實踐中發生的嗎? Linux的喚醒機制根據Wikipedia關於偽造喚醒的文章,linux實現了pthread_cond_wait()功能的Linux實現,...
    程式設計 發佈於2025-07-03
  • 如何在GO編譯器中自定義編譯優化?
    如何在GO編譯器中自定義編譯優化?
    在GO編譯器中自定義編譯優化 GO中的默認編譯過程遵循特定的優化策略。 However, users may need to adjust these optimizations for specific requirements.Optimization Control in Go Compi...
    程式設計 發佈於2025-07-03
  • 為什麼使用Firefox後退按鈕時JavaScript執行停止?
    為什麼使用Firefox後退按鈕時JavaScript執行停止?
    導航歷史記錄問題:JavaScript使用Firefox Back Back 此行為是由瀏覽器緩存JavaScript資源引起的。要解決此問題並確保在後續頁面訪問中執行腳本,Firefox用戶應設置一個空功能。 警報'); }; alert('inline Alert')...
    程式設計 發佈於2025-07-03
  • 可以在純CS中將多個粘性元素彼此堆疊在一起嗎?
    可以在純CS中將多個粘性元素彼此堆疊在一起嗎?
    [2这里: https://webthemez.com/demo/sticky-multi-header-scroll/index.html </main> <section> { display:grid; grid-template-...
    程式設計 發佈於2025-07-03
  • Python元類工作原理及類創建與定制
    Python元類工作原理及類創建與定制
    python中的metaclasses是什麼? Metaclasses負責在Python中創建類對象。就像類創建實例一樣,元類也創建類。他們提供了對類創建過程的控制層,允許自定義類行為和屬性。 在Python中理解類作為對象的概念,類是描述用於創建新實例或對象的藍圖的對象。這意味著類本身是使用...
    程式設計 發佈於2025-07-03
  • FastAPI自定義404頁面創建指南
    FastAPI自定義404頁面創建指南
    response = await call_next(request) if response.status_code == 404: return RedirectResponse("https://fastapi.tiangolo.com") else: ...
    程式設計 發佈於2025-07-03
  • 如何在鼠標單擊時編程選擇DIV中的所有文本?
    如何在鼠標單擊時編程選擇DIV中的所有文本?
    在鼠標上選擇div文本單擊帶有文本內容,用戶如何使用單個鼠標單擊單擊div中的整個文本?這允許用戶輕鬆拖放所選的文本或直接複製它。 在單個鼠標上單擊的div元素中選擇文本,您可以使用以下Javascript函數: function selecttext(canduterid){ if(d...
    程式設計 發佈於2025-07-03

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3