OOP 核心之 SOLID 初体验

SOLID principles

总结一下过去的半年实习经历,感觉在各种项目中我总是用最简单的代码逻辑完成一些任务,虽然大部分时间都成功 fulfill 了各种坑爹的 requirements 但是总是感觉自己的代码不够优雅,不够美观。而且一旦有了新的requirements,自己很容易开始修改已有的 Production 代码导致各种新的 bug 的引入,所以以至于有时候一个很简单的新功能经常会引进一些新的bug,总结了一下,还是对 OOP 当中 SOLID 了解不够深入。

The principles of SOLID are guidelines that can be applied while working on software to remove code smells by causing the programmer to refactor the software’s source code until it is both legible and extensible. It is part of an overall strategy of agile and adaptive programming. – wiki

具体来讲 SOLID 是五个简称

  1. Single responsibility principle
  2. Open/closed principle
  3. Liskov substitution principle
  4. Interface segregation principle
  5. Dependency inversion principle

Single Responsibility principle

A Class should only have one, and only one, reason to change

举例说明(pseudo code)

<?php

class SalesReporter {
  // 查找关于 start_date 到 end_date 的 报告
  public between($start_date, $end_date) {
    // 检查用户权限

    // 查询 database

    // 将 database 查询转化成 html 并返回
  }
}

上面的例子是典型的违反 Single Responsibility 原则的案例

为什么?

<?php

class SalesReporter {
  // 查找关于 start_date 到 end_date 的 报告
  public between($start_date, $end_date) {
    // 检查用户权限 -> 涉及用户权限,应该交给 middleware / controller 来做

    // 查询 database -> 典型的 Model 层该做的事儿

    // 将 database 查询转化成 html 并返回 -> 典型的 view 层的责任
  }
}
  1. 在一个大类里面引入过多的元素,(auth + dbquery + view format) 使得这个类涉及的东西太多了,其测试不友好(要 mock 出来很多东西才能完成一个简单的 Unit test)
  2. 当这个类中的任何一个元素需要改变的时候我们都要修改这个类的源代码以适配新的 requirements

如何解决?

  1. 在 controller / middleware 的层面就将 authentication 的任务扼杀在摇篮里
  2. 新建 ReportsRepository (interface) 并且接受 start_date end_date 作为参数,返回所有数据查询 将数据查询的逻辑分割在 Repository 里面
  3. 新建 ViewsMaker (interface) 并接受数据 然后加工成我们所需的 format

加工完了之应该是这样的:

<?php

class SalesReporter {
  protected $repo;
  protected $formatter;
  public function __constructor(ReportsRepository $repo, DataFormatter $formatter) {
    //初始化
  }

  // 查找关于 start_date 到 end 的 报告
  public between($start_date, $end_date) {
    // 查询 $repo 查询 database

    // $formatter 加工数据

    // return formatted data
  }
}

我们看到这个类其实起到了一个 Controller 的作用,就是协调各种类完成一个项目。而各个小的项目则交个不同的类去做 这样分割了 Responsibility 也可以帮助我们更好更清晰地管理代码,最重要的是代码耦合度很低,模块化很高,使 Unit Test 更加轻松 对于每一个模块,因为它只负责一项任务,那么我们只需要提供这项任务的 Mock 就可以完成 Unit Test

Open Close Principle

Entities Should be open for Extension, and closed for modification

这个 原理比较理想化了, 就是我们在迭代的时候永远不要修改我们之前的代码,而是应该刚开始的时候就定好了某一项功能的 interface 然后修改 interface 的实现来完成功能的拓展

Separate extensible behavior behind an interface, and flip the dependencies

什么意思? 假设下面的情况

任务: 收钱

<?php
class Cashier {

  public function __construct(CashCollector $cash) {
      // 初始化
  }

  public function charge ($amount) {
    $this->cash->income($amount);
  }
}
...

问题:如果用信用卡得话那么这个类就废了

怎么解决?

做一个 interface Collector 并且使得 CashCollector, CreditCardCollector 都可以实现这个接口

这样的话 Cashier 完全不用知道自己需要知道自己的 input 是谁也可以收钱, Cashier 只负责收钱但是完全不知道这钱是信用卡来的还是现金来的。所以一旦这个类完成了并且通过了所有的测试,我们永远都不用去修改这个类的内部实现了。 如果需要多收一种 BitCoin 的钱,只需要 implement Collector 接口然后实现 income 方法并且将 BitCoinCollector 注入到 Cashier 中就可以完成收钱的过程了

最最简单的一句话 如果在一个类中检查传入类的类型,那么这时候就要应该检视自己是不是 break 了 open close

所以每次使用 instanceof 之类的运算符的时候就要想多一层了

这就是 控制反转 的概念, 对于底层的类,应该告诉他处理什么东西,而不是由底层去根据类型的种类做出决策

Liskov Substitution

Let q(x) be a property provable about objects x of type T, then q(y) should be provable for object y of type S, where S is subtype of T

Derived Classes must be substitutable for their base class

简单讲就是子类(implement 或者 extend) 必须可以完全替代他们的父类

四个标志

  1. Signature Must Match(参数的数量,类型,返回值的类型必须相同)
  2. Preconditions can’t be greater (子类的执行条件不可以多于父类)
  3. Postconditions at least equal to (子类应该完成父类本身应该完成的东西并完成更多的任务)
  4. Exception types must match(子类父类抛出的异常类型必须相符)

