面向对象基本原则(3)- 最少知道原则与开闭原则

面向对象基本原则(3)- 最少知道原则与开闭原则

面向对象基本原则(1)- 单一职责原则与接口隔离原则
面向对象基本原则(2)- 里式代换原则与依赖倒置原则
面向对象基本原则(3)- 最少知道原则与开闭原则php


5、最少知道原则【迪米特法则】

1. 最少知道原则简介

最少知识原则(Least KnowledgePrinciple,LKP)也称为迪米特法则(Law of Demeter,LoD)。虽然名字不一样,但描述的是同一个规则:一个对象应该对其余对象有最少的了解。算法

通俗地讲,一个类应该对本身须要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我不要紧,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其余的我一律不关心。数据库

2. 最少知道原则实现

只与直接关联的类交流

每一个对象都必然会与其余对象有耦合关系,耦合关系的类型有不少,例如组合、聚合、依赖等。
出如今成员变量、方法的输入输出参数中的类称为直接关联的类,而出如今方法体内部的类不属于直接关联的类。编程

下面举例说明如何才能作到只与直接关联的类交流。segmentfault

场景:老师想让班长清点女生的数量设计模式

  • Bad
/**
 * 老师类
 * Class Teacher
 */
class Teacher {
    /**
     * 老师对班长发布命令,清点女生数量
     * @param GroupLeader $groupLeader
     */
    public function command(GroupLeader $groupLeader)
    {
        // 产生一个女生群体
        $girlList = new \ArrayIterator();
        // 初始化女生
        for($i = 0; $i < 20; $i++){
            $girlList->append(new Girl());
        }

        // 告诉班长开始执行清点任务
        $groupLeader->countGirls($girlList);
    }
}

/**
 * 班长类
 * Class GroupLeader
 */
class GroupLeader {

    /**
     * 清点女生数量
     * @param \ArrayIterator $girlList
     */
    public function countGirls($girlList)
    {
        echo "女生数量是:", $girlList->count(), "\n";
    }
}

/**
 * 女生类
 * Class Girl
 */
class Girl {

}

$teacher= new Teacher();
//老师发布命令
$teacher->command(new GroupLeader()); // 女生数量是:20

上面实例中,Teacher类仅有一个直接关联的类 -- GroupLeader。而Girl这个类就是出如今commond方法体内,所以不属于与Teacher类直接关联的类。
方法是类的一个行为,类居然不知道本身的行为与其余类产生依赖关系,这是不容许的,违反了迪米特法则。架构

对程序进行简单的修改,把 对 $girlList 的初始化移出 Teacher 类,同时在 GroupLeader 中增长对 Girl 的注入,避开 Teacher 类对陌生类 Girl 的访问,下降系统间的耦合,提升系统的健壮性。
下面是改进后的代码:app

  • Good
/**
 * 老师类
 * Class Teacher
 */
class Teacher {

    /**
     * 老师对班长发布命令,清点女生数量
     * @param GroupLeader $groupLeader
     */
    public function command(GroupLeader $groupLeader)
    {
        // 告诉班长开始执行清点任务
        $groupLeader->countGirls();
    }
}

/**
 * 班长类
 * Class GroupLeader
 */
class GroupLeader {

    private $_girlList;

    /**
     * 传递全班的女生进来
     * GroupLeader constructor.
     * @param Girl[]|\ArrayIterator $girlList
     */
    public function __construct(\ArrayIterator $girlList)
    {
        $this->_girlList = $girlList;
    }

    //清查女生数量
    public function countGirls()
    {
        echo "女生数量是:", $this->_girlList->count(), "\n";
    }
}

/**
 * 女生类
 * Class Girl
 */
class Girl {

}

// 产生一个女生群体
$girlList = new \ArrayIterator();
// 初始化女生
for($i = 0; $i < 20; $i++){
    $girlList->append(new Girl());
}

$teacher= new Teacher();
//老师发布命令
$teacher->command(new GroupLeader($girlList)); // 女生数量是:20

关联的类之间也要有距离

迪米特法则要求类“羞涩”一点,尽可能不要对外公布太多的public方法和非静态的public变量,尽可能内敛,多使用private、protected等访问权限。编程语言

