领域知识与SOLID单一责任原则的解释


单一责任原则规定一个类或函数应该只有一个改变的理由。本文介绍了为什么理解域对于了解如何实现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 订阅该事件。这将使我们无需同时处理用户的实际删除和删除后的副作用。

事实是,理解域可以通过保持责任的单一性并集中在堆栈的每个级别来改进我们的代码。

结论:设计随着领域启发而提高

当我们在架构层面理解域时,考虑:

  • 我们能够逐个模块地实现
  • 我们可以将代码拆分为子域
  • 我们能够确定如何独立部署微服务

当我们理解域时,在模块级别考虑:
  • 我们能够识别代码块何时不属于该特定子域/模块,并且更适合于另一个子域/模块

当我们了解域时,在类级别考虑以下点:
  • 我们可以理解这个代码块是否属于一个帮助器/实用程序类,或者是否留在这个类中是有意义的。