SOLID设计原则:单一职责原则SRP

446 阅读2分钟

定义

今天讨论下软件架构设计原则SOLID中的S,即SRP(Single Responsibility Principle),网上的定义是:

A class should have a single responsibility and this responsibility should be entirely encapsulated by the class.

一个类应该有一个单一的职责,并且这个职责应该被该类完全封装。

在《Clean Architecture》中最初的定义是"a module should have one, and only one, reason to change”。 但后来Uncle Bob修改成了"a module should be responsible to one, and only one, actor."

从这些定义中可以抓取几个关键字:responsibility、change、actor,并结合SRP介绍中最经常提及的例子进行理解。

举例分析

# 例子1:Employee

《Clean Architecture》中提到的例子是Employee,其中Employee这个类里面包含了3个方法,calcuatePay()由财务部制定,向CFO汇报;reportHours()由人力资源部制定,向COO汇报;save()由DBA制定,向CTO汇报。这3个函数【实现】在同一个类Employee中,这导致任何他们一方修改逻辑有可能会影响到其他部门,这样Employee从设计角度涵盖了多个责任,不符合SRP原则。

出于这个原因,把所有的逻辑放到一个类中不合理。

于是新的结构中,将所有的功能进行分类在3个类中实现,共享一份Employee数据,但这又引入了新问题,需要使用Employee的人需要实例化3个类。一种通用的解决方法就是用Facade设计模式将3种能力封装提供给外界使用。

在这个经典例子中,从程序猿角度看Employee的3个功能计算工资、汇报工时、存储,但我们划分的依据是看这些功能对应负责(responsibility)的actor,比如这里是CFO、COO、CTO,这些actor的需求就是该模块后续修改(change)的来源。需要注意具有功能跟responsibility是不同概念:

  1. 如果从程序猿视角某个类需要m个不同功能,但m个功能其实只有对应负责的n个actor(n < m),那应该是要拆分成n个模块,即功能不等于responsibility

  2. 如果这个类有某个功能,但这个功能的实现是在另外一个类中,如Employee中的save是由EmployeeSaver.saveEmployee()来实现,那保存employee的responsibility是由EmployeeSaver来完成,而不是由Employee,后续要修改(change)改功能也不需要动Employee,即responsibility意味着后续要负责change。

# 例子2:ORM

再看一个例子,ORM(Object/Relation Mapping对象关系映射)框架流行着两种设计方法:ActiveRecord和DataMapper,见参考#2。

ActiveRecord模式在许多框架中都有,从Rails的ActiveRecord,到Laravel的Eloquent,以PHP中Eloquent 为例

namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{    
     /**
     * 与模型关联的表名     
     *     
     * @var string
     */    
    protected $table = 'my_flights';
     /**
     * 与表关联的主键     
     *     
     * @var string
     */    
    protected $primaryKey = 'flight_id';
}

//检索
$flights = App\Models\Flight::all();
foreach ($flights as $flight) {
    echo $flight->name;
}
//更新
$flight = App\Models\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();

一般ActiveRecord都同时处理两件事情:

  1. Data Access Logic (处理数据访问相关操作,如DB操作)

  2. Domain Logic(处理业务逻辑)

这违背了SRP原则,比如DB Schema一变,这部分代码就需要修改;另外业务逻辑一改,这部分代码也需要进行修改,而且很多开发人员还会把所有可能的功能都倾销到他们的模型类中,导致Model越来越大,而且由于依赖DB,这部分代码也很难编写单测。关于AR的弊端可以细读参考#5。

另外一种ORM设计是DataMapper模式,它与ActiveRecord的区别在于它增加了一个映射器类,将持久化对象的数据和行为分开,保持了单一职责原则。可以看下Doctrine的例子。

数据模型

<?php
// src/Product.php
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="products")
*/
class Product
{    
     /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */    
    protected $id;
     /**
     * @ORM\Column(type="string")
     */    
    protected $name;
    public function getId()
    {
        return $this->id;
    }    
    public function getName()
    {
        return $this->name;
    }    
    public function setName($name)
    {
        $this->name = $name;
    }
}

持久化操作

<?php
// create_product.php <name>
require_once "bootstrap.php";
$newProductName = $argv[1];
$product = new Product();
$product->setName($newProductName);
$entityManager->persist($product);
$entityManager->flush();
echo "Created Product with ID " . $product->getId() . "\n";

获取数据

<?php
// list_products.phprequire_once "bootstrap.php";
$productRepository = $entityManager->getRepository('Product');
$products = $productRepository->findAll();
foreach ($products as $product) {
    echo sprintf("-%s\n", $product->getName());
}

通过这种方式能把业务代码和数据隔离开。

思考

  1. ActiveRecord是种常用的ORM手段,但他违反了SRP原则,那为啥还流行,好处是什么?

  2. SRP vs KISS?

  3. SRP中的责任是指功能还是指实现?

  4. SRP用于类和模块的设计,那是否适用于数据库表呢?

  5. 模块是指啥?函数是否也算?

参考

  1. blog.ndepend.com/solid-desig…

  2. 【ORM 的两种模式:Active Record 与 Data Mapper 比较】jerrymei.cn/orm-activer…

  3. blog.cleancoder.com/uncle-bob/2…

  4. pro-hussein-reda.medium.com/solid-princ…

  5. 【批評 ACTIVE RECORD 的13個論點:最好用也最危險的 ANTI-PATTERN】blog.turn.tw/?p=2992