一个类公开的public属性或方法越多,修改时涉及的面也就越大,变动引发的风险扩散也就越大。所以,为了保持类间的距离,在设计时须要反复衡量:是否还能够再减小public方法和属性,是否能够修改成private、protected等访问权限,是否能够加上final关键字等。函数

实例场景:实现软件安装的过程,其中first方法定义第一步作什么,second方法定义第二步作什么,third方法定义第三步作什么。

  • Bad
/**
 * 导向类
 * Class Wizard
 */
class Wizard {

    /**
     * 第一步
     * @return int
     */
    public function first()
    {
        echo "执行第一步安装...\n";
        // 模拟用户点是或取消
        return rand(0, 1);
    }

    /**
     * 第二步
     * @return int
     */
    public function second()
    {
        echo "执行第二步安装...\n";
        // 模拟用户点是或取消
        return rand(0, 1);
    }

    /**
     * 第三步
     * @return int
     */
    public function third()
    {
        echo "执行第三步安装...\n";
        // 模拟用户点是或取消
        return rand(0, 1);
    }
}

/**
 * 安装软件类
 * Class InstallSoftware
 */
class InstallSoftware {

    /**
     * 执行安装软件操做
     * @param Wizard $wizard
     */
    public function installWizard(Wizard $wizard)
    {
        $first = $wizard->first();
        //根据first返回的结果,看是否须要执行second
        if($first === 1){
            $second = $wizard->second();
            if($second === 1){
                $third = $wizard->third();
                if($third === 1){
                    echo "软件安装完成!\n";
                }
            }
        }
    }
}

// 实例化软件安装类
$invoker = new InstallSoftware();
// 开始安装软件
$invoker->installWizard(new Wizard()); // 运行结果和随机数有关,每次的执行结果都不相同

Wizard类把太多的方法暴露给InstallSoftware类,二者的朋友关系太亲密了,耦合关系变得异常牢固。若是要将Wizard类中的first方法返回值的类型由int改成boolean,就须要修改InstallSoftware类,从而把修改变动的风险扩散开了。所以,这样的耦合是极度不合适的。

改进:在Wizard类中增长一个installWizard方法,对安装过程进行封装,同时把原有的三个public方法修改成private方法。

/**
 * 导向类
 * Class Wizard
 */
class Wizard {

    //第一步
    private function first()
    {
        echo "执行第1个方法...\n";
        // 模拟用户点是或取消
        return rand(0, 1);
    }
    //第二步
    private function second()
    {
        echo "执行第2个方法...\n";
        // 模拟用户点是或取消
        return rand(0, 1);
    }
    //第三个方法
    private function third()
    {
        echo "执行第3个方法...\n";
        // 模拟用户点是或取消
        return rand(0, 1);
    }

    public function installWizard(){
        $first = $this->first();
        //根据first返回的结果,看是否须要执行second
        if($first === 1){
            $second = $this->second();
            if($second === 1){
                $third = $this->third();
                if($third === 1){
                    echo "软件安装完成!\n";
                }
            }
        }
    }
}

/**
 * 安装软件类
 * Class InstallSoftware
 */
class InstallSoftware {

    /**
     * 执行安装软件操做
     * @param Wizard $wizard
     */
    public function installWizard(Wizard $wizard)
    {
        $wizard->installWizard();
    }
}

// 实例化软件安装类
$invoker = new InstallSoftware();
// 开始安装软件
$invoker->installWizard(new Wizard()); // 运行结果和随机数有关,每次的执行结果都不相同

代码改进后,类间的耦合关系变弱了,结构也清晰了,变动引发的风险也变小了。

3. 最佳实践

在实际应用中常常会出现这样一个方法:放在本类中也能够,放在其余类中也没有错,那怎么去衡量呢?
你能够坚持这样一个原则:若是一个方法放在本类中,既不增长类间关系,也对本类不产生负面影响,那就放置在本类中。

在实际应用中,若是一个类跳转两次以上才能访问到另外一个类,就须要想办法进行重构了。
由于一个系统的成功不只仅是一个标准或是原则就可以决定的,有很是多的外在因素决定,跳转次数越多,系统越复杂,维护就越困难,因此只要跳转不超过两次都是能够忍受的,这须要具体问题具体分析。

