- S: 单一职责原则 (SRP)
- O: 开闭原则 (OCP)
- L: 里氏替换原则 (LSP)
- I: 接口隔离原则 (ISP)
- D: 依赖反转原则 (DIP)
单一职责原则 Single Responsibility Principle (SRP)
"修改一个类应该只为一个理由"。人们总是易于用一堆方法塞满一个类,如同我们在飞机上只能携带一个行李箱(把所有的东西都塞到箱子里)。这样做的问题是:从概念上这样的类不是高内聚的,并且留下了很多理由去修改它。将你需要修改类的次数降低到最小很重要。这是因为,当有很多方法在类中时,修改其中一处,你很难知晓在代码库中哪些依赖的模块会被影响到。
Bad:
1 class UserSettings 2 { 3 private $user; 4 5 public function __construct($user) 6 { 7 $this->user = $user; 8 } 9 10 public function changeSettings($settings) 11 { 12 if ($this->verifyCredentials()) { 13 // ... 14 } 15 } 16 17 private function verifyCredentials() 18 { 19 // ... 20 } 21 }
Good:
1 class UserAuth 2 { 3 private $user; 4 5 public function __construct($user) 6 { 7 $this->user = $user; 8 } 9 10 public function verifyCredentials() 11 { 12 // ... 13 } 14 } 15 16 class UserSettings 17 { 18 private $user; 19 private $auth; 20 21 public function __construct($user) 22 { 23 $this->user = $user; 24 $this->auth = new UserAuth($user); 25 } 26 27 public function changeSettings($settings) 28 { 29 if ($this->auth->verifyCredentials()) { 30 // ... 31 } 32 } 33 }
开闭原则 Open/Closed Principle (OCP)
正如Bertrand Meyer所述,"软件的实体(类, 模块, 函数,等)应该对扩展开放,对修改关闭。"这个原则是在说明应该允许用户在不改变已有代码的情况下增加新的功能。
Bad:
1 abstract class Adapter 2 { 3 protected $name; 4 5 public function getName() 6 { 7 return $this->name; 8 } 9 } 10 11 class AjaxAdapter extends Adapter 12 { 13 public function __construct() 14 { 15 parent::__construct(); 16 17 $this->name = 'ajaxAdapter'; 18 } 19 } 20 21 class NodeAdapter extends Adapter 22 { 23 public function __construct() 24 { 25 parent::__construct(); 26 27 $this->name = 'nodeAdapter'; 28 } 29 } 30 31 class HttpRequester 32 { 33 private $adapter; 34 35 public function __construct($adapter) 36 { 37 $this->adapter = $adapter; 38 } 39 40 public function fetch($url) 41 { 42 $adapterName = $this->adapter->getName(); 43 44 if ($adapterName === 'ajaxAdapter') { 45 return $this->makeAjaxCall($url); 46 } elseif ($adapterName === 'httpNodeAdapter') { 47 return $this->makeHttpCall($url); 48 } 49 } 50 51 private function makeAjaxCall($url) 52 { 53 // request and return promise 54 } 55 56 private function makeHttpCall($url) 57 { 58 // request and return promise 59 } 60 }
在上面的代码中,对于HttpRequester类中的fetch方法,如果我新增了一个新的xxxAdapter类并且要在fetch方法中用到的话,就需要在HttpRequester类中去修改类(如加上一个elseif 判断),而通过下面的代码,就可很好的解决这个问题。下面代码很好的说明了如何在不改变原有代码的情况下增加新功能。
Good:
1 interface Adapter 2 { 3 public function request($url); 4 } 5 6 class AjaxAdapter implements Adapter 7 { 8 public function request($url) 9 { 10 // request and return promise 11 } 12 } 13 14 class NodeAdapter implements Adapter 15 { 16 public function request($url) 17 { 18 // request and return promise 19 } 20 } 21 22 class HttpRequester 23 { 24 private $adapter; 25 26 public function __construct(Adapter $adapter) 27 { 28 $this->adapter = $adapter; 29 } 30 31 public function fetch($url) 32 { 33 return $this->adapter->request($url); 34 } 35 }
里氏替换原则 Liskov Substitution Principle (LSP)
对这个概念最好的解释是:如果你有一个父类和一个子类,在不改变原有结果正确性的前提下父类和子类可以互换。这个听起来让人有些迷惑,所以让我们来看一个经典的正方形-长方形的例子。从数学上讲,正方形是一种长方形,但是当你的模型通过继承使用了"is-a"的关系时,就不对了。
Bad:
1 class Rectangle 2 { 3 protected $width = 0; 4 protected $height = 0; 5 6 public function render($area) 7 { 8 // ... 9 } 10 11 public function setWidth($width) 12 { 13 $this->width = $width; 14 } 15 16 public function setHeight($height) 17 { 18 $this->height = $height; 19 } 20 21 public function getArea() 22 { 23 return $this->width * $this->height; 24 } 25 } 26 27 class Square extends Rectangle 28 { 29 public function setWidth($width) 30 { 31 $this->width = $this->height = $width; 32 } 33 34 public function setHeight(height) 35 { 36 $this->width = $this->height = $height; 37 } 38 } 39 40 function renderLargeRectangles($rectangles) 41 { 42 foreach ($rectangles as $rectangle) { 43 $rectangle->setWidth(4); 44 $rectangle->setHeight(5); 45 $area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20. 46 $rectangle->render($area); 47 } 48 } 49 50 $rectangles = [new Rectangle(), new Rectangle(), new Square()]; 51 renderLargeRectangles($rectangles);
Good:
1 abstract class Shape 2 { 3 protected $width = 0; 4 protected $height = 0; 5 6 abstract public function getArea(); 7 8 public function render($area) 9 { 10 // ... 11 } 12 } 13 14 class Rectangle extends Shape 15 { 16 public function setWidth($width) 17 { 18 $this->width = $width; 19 } 20 21 public function setHeight($height) 22 { 23 $this->height = $height; 24 } 25 26 public function getArea() 27 { 28 return $this->width * $this->height; 29 } 30 } 31 32 class Square extends Shape 33 { 34 private $length = 0; 35 36 public function setLength($length) 37 { 38 $this->length = $length; 39 } 40 41 public function getArea() 42 { 43 return pow($this->length, 2); 44 } 45 } 46 47 function renderLargeRectangles($rectangles) 48 { 49 foreach ($rectangles as $rectangle) { 50 if ($rectangle instanceof Square) { 51 $rectangle->setLength(5); 52 } elseif ($rectangle instanceof Rectangle) { 53 $rectangle->setWidth(4); 54 $rectangle->setHeight(5); 55 } 56 57 $area = $rectangle->getArea(); 58 $rectangle->render($area); 59 } 60 } 61 62 $shapes = [new Rectangle(), new Rectangle(), new Square()]; 63 renderLargeRectangles($shapes);
接口隔离原则
接口隔离原则:"客户端不应该被强制去实现于它不需要的接口"。
有一个清晰的例子来说明示范这条原则。当一个类需要一个大量的设置项,为了方便不会要求客户端去设置大量的选项,因为在通常他们不需要所有的设置项。使设置项可选有助于我们避免产生"胖接口"
Bad:
1 interface Employee 2 { 3 public function work(); 4 5 public function eat(); 6 } 7 8 class Human implements Employee 9 { 10 public function work() 11 { 12 // ....working 13 } 14 15 public function eat() 16 { 17 // ...... eating in lunch break 18 } 19 } 20 21 class Robot implements Employee 22 { 23 public function work() 24 { 25 //.... working much more 26 } 27 28 public function eat() 29 { 30 //.... robot can't eat, but it must implement this method 31 } 32 }
上面的代码中,Robot类并不需要eat()这个方法,但是实现了Emplyee接口,于是只能实现所有的方法了,这使得Robot实现了它并不需要的方法。所以在这里应该对Emplyee接口进行拆分,正确的代码如下:
Good:
1 interface Workable 2 { 3 public function work(); 4 } 5 6 interface Feedable 7 { 8 public function eat(); 9 } 10 11 interface Employee extends Feedable, Workable 12 { 13 } 14 15 class Human implements Employee 16 { 17 public function work() 18 { 19 // ....working 20 } 21 22 public function eat() 23 { 24 //.... eating in lunch break 25 } 26 } 27 28 // robot can only work 29 class Robot implements Workable 30 { 31 public function work() 32 { 33 // ....working 34 } 35 }
依赖反转原则 Dependency Inversion Principle (DIP)
这条原则说明两个基本的要点:
- 高阶的模块不应该依赖低阶的模块,它们都应该依赖于抽象
- 抽象不应该依赖于实现,实现应该依赖于抽象
问题由来:
类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:
将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
Bad:
1 class Employee 2 { 3 public function work() 4 { 5 // ....working 6 } 7 } 8 9 class Robot extends Employee 10 { 11 public function work() 12 { 13 //.... working much more 14 } 15 } 16 17 class Manager 18 { 19 private $employee; 20 21 public function __construct(Employee $employee) 22 { 23 $this->employee = $employee; 24 } 25 26 public function manage() 27 { 28 $this->employee->work(); 29 } 30 }
Good:
1 interface Employee 2 { 3 public function work(); 4 } 5 6 class Human implements Employee 7 { 8 public function work() 9 { 10 // ....working 11 } 12 } 13 14 class Robot implements Employee 15 { 16 public function work() 17 { 18 //.... working much more 19 } 20 } 21 22 class Manager 23 { 24 private $employee; 25 26 public function __construct(Employee $employee) 27 { 28 $this->employee = $employee; 29 } 30 31 public function manage() 32 { 33 $this->employee->work(); 34 } 35 }
别写重复代码 (DRY)
这条原则大家应该都是比较熟悉了。
尽你最大的努力去避免复制代码,它是一种非常糟糕的行为,复制代码通常意味着当你需要变更一些逻辑时,你需要修改不止一处。
Bad:
1 function showDeveloperList($developers) 2 { 3 foreach ($developers as $developer) { 4 $expectedSalary = $developer->calculateExpectedSalary(); 5 $experience = $developer->getExperience(); 6 $githubLink = $developer->getGithubLink(); 7 $data = [ 8 $expectedSalary, 9 $experience, 10 $githubLink 11 ]; 12 13 render($data); 14 } 15 } 16 17 function showManagerList($managers) 18 { 19 foreach ($managers as $manager) { 20 $expectedSalary = $manager->calculateExpectedSalary(); 21 $experience = $manager->getExperience(); 22 $githubLink = $manager->getGithubLink(); 23 $data = [ 24 $expectedSalary, 25 $experience, 26 $githubLink 27 ]; 28 29 render($data); 30 } 31 }
Good:
1 function showList($employees) 2 { 3 foreach ($employees as $employee) { 4 $expectedSalary = $employee->calculateExpectedSalary(); 5 $experience = $employee->getExperience(); 6 $githubLink = $employee->getGithubLink(); 7 $data = [ 8 $expectedSalary, 9 $experience, 10 $githubLink 11 ]; 12 13 render($data); 14 } 15 }
Very good:
function showList($employees) { foreach ($employees as $employee) { render([ $employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink() ]); } }
后记:虽然OOP设计需要遵守如上原则,不过实际的代码设计一定要简单、简单、简单。在实际编码中要根据情况进行取舍,一味遵守原则,而不注重实际情况的话,可能会让你的代码变的难以理解!