这些年,月小升同学发现自己不会读书于是买了一本《如何阅读一本书》,发现自己不会做笔记就买了一本《如何做笔记》,写代码久了,发现自己一直在用的面向对象不是很了解,经常把代码写成一坨一坨的,于是回头来学习怎么面向对象。那些不熟练的基础,总要还债的。
出来混总是要还的
SOLID 是Michael Feathers推荐的便于记忆的首字母简写,它代表了Robert Martin命名的最重要的五个面对对象编码设计原则
S: 单一职责原则 (SRP) Single Responsibility Principle
O: 开闭原则 (OCP) Open/Closed Principle
L: 里氏替换原则 (LSP) Liskov Substitution Principle
I: 接口隔离原则 (ISP) Interface Segregation Principle
D: 依赖反转原则 (DIP) Dependency Inversion Principle
1. 单一责任原则:
当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。
举例子:不要让一个类,负责发邮件,还负责修改客户名称
不良代码:
class Email{ public function sendemail(){ //... } public function changeUsername(){ //... } }
改进版:
class Email{ public function sendemail(){ //... } } class User{ public function changeUsername(){ //... } }
2. 开放封闭原则 Open/Closed Principle (OCP)
软件实体应该是可扩展,而不可修改的。也就是说,对”扩展是开放的,而对修改是封闭的”。这个原则是在说明应该允许用户在不改变已有代码的情况下增加新的功能。
实现开闭原则的关键就在于“抽象”。把系统的所有可能的行为抽象成一个抽象底层,这个抽象底层规定出所有的具体实现必须提供的方法的特征。作为系统设计的抽象层,要预见所有可能的扩展,从而使得在任何扩展情况下,系统的抽象底层不需修改;同时,由于可以从抽象底层导出一个或多个新的具体实现,可以改变系统的行为,因此系统设计对扩展是开放的。
看例子
<?php abstract class vehicle { protected $name; public function __construct(){ } public function getName() { return $this->name; } } class car extends vehicle { public function __construct() { parent::__construct(); $this->name = 'car'; } } class bike extends vehicle { public function __construct() { parent::__construct(); $this->name = 'bike'; } } class task { private $vehicle; public function __construct($vehicle) { $this->vehicle = $vehicle; } public function dotask($kilometer) { $vName = $this->vehicle->getName(); if ($vName === 'car') { return $this->CarRun($kilometer); } elseif ($vName === 'bike') { return $this->BikeRun($kilometer); } } private function CarRun($kilometer) { echo '小汽车跑了'.$kilometer.'公里<hr>'; } private function BikeRun($kilometer) { echo '自行车跑了'.$kilometer.'公里<hr>'; } } echo '<meta charset="utf-8">'; $car = new car; $bike = new bike; $task = new task($car); $task->dotask(10); $task = new task($bike); $task->dotask(10); ?>
这个例子,如果增加一个车,bus,那么就要改动task任务这个类的底层代码
改良版本
<?php interface vehicle { public function run($url); } class car implements vehicle { public function run($kilometer) { echo '小汽车跑了'.$kilometer.'公里<hr>'; } } class bike implements vehicle { public function run($kilometer) { echo '自行车跑了'.$kilometer.'公里<hr>'; } } class task { private $vehicle; public function __construct($vehicle) { $this->vehicle = $vehicle; } public function dotask($kilometer) { $this->vehicle->run($kilometer); } } echo '<meta charset="utf-8">'; $car = new car; $bike = new bike; $task = new task($car); $task->dotask(10); $task = new task($bike); $task->dotask(10); ?>
改良后的车辆,任意增加新车型,都不会改动task的内容
3. 里氏替换原则
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系
可以理解为:只要有父类出现的地方,都可以使用子类来替代。而且不会出现任何错误或者异常。但是反过来却不行。子类出现的地方,不能使用父类来替代。
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
理解为“只要有父类出现的地方,都可以使用子类来替代”
定义2:所有引用基类的地方必须能透明地使用其子类的对象。
问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
不良的设计:违背了里氏替换原则
假设一个父亲厨师会个炒鸡蛋的手艺,辣椒炒鸡蛋,儿子应该会这个手艺,但是儿子不会用辣椒,只会用大葱,所以炒出来的鸡蛋不一样了。
<?php class father{ public function cookeEgg(){ echo '辣椒炒鸡蛋<br/>'; } } class son1 extends father{ public function cookeEgg(){ echo '大葱炒鸡蛋<br/>'; } } echo '<meta charset="utf-8">'; $f = new father; $f->cookeEgg(); $s1 = new son1; $s1->cookeEgg(); ?>
辣椒炒鸡蛋 大葱炒鸡蛋
违背了原则1:只要有父类出现的地方,都可以使用子类来替代。
现在父亲会辣椒炒鸡蛋,换成儿子来炒,结果儿子因为不敢碰辣椒,炒成了大葱烧鸡蛋。 这个儿子就不是好儿子。我们假设这个是大儿子。
改良版本:出来一个好的二儿子的样子
<?php class father{ public function cookeEgg(){ echo '辣椒炒鸡蛋<br/>'; } } class son2 extends father{ public function cookeEggWithScallion(){ echo '大葱炒鸡蛋<br/>'; } } echo '<meta charset="utf-8">'; $f = new father; $f->cookeEgg(); $s2 = new son2; $s2->cookeEgg(); $s2->cookeEggWithScallion(); ?>
辣椒炒鸡蛋 辣椒炒鸡蛋 大葱炒鸡蛋
这个儿子,复合了定义1,父亲出来的地方,儿子出来就能提到,父亲会辣椒炒鸡蛋,儿子也会,所以儿子自动继承,但是二儿子还会大葱炒鸡蛋,二儿子就会两个炒蛋了
按解决方案:不要重写父亲的方法
规则1: 不要重写父亲的方案
规则2: 所有孩子都会有父亲的技能(父亲能辣椒炒鸡蛋,儿子就会,父亲出现的地方,儿子就可以替代)
规则3: 孩子会额外的技能,自己单独再写函数。(子类出现的地方,父亲不一定能替代)
4. 依赖反转原则
1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
2. 抽象不应该依赖于细节,细节应该依赖于抽象
实际理解为:高级逻辑层代码,不要因为底层的模块代码改变而变动。
高层模块:业务逻辑层,比如群发邮件,我决定发给购买者和没有购买者,那么群发邮件这个send的工作就是业务逻辑层的高层模块,而决定哪些用户时购买者的底层数据库查询操作属于底层模块。
举例子:老张开宝马,这个动作,开车是业务逻辑层,宝马车跑动是底层。
不良设计
<?php class Bwm{ public function run(){ echo "开动宝马汽车"; } } class Audi{ public function run(){ echo "开动奥迪汽车"; } } //高层模块Driver 依赖了底层模块Bwm , 出来模块C Audi,我就只好改Driver了。 class Driver{ public function drive(Bwm $car){ $car->run(); } } echo '<meta charset="utf-8">'; $bwm = new Bwm; $zhang = new Driver(); $zhang->drive($bwm); $audi = new Audi; $zhang->drive($audi); //奥迪我没法开了。 此处代码会报错。 ?>
有个办法就是业务层,再写一个函数funciton driveAudi() 是不是很熟悉,我们因为要负责处理额外的情况,又写了个看起来很重复函数。
改良的版本,把车做成接口
宝马和奥迪都是来实现车的底层逻辑函数。这样再新车进入的时候,就不用改动逻辑层的代码了。
<?php interface Car{ public function run(); } class Bwm implements Car{ public function run(){ echo "开动宝马汽车"; } } class Audi implements Car{ public function run(){ echo "开动奥迪汽车"; } } class Driver{ public function drive(Car $car){ $car->run(); } } echo '<meta charset="utf-8">'; $bwm = new Bwm; $zhang = new Driver(); $zhang->drive($bwm); $audi = new Audi; $zhang->drive($audi); ?>
现在老张可以开宝马也可以开奥迪,你拿个大众,我也照样开。
再次理解这句话:高级逻辑层代码,不要因为底层的模块代码改变而变动。
5. 接口分离原则
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。
interface Employee { public function work(); public function eat(); } class Human implements Employee { public function work() { // ....working } public function eat() { // ...... eating in lunch break } }
机器人雇员不能吃,但是被强迫必须实现吃的接口
class Robot implements Employee { public function work() { //.... working much more } public function eat() { //.... robot can't eat, but it must implement this method } }
改良版本
interface Workable { public function work(); } interface Feedable { public function eat(); } interface Employee extends Feedable, Workable { } class Human implements Employee { public function work() { // ....working } public function eat() { //.... eating in lunch break } } // robot can only work class Robot implements Workable { public function work() { // ....working } }
对面向对象的领悟,有助于在大型代码量的工程里,实现有效分离函数,互不干扰,团队协作。