領域知識與SOLID單一責任原則的解釋

banq發表於2019-06-14

單一責任原則規定一個類或函式應該只有一個改變的理由。本文介紹了為什麼理解域對於瞭解如何實現SRP很重要。SRP是SOLID Princples最難理解的原則,因為每個人對它都有不同的解釋。我將嘗試解釋為什麼理解領域可以幫助您瞭解如何實施SRP。

“理解領域” 完全是單一責任原則的重點。理解域是我們編寫SRP程式碼的唯一方法。

我們根據呼叫一個類的客戶端和這個類的相互關係結構來分割程式碼。下面的示例是包含人力資源部門,IT部門和會計部門的應用程式,每個部門都需要報告其工作時間並計算其工資。

class Employee {
  public calculatePay (): number {
    // implement algorithm for hr, accounting and it
  }
  public reportHours (): number {
    // implement algorithm for hr, accounting and it
  }

  public save (): Promise<void> {
    // implement algorithm for hr, accounting and it
  }
}


我們意識到,由於每個Employee演算法可能不同,並且變更請求可能來自每個部門,因此建立一個類來定位單個演算法以負責每個不同的參與者(HR,IT和會計)將是危險的。

我們認為最好將它們的演算法分開。

abstract class Employee {
  // This needs to be implemented
  abstract calculatePay (): number;
  // This needs to be implemented
  abstract reportHours (): number;
  // let's assume THIS is going to be the 
  // same algorithm for each employee- it can
  // be shared here.
  protected save (): Promise<void> {
    // common save algorithm
  }
}

class HR extends Employee {
  calculatePay (): number {
    // implement own algorithm
  }
  reportHours (): number {
    // implement own algorithm
  }
}

class Accounting extends Employee {
  calculatePay (): number {
    // implement own algorithm
  }
  reportHours (): number {
    // implement own algorithm
  }

}

class IT extends Employee {
  ...
}


當我們將演算法從Employee類中分解為單獨的一個個演算法時,我們可能會在一個類中試圖維護3種不同的演算法(可能每個都可以獨立地容易改變)。

問題是:我們怎麼知道我們需要這樣做?
我們怎麼可能知道那是一件好事呢?這是因為我們正在考慮這個領域

軟體設計正在對未來進行有根據的猜測
有時候,我將軟體設計等同於在足球中打中場。作為一名中場球員,你必須時刻注意周圍發生的事情。一個好的中場球員應該在任何時候都試圖預測未來3秒鐘會發生什麼。

一個偉大的中場球員對周圍的環境非常敏感,並且經常會在隊友需要他的地方,甚至在他們知道他們需要她在那裡之前。

她能夠確定她的隊友是否以及什麼時候會被阻擋並被壓迫傳球,因此她將自己定位為可用於該傳球。

軟體設計和架構類似。我們什麼我們預計將會需要在未來發生。

我們制定這些知情和受過教育的設計決策的唯一途徑是什麼?瞭解我們正在開發的領域

如果我們不理解我們編寫程式碼的領域,那麼我們註定要製造昂貴的混亂,因為軟體需求肯定會隨著時間而變化。

因此,如果您瞭解領域,我相信單一責任可以正確完成。我在早期的合作角色中編寫的相當多的糟糕程式碼,因為我不關心理解領域,只是想證明我可以編寫可執行的程式碼。

不要像我一樣。不要成為程式碼。(要凌駕於程式碼之上)

與領域專家交談,加強對域的理解以及提出問題所花費的時間通常與我們的程式碼質量有關。

這段程式碼對您負有特殊責任嗎?
我在網際網路上找到了這個示例Nodejs / JavaScript程式碼,我想談談它。

const UserModel = require('models/user');
const EmailService = require('services/email');
const NotificationService = require('services/notification');
const Logger = require('services/logger');

const removeUserHandler = async (userId) => {
  const message = 'Your account has been deleted';
  try {
    const user = await UserModel.getUser(userId);
    await UserModel.removeUser(userId);
    await EmailService.send(userId, message);
    await NotificationService.push(userId, message);
    return true;
  } catch (e) {
    console.error(e);
    Logger.send('removeUserHandler', e);
  };
  return true;
};



這段程式碼對您說單一責任嗎?
起初,我認為不是因為它必須使用幾個不同的服務,這些服務可能與User這個程式碼可能存在的子域無關。但後來我又考慮深入了一些。

在閱讀並理解了這個removeUserHandler 介面卡的作用之後,它似乎是負責任的兩件事。
  1. 刪除使用者 除了
  2. 刪除使用者後的其他所有副作用(傳送電子郵件,通知使用者,在發生故障時記錄)。

雖然這對我來說不是一個單一的責任,但這是一個公平的責任歸屬。

如果明天,我的經理要告訴我:我需要你確保在使用者被刪除後,我們還會從Amazon S3中刪除他們的影像。

我會確切地知道在哪裡新增該程式碼,因為有一個地方可以改變刪除使用者的副作用。此外,它需要更改的唯一原因是我們是否更改了刪除使用者後所發生情況的要求。

使用領域事件改進它
使用領域事件,我們實際上可以傳送UserRemoved領域事件,讓子域Email和Notification 訂閱該事件。這將使我們無需同時處理使用者的實際刪除和刪除後的副作用。

事實是,理解域可以透過保持責任的單一性並集中在堆疊的每個級別來改進我們的程式碼。

結論:設計隨著領域啟發而提高

當我們在架構層面理解域時,考慮:

  • 我們能夠逐個模組地實現
  • 我們可以將程式碼拆分為子域
  • 我們能夠確定如何獨立部署微服務

當我們理解域時,在模組級別考慮:
  • 我們能夠識別程式碼塊何時不屬於該特定子域/模組,並且更適合於另一個子域/模組

當我們瞭解域時,在類級別考慮以下點:
  • 我們可以理解這個程式碼塊是否屬於一個幫助器/實用程式類,或者是否留在這個類中是有意義的。

相關文章