迪米特法则要求类间解耦,但解耦是有限度的,除非是计算机的最小单元——二进制的0和1。那才是彻底解耦,在实际的项目中,须要适度地考虑这个原则,别为了套用原则而作项目。
原则只是供参考,若是违背了这个原则,项目也未必会失败,这就须要你们在采用原则时反复度量,不遵循是不对的,严格执行就是“过犹不及”。

6、开闭原则

1. 开闭原则简介

开闭原则的英文名称是 Open-Close Principle,简称OCP。
开闭原则是面向对象设计中最基础的设计原则,它指导咱们如何创建一个稳定、灵活的软件系统。
开闭原则的英文定义是

Software entities like classes,modules and functions should be open for extension but closed for modifications.

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 其含义是说一个软件实体应该经过扩展来实现变化,而不是经过修改已有的代码来实现变化。

软件实体包括如下几个部分:

  • 项目或软件产品中按照必定的逻辑规则划分的模块。
  • 抽象和类。
  • 方法。

一个软件产品只要在生命期内,都会发生变化,既然变化是一个既定的事实,咱们就应该在设计时尽可能适应这些变化,以提升项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉咱们应尽可能经过扩展软件实体的行为来实现变化,而不是经过修改已有的代码来完成变化,它是为软件实体的将来事件而制定的对现行开发设计进行约束的一个原则。

2. 开闭原则的优势

提升复用率

在面向对象的设计中,全部的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才能够复用,粒度越小,被复用的可能性就越大。
复用能够减小代码量,避免相同的逻辑分散在多个角落,避免往后的维护人员为了修改一个微小的缺陷或增长新功能而要在整个项目中处处查找相关的代码。
那怎么才能提升复用率呢?缩小逻辑粒度,直到一个逻辑不可再拆分为止。

提升可维护性

一款软件投产后,维护人员的工做不只仅是对数据进行维护,还可能要对程序进行扩展,维护人员最乐意作的事情就是扩展一个类,而不是修改一个类,甭管原有的代码写得多么优秀仍是多么糟糕,让维护人员读懂原有的代码,而后再修改,是一件很痛苦的事情,不要让他在原有的代码海洋里游弋完毕后再修改,那是对维护人员的一种折磨和摧残。

面向对象开发的要求

万物皆对象,咱们须要把全部的事物都抽象成对象,而后针对对象进行操做,可是万物皆运动,有运动就有变化,有变化就要有策略去应对。怎么快速应对呢?这就须要在设计之初考虑到全部可能变化的因素,而后留下接口,等待“可能”转变为“现实”。

2. 变化的三种类型

逻辑变化

只变化一个逻辑,而不涉及其余模块,好比原有的一个算法是 a*b+c,如今须要修改成 a*b*c,能够经过修改原有类中的方法的方式来完成,前提条件是全部依赖或关联类都按照相同的逻辑处理。

子模块变化

一个模块变化,会对其余的模块产生影响,特别是一个低层次的模块变化必然引发高层模块的变化,所以在经过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是相似的处理模块,该部分的变化甚至会引发界面的变化。

视图变化

可见视图是提供给客户使用的界面,该部分的变化通常会引发连锁反应。若是仅仅是界面上按钮、文字的从新排布却是简单,最司空见惯的是业务耦合变化,例如一个展现数据的列表,按照原有的需求是6列,忽然有一天要增长1列,并且这一列要跨N张表,处理M个逻辑才能展示出来,这样的变化是比较恐怖的,但仍是能够经过扩展来完成变化,这就要看咱们原有的设计是否灵活。

3. 开闭原则的使用

抽象约束

抽象是对一组事物的通用描述,没有具体的实现,也就表示它能够有很是多的可能性,能够跟随需求的变化而变化。所以,经过接口或抽象类能够约束一组可能变化的行为,而且可以实现对扩展开放,其包含三层含义:
第一,经过接口或抽象类约束扩展,对扩展进行边界限定,不容许出如今接口或抽象类中不存在的public方法;
第二,参数类型、引用对象尽可能使用接口或者抽象类,而不是实现类;
第三,抽象层尽可能保持稳定,一旦肯定即不容许修改。

