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

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

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

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

Начало пути

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

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

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

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

Код Ученика:

// 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]刪除
最新教學 更多>
  • C++20 Consteval函數中模板參數能否依賴於函數參數?
    C++20 Consteval函數中模板參數能否依賴於函數參數?
    [ consteval函數和模板參數依賴於函數參數在C 17中,模板參數不能依賴一個函數參數,因為編譯器仍然需要對非contexexpr futcoriations contim at contexpr function進行評估。 compile time。 C 20引入恆定函數,必須在編譯時進...
    程式設計 發佈於2025-05-28
  • 如何正確使用與PDO參數的查詢一樣?
    如何正確使用與PDO參數的查詢一樣?
    在pdo 中使用類似QUERIES在PDO中的Queries時,您可能會遇到類似疑問中描述的問題:此查詢也可能不會返回結果,即使$ var1和$ var2包含有效的搜索詞。錯誤在於不正確包含%符號。 通過將變量包含在$ params數組中的%符號中,您確保將%字符正確替換到查詢中。沒有此修改,PD...
    程式設計 發佈於2025-05-28
  • 可以在純CS中將多個粘性元素彼此堆疊在一起嗎?
    可以在純CS中將多個粘性元素彼此堆疊在一起嗎?
    [2这里: https://webthemez.com/demo/sticky-multi-header-scroll/index.html </main> <section> { display:grid; grid-template-...
    程式設計 發佈於2025-05-28
  • 如何在Java中正確顯示“ DD/MM/YYYY HH:MM:SS.SS”格式的當前日期和時間?
    如何在Java中正確顯示“ DD/MM/YYYY HH:MM:SS.SS”格式的當前日期和時間?
    如何在“ dd/mm/yyyy hh:mm:mm:ss.ss”格式“ gormat 解決方案:的,請訪問量很大,並應為procectiquiestate的,並在整個代碼上正確格式不多: java.text.simpledateformat; 導入java.util.calendar; 導入java...
    程式設計 發佈於2025-05-28
  • 用戶本地時間格式及時區偏移顯示指南
    用戶本地時間格式及時區偏移顯示指南
    在用戶的語言環境格式中顯示日期/時間,並使用時間偏移在向最終用戶展示日期和時間時,以其localzone and格式顯示它們至關重要。這確保了不同地理位置的清晰度和無縫用戶體驗。以下是使用JavaScript實現此目的的方法。 方法:推薦方法是處理客戶端的Javascript中的日期/時間格式化和...
    程式設計 發佈於2025-05-28
  • Java字符串非空且非null的有效檢查方法
    Java字符串非空且非null的有效檢查方法
    檢查字符串是否不是null而不是空的 if(str!= null && str.isementy())二手: if(str!= null && str.length()== 0) option 3:trim()。 isement(Isement() trim whitespace whites...
    程式設計 發佈於2025-05-28
  • 如何使用“ JSON”軟件包解析JSON陣列?
    如何使用“ JSON”軟件包解析JSON陣列?
    parsing JSON與JSON軟件包 QUALDALS:考慮以下go代碼:字符串 } func main(){ datajson:=`[“ 1”,“ 2”,“ 3”]`` arr:= jsontype {} 摘要:= = json.unmarshal([] byte(...
    程式設計 發佈於2025-05-28
  • Java中如何使用觀察者模式實現自定義事件?
    Java中如何使用觀察者模式實現自定義事件?
    在Java 中創建自定義事件的自定義事件在許多編程場景中都是無關緊要的,使組件能夠基於特定的觸發器相互通信。本文旨在解決以下內容:問題語句我們如何在Java中實現自定義事件以促進基於特定事件的對象之間的交互,定義了管理訂閱者的類界面。 以下代碼片段演示瞭如何使用觀察者模式創建自定義事件: args...
    程式設計 發佈於2025-05-28
  • 如何在其容器中為DIV創建平滑的左右CSS動畫?
    如何在其容器中為DIV創建平滑的左右CSS動畫?
    通用CSS動畫,用於左右運動 ,我們將探索創建一個通用的CSS動畫,以向左和右移動DIV,從而到達其容器的邊緣。該動畫可以應用於具有絕對定位的任何div,無論其未知長度如何。 問題:使用左直接導致瞬時消失 更加流暢的解決方案:混合轉換和左 [並實現平穩的,線性的運動,我們介紹了線性的轉換。...
    程式設計 發佈於2025-05-28
  • Java為何無法創建泛型數組?
    Java為何無法創建泛型數組?
    通用陣列創建錯誤 arrayList [2]; JAVA報告了“通用數組創建”錯誤。為什麼不允許這樣做? 答案:Create an Auxiliary Class:public static ArrayList<myObject>[] a = new ArrayList<my...
    程式設計 發佈於2025-05-28
  • 為什麼我在Silverlight Linq查詢中獲得“無法找到查詢模式的實現”錯誤?
    為什麼我在Silverlight Linq查詢中獲得“無法找到查詢模式的實現”錯誤?
    查詢模式實現缺失:解決“無法找到”錯誤在Silverlight應用程序中,嘗試使用LINQ建立LINQ連接以錯誤而實現的數據庫”,無法找到查詢模式的實現。”當省略LINQ名稱空間或查詢類型缺少IEnumerable 實現時,通常會發生此錯誤。 解決問題來驗證該類型的質量是至關重要的。在此特定實例...
    程式設計 發佈於2025-05-28
  • 在UTF8 MySQL表中正確將Latin1字符轉換為UTF8的方法
    在UTF8 MySQL表中正確將Latin1字符轉換為UTF8的方法
    在UTF8表中將latin1字符轉換為utf8 ,您遇到了一個問題,其中含義的字符(例如,“jáuòiñe”)在utf8 table tabled tablesset中被extect(例如,“致電。為了解決此問題,您正在嘗試使用“ mb_convert_encoding”和“ iconv”轉換受...
    程式設計 發佈於2025-05-28
  • 在GO中構造SQL查詢時,如何安全地加入文本和值?
    在GO中構造SQL查詢時,如何安全地加入文本和值?
    在go中構造文本sql查詢時,在go sql queries 中,在使用conting and contement和contement consem per時,尤其是在使用integer per當per當per時,per per per當per. [&​​&&&&&&&&&&&&&&&默元組方法在...
    程式設計 發佈於2025-05-28
  • 如何從Google API中檢索最新的jQuery庫?
    如何從Google API中檢索最新的jQuery庫?
    從Google APIS 問題中提供的jQuery URL是版本1.2.6。對於檢索最新版本,以前有一種使用特定版本編號的替代方法,它是使用以下語法:獲取最新版本:未壓縮)While these legacy URLs still remain in use, it is recommended ...
    程式設計 發佈於2025-05-28
  • Python讀取CSV文件UnicodeDecodeError終極解決方法
    Python讀取CSV文件UnicodeDecodeError終極解決方法
    在試圖使用已內置的CSV模塊讀取Python中時,CSV文件中的Unicode Decode Decode Decode Decode decode Error讀取,您可能會遇到錯誤的錯誤:無法解碼字節 在位置2-3中:截斷\ uxxxxxxxx逃脫當CSV文件包含特殊字符或Unicode的路徑逃...
    程式設計 發佈於2025-05-28

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

Copyright© 2022 湘ICP备2022001581号-3