这个就比较抽象了,(其实强类型语言已经在 1 上做了强制)

继续上例子

<?php

class VideoPlayer {
  public function playVideo($file) {
    // play it with basic player
  }
}

class AviPlayer extends VideoPlayer {
  public function playVideo($file) {
    if(/* type is not avi */) {
      throw Exception; // violation 2. 4.
    }

    // player it with awesome player
    // awesome player = basic player + something else
    -> 3 fulfilled
  }
}

其中 AviPlayer 典型违反了 LSP。

因为

  1. 父类没有抛出异常
  2. 子类做了额外的检查使得行为不一致了

就行为来说 (post condition),子类做的事儿应该是完成了父类的任务之后多加了一些新的行为,所以上面例子里面使用了高级播放器播放所以没有违反 LSP

1 太简单就不解释了。

Interface Segregation

A client should not be force be implemented an interface that it doesn’t use

这个原则很简单。 就是 interface 必须要小而精,不要做很多大的 interface, 因为 大 interface 里面很可能含有一些类不必要的接口

<?php

interface WorkerInterface() {
  public function work();
  public function sleep();
}

class HumanWorker implements WorkerInterface {
  public function work() {}
  public function sleep() {}
}

class AndroidWorker implements WorkerInterface {
  public function work() {}
  public function sleep() {} // violates Interface Segregation
}

遇到这样的情况就应该将 interface 再次做进一步的细致划分

<?php

interface Workable() {
  public function work();
}

interface Sleepable() {
  public function sleep();
}

最后一个很简单的标志就是,当 implements 一个接口之后发现实现接口时 return null 那就是时候重新审视了

解决方案: 再次抽象

<?php

interface Workable() {
  public function work();
}

interface Sleepable() {
  public function sleep();
}

interface Manageable() {
  public function manage();
}

class HumanWorker implements Workable, Sleepable, Manageable {
  public function work() { return 'working'; }
  public function sleep() { return 'sleeping'; }

  public function manage()
  {
    $this->work();
    $this->sleep();
  }
}

class AndroidWorker implements Workable, Manageable {
  public function work() {return 'Android Working'; }
  public function manage()
  {
    $this->work();
  }

  function (Managable $worker) {
    return $worker->manage();
  }
}

至此可以看到 再次抽象 的方法将接口再次抽象并且交给不同的具象类处理不同类型区别对待 同时维持了 open/close + LSP

Dependency Inversion

Depend on abstractions, not on concretions

这其实是之前各种原则的一个范式, code to interface

值得注意的是 dependency inversion != dependency injection

dependency injection 说的是将 dependency 注入 到需要那个 dependency 的类中

dependency inversion 说的是 高层功能应该依赖于抽象层,而不是具项实现

所以 dependency injection 其实是可以实现 dependency inversion 的一个方法

High level modules should not depend on low level modules, depend on abstractions, not concretions

<?php
interface ConnectionInterface {
  public function connect();
}

class MySqlConnection implements ConnectionInterface {
  public function connect() {

  }
}

class PasswordRewinder {
  // Dependency injection!
  public function __construct (ConnectionInterface $connection) {

  }
}

上面例子中最高层的功能是重设密码类,它将使用 database connection, 然而我们并没有将 MysqlConnection 注入到构造器中,而是将一个抽象的 interface 注入

这就是 Dependency Inversion,就是高层依赖底层抽象层 > 底层抽象层依赖抽象层 > 。。。

由此就会自然导出 IoC 的概念 Inversion of Control

本能而言我们码代码的时候会自然而然地将现在的类想象成最高层的控制者,它将从根本上控制整个业务的流向。 我刚刚开始用 CodeIgniter 的时候就是这种想法,controller 经常动不动就是上百行

但是这显然不符合 Inversion Of Control 原则。 很多时候,比如 MVC 中 M 层 拿 data,用 MySQL 还是 Mongo 并不是我们顶层 Controller 应该考虑的事儿,Controller 应该可以顺利地拿到 data 但是 Controller 不应该知道 data 是怎么拿出来的。

就好像做外包的时候我们负责编码,客户直管看最后做出来出来的网站但是根本不在意我们是用怎么实现整个系统的。

真正拥有抽象层的人并不是客户,而是我们 实现者。 对于 Controller 中的 Model 也是一样的原理。 Controller 不管 Model 怎么拿到 data 的,只要能接到一个装满 data 的 Collection 或者 Array 就大功告成了

所以 Controller 并不是真正意义上的掌控者,而是一个 要求提取者, 掌控者其实是 laravel Eloquent, 它拥有 MySQL 的抽象连接层 就好像外包给我的项目最后要看我做出个什么鬼来

至于好处,用外包的概念就很好理解了。 如果说我做的网站太烂了,那么中介 IoC Container 就可以终止我的协议,要求另一个外包商去完成客户的 requirement (interface/抽象层) => resolve out of IoC Container

这就可以使整个生态圈松耦合 (decoupling) 即客户的要求得以实现 (Controller 拿到 data) 但是底层的实现可以是 Mongo 也可以是 MySQL, 底层 MySQL 实现不知道谁要调用它,但是它可以完成特定的接口功能并传回 data 就可以完成任务

汗。。。。 太理论了, 但是 inversion controller 确实不好理解,花了很久才看出个大概其来

参考:

happy coding, may the code will always be with you~