封装变化

对变化的封装包含两层含义:
第一,将相同的变化封装到一个接口或抽象类中;
第二,将不一样的变化封装到不一样的接口或抽象类中,不该该有两个不一样的变化出如今同一个接口或抽象类中。
封装变化,也就是受保护的变化(protected variations),找出预计有变化或不稳定的点,咱们为这些变化点建立稳定的接口,准确地讲是封装可能发生的变化,一旦预测到或“第六感”发觉有变化,就能够进行封装。

4. Show me the code

书店售书场景

代码使用PHP7.2语法编写
  • 书籍接口
/**
 * Interface IBook
 * 书籍接口
 */
interface IBook {
    /**
     * 书籍名称
     * @return mixed
     */
    public function getName() : string ;

    /**
     * 书籍价格
     * 这里把价格定义为int类型并非错误,
     * 在非金融类项目中对货币处理时,通常取2位精度,
     * 一般的设计方法是在运算过程当中扩大100倍,在须要展现时再缩小100倍,减小精度带来的偏差。
     * @return mixed
     */
    public function getPrice() : int ;

    /**
     * 书籍做者
     * @return mixed
     */
    public function getAuthor() : string ;
}
  • 小说类
/**
 * 小说类
 * Class NovelBook
 */
class NovelBook implements IBook {

    /**
     * 书籍名称
     * @var string $_name
     */
    private $_name;

    /**
     * 书籍价格
     * @var int $_price
     */
    private $_price;

    /**
     * 书籍做者
     * @var string $_author
     */
    private $_author;

    /**
     * 经过构造函数传递书籍信息
     * @param string $name
     * @param int $price
     * @param string $author
     */
    public function __construct(string $name, int $price, string $author)
    {
        $this->_name = $name;
        $this->_price = $price;
        $this->_author = $author;
    }

    /**
     * 获取书籍名称
     * @return string
     */
    public function getName() : string
    {
        return $this->_name;
    }

    /**
     * 获取书籍价格
     * @return int
     */
    public function getPrice() : int
    {
        return $this->_price;
    }

    /**
     * 获取书籍做者
     * @return string
     */
    public function getAuthor() : string
    {
        return $this->_author;
    }

}
  • 售书场景
// 产生一个书籍列表
$bookList = new ArrayIterator();

// 始化数据
$bookList->append(new NovelBook("天龙八部",3200,"金庸"));
$bookList->append(new NovelBook("巴黎圣母院",5600,"雨果"));

echo "------书店卖出去的书籍记录以下:--------\n";
foreach($bookList as $book){
    $price = $book->getPrice() / 100;
    echo <<<TXT
书籍名称: {$book->getName()}
书籍做者: {$book->getAuthor()}
书籍价格: {$price} 元
---\n
TXT;
}
------书店卖出去的书籍记录以下:--------
书籍名称: 天龙八部
书籍做者: 金庸
书籍价格: 32 元
---
书籍名称: 巴黎圣母院
书籍做者: 雨果
书籍价格: 56 元
---

一段时间以后,书店决定对小说类书籍进行打折促销:全部40元以上的书籍9折销售,其余的8折销售。面对需求的变化,咱们有两种解决方案。

  1. 修改实现类NovelBook

直接修改NovelBook类中的getPrice()方法实现打折处理。该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个很是优秀的方法,可是该方法仍是有缺陷的。例如采购书籍人员也是要看价格的,因为该方法已经实现了打折处理价格,所以采购人员看到的也是打折后的价格,会因信息不对称而出现决策失误的状况。

  1. 经过扩展实现变化

增长一个子类OffNovelBook,覆写getPrice方法,高层次的模块经过OffNovelBook类产生新的对象,完成业务变化对系统的最小化开发,修改少,风险也小。

  • 打折销售的小说类
/**
 * 打折销售的小说类
 * Class OffNovelBook
 */
class OffNovelBook extends NovelBook {
    /**
     * 覆写获取销售价格方法
     *
     * @return int
     */
    public function getPrice() : int
    {
        //原价
        $originPrice = parent::getPrice();

        if($originPrice > 40){ //原价大于40元,则打9折
            $discountPrice = $originPrice * 90 / 100;
        }else{
            $discountPrice = $originPrice * 80 / 100;
        }
        return $discountPrice;
    }
}
  • 打折售书场景
// 产生一个书籍列表
$bookList = new ArrayIterator();

// 始化数据,实际项目中通常是由持久层完成
$bookList->append(new OffNovelBook("天龙八部",3200,"金庸"));
$bookList->append(new OffNovelBook("巴黎圣母院",5600,"雨果"));

echo "------书店卖出去的书籍记录以下:------\n";
foreach($bookList as $book){
    $price = $book->getPrice() / 100;
    echo <<<TXT
书籍名称: {$book->getName()}
书籍做者: {$book->getAuthor()}
书籍价格: {$price} 元
---\n
TXT;
}
------书店卖出去的书籍记录以下:------
书籍名称: 天龙八部
书籍做者: 金庸
书籍价格: 28.8 元
---
书籍名称: 巴黎圣母院
书籍做者: 雨果
书籍价格: 50.4 元
---

又过了一段时间,书店新增长了计算机书籍,它不只包含书籍名称、做者、价格等信息,还有一个独特的属性:面向的是什么领域,也就是它的范围,好比是和编程语言相关的,仍是和数据库相关的,等等。

  • 增长一个IComputerBook接口,它继承自IBook
/**
 * 计算机类书籍接口
 * Interface IComputerBook
 */
interface IComputerBook extends IBook
{
    /**
     * 计算机书籍增长一个范围属性
     * @return string
     */
    public function getScope() : string ;
}
  • 计算机书籍类
/**
 * 计算机书籍类
 * Class ComputerBook
 */
class ComputerBook implements IComputerBook
{
    /**
     * 书籍名称
     * @var string $_name
     */
    private $_name;

    /**
     * 书籍价格
     * @var int $_price
     */
    private $_price;

    /**
     * 书籍做者
     * @var string $_author
     */
    private $_author;

    /**
     * 书籍范围
     * @var string $_scope
     */
    private $_scope;

    /**
     * 经过构造函数传递书籍信息
     * ComputerBook constructor.
     * @param string $name
     * @param int $price
     * @param string $author
     * @param string $scope
     */
    public function __construct(string $name, int $price, string $author, string $scope)
    {
        $this->_name = $name;
        $this->_price = $price;
        $this->_author = $author;
        $this->_scope = $scope;
    }

    /**
     * 获取书籍名称
     * @return string
     */
    public function getName() : string
    {
        return $this->_name;
    }

    /**
     * 获取书籍价格
     * @return int
     */
    public function getPrice() : int
    {
        return $this->_price;
    }

    /**
     * 获取书籍做者
     * @return string
     */
    public function getAuthor() : string
    {
        return $this->_author;
    }

    /**
     * 获取书籍范围
     * @return string
     */
    public function getScope() : string
    {
        return $this->_scope;
    }
}
  • 增长计算机书籍销售
//产生一个书籍列表
$bookList = new ArrayIterator();

// 始化数据,实际项目中通常是由持久层完成
$bookList->append(new OffNovelBook("天龙八部",3200,"金庸"));
$bookList->append(new OffNovelBook("巴黎圣母院",5600,"雨果"));
$bookList->append(new ComputerBook("高性能MySQL",4800,"Baron", '数据库'));


echo "------书店卖出去的书籍记录以下:------\n";
foreach($bookList as $book) {
    $price = $book->getPrice() / 100;
    echo <<<TXT
书籍名称: {$book->getName()}
书籍做者: {$book->getAuthor()}
书籍价格: {$price} 元
---\n
TXT;
}
------书店卖出去的书籍记录以下:------
书籍名称: 天龙八部
书籍做者: 金庸
书籍价格: 28.8 元
---
书籍名称: 巴黎圣母院
书籍做者: 雨果
书籍价格: 50.4 元
---
书籍名称: 高性能MySQL
书籍做者: Baron
书籍价格: 48 元
---

开闭原则对扩展开放,对修改关闭,并不意味着不作任何修改,低层模块的变动,必然要有高层模块进行耦合,不然就是一个孤立无心义的代码片断。

参考文献:《设计模式之禅》
相关文章
相关标签/搜索