美文网首页php开发
面向对象的5个设计原则

面向对象的5个设计原则

作者: 单板小智 | 来源:发表于2017-02-12 23:51 被阅读200次

    在面向对象中,类是基本单位,各种设计都是围绕着类来进行的。可以说,类与类之间的关系,构成了设计模式的大部分内容。

    经典的设计模式有23种,学习设计模式推荐GOF经典以及《敏捷软件开发——原则、方法与实践》

    1.单一职责原则

    背景故事:亚当.斯密 对制针业做过一个分工生产效率的例子(《国富论》第一章)。每个工人专学一种专门的业务,相比每个人独立完成整个工作流程,达到数千倍的效率。在面向对象设计中,分工理论就是单一职责原则(Single Pesponsibility Prineiple, SRP)。 就一个类而言,应该只有一个引起它变化的原因。此原则容易理解却不容易做到,主要是怎样设计类与类的方法界定问题。

    单一职责的两个含义:

    1. 避免相同的职责分散到不同的类中
    2. 避免一个类承担太多职责

    为什么要遵循SRP:

    1. 可以减少类之间的耦合:当需求变化时,只修改一个类,从而隔离了变化。
    2. 提高类的复用性

    单一职责使得组件可以方便的拆卸和组装

    SPR实际开发中的应用:用工厂模式来实现不同数据库操作类。

    工厂模式(Factory) 允许你在代码执行时实例化对象。之所以被称为工厂模式,是因为它负责“生产”对象。

    下面案例是使用工厂模式实现一个数据库操作类,主要是讲解工厂模式。

    Db_Adapter.php

    //先定义一个适配器接口
    interface Db_Adapter
    {
        /**
         * 数据库连接
         * @param $config 数据库配置
         * @return resource
         */
        public function connection($config);
        
        /**
         * 执行数据库查询
         * @param string $query 数据库查询SQL字符串
         * @param mixed $handle 连接对象
         * @return resource
         */
        public function query($query, $handle);
    }
    

    Db_Adapter_Mysql.php

    // 定义mysql 操作类
    require_once 'Db_Adapter.php';
    
    class Db_Adapter_Mysql implements Db_Apdater
    {
        private $_dbLink;       //数据库连接标识
        
        /**
         * 数据库连接函数
         * 
         * @param $config 数据库配置
         * @throws Db_Exception
         * @return resource
         */
        public function connection($config)
        {
            if ($this->_dbLink = @mysql_connect($config->host . (empty($config->port) ? '' : ':' . $config->port), $config->user, $config->password, true)) {
                if (@mysql_select_db($config->database, $this->_dbLink)) {
                    if ($config->charset) {
                        mysql_query("SET NAMES '{$config->charset}'", $this->_dbLink);
                    }
                    
                    return $this->_dbLink;
                }
            }
            
            /** 数据库异常 */
            throw new Db_Exception(@mysql_error($this->_dbLink));
        }
        
        /**
         * 执行数据库查询
         * 
         * @param string $query 数据库查询SQL 字符串
         * @param mixed $handle 连接对象
         * @return resource
        public function query($query, $handle) 
        {
            if ($resource = @mysql_query($query, $handle) {
                return $resource;
            }
        }
    }
    

    Db_Adapter_sqlite.php

    // 定义sqlite 操作类
    require_once 'Db_Adapter.php';
    
    class Db_Adapter_sqlite implements Db_Adapter
    {
        private $_dbLink;       // 数据库连接标识
        
        public function connection($config)
        {
            if ($this->_dbLink = sqlite_open($config->file, 0666, $error)) {
                return $this->_dbLink;
            }
            
            /** 数据库异常 */
            throw new Db_Exception($error);
        }
        
        /**
         * 执行数据库查询
         * 
         * @param string $query 数据库查询 SQL 字符串
         * @param mixed $handle 连接对象
         * @return resource
         */
        public function query($query, $handle)
        {
            if ($resource = @sqlite_query($query, $handle)) {
                return $resource;
            }
        }
    }
    

    sqlFactory.php

    // 定义工厂类
    class sqlFactory
    {
        public static function factory($type)
        {
            if (incluede_once '/Db_Adapter_' . $type . '.php') {
                $classname = 'Db_Adapter_' . $type;
                return new $classname;
            } else {
                throw new Exception ('Driver not found');
            }
        }
    }
    

    test.php

    // 调空上面的库
    require 'sqlFactory.php';
    
    $dn = sqlFactory::factory('Mysql');
    $db = sqlFactory::factory('sqlite');
    
    print_r($dn);
    print_r($db);
    

    在上面的例子中,sqlFactory类是一个工厂类,用户可以通过调用sqlFactory类中的factory方法和它的参数来动态的初始化不同类型的数据库的操作类,Db_Adapter.php定义了数据库操作类的规范,这样让所有数据库的操作完全一样,如果更换了数据库,只需要用工厂类生成不同的对象就可以实现不同数据库的操作,而不需要去修改数据库操作的业务代码。

    SRP是最简单的原则之一,也是最难做好的原则之一。

    一些应该遵循的做法:

    1. 根据业务流程,把业务对象提炼出来。 如果业务流程链路太复杂,就把这个业务对象分离为多个单一业务对象。当业务链标准化后,对业务对象的内部情况做进一部处理。把第一次标准化视为最高层抽象,第二次视为次高层抽象,以此类推,直到“恰如其分”的设计层次。
    2. 职责的分类需要注意。 有业务职责,还要脱离业务的抽象职责,从认识业务到抽象算法是一个层层递进的过程。就好比命令模式中的顾客,服务员和厨师的职责,作为老板的你需要规划好各自的职责范围,既要防止越俎代庖,也要防止互相推诿。

    2.接口隔离原则

    设计应用程序的时候,如果一个模块包含多个子模块,那么我们应该小心对该模块做出抽象。

    接口隔离原则(Interface Segregation Principle, ISP): 客户端不应该被强迫实现不会使用的接口,应该把胖接口中的方法分组,然后用多个接口代替它,每个接口服务于一个子模块。简单的说,就是使用多个专门的接口比使用单个接口要好得多。

    ISP主要观点:

    1. 一个类对另外一个类的依赖性应当是建立在最小的接口上。
    • ISP 可以达到不强迫客户依赖于他们不使用的方法,接口的实现类应该只呈现为单一职责的角色(遵守SRP原则)。
    • ISP 还可降低客户端之间的相互影响——当某个客户程序要求提供新的职责(需求变更)而迫使接口发生改变时,影响到其他客户程序的可能性会是最小的。
    1. 客户端程序不应该依赖它不需要的接口方法。

    ISP强调的是接口对客户端的承诺越少越好,并且要做到专一。

    接口污染

    过于臃肿的接口设计是对接口的污染。接口污染就是为接口添加不必要的职责,如果开发人员在接口中增加一个新功能的主要目的只是减少接口实现类的数目,则此设计导致接口被不断的“污染” 并 “变胖”。

    上面的结构就是一个标准的胖接口,客户A需要服务A、客户B需要服务B、客户C需要服务C,当客户A的需求变化后,也会影响到客户B和C。

    使用接口隔离后如下:


    这里写图片描述

    使用接口隔离原则后,虽然接口数量变多,但是当客户A的需求变更后,就不会再影响到客户B和C。

    考虑下如何实现禽类动物的接口?他们都有翅膀,都是卵生动物,都有羽毛,但是鸟会飞,鸭子不会飞。

    可以把它们共性设计为一个基础类,针对部分特有的属性,可以另外定义一个接口,让具有这些特殊属性的同时实现基础接口和特殊接口。

    对于接口污染,可以考虑以下两种处理方式:

    • 利用委托分离接口
    • 利用多继承分离接口

    3.开放 - 封闭原则

    • Open(Open for extension) 模块的行为必须是开放的、支持扩展的,而不是僵化的。
    • Closed (Closed for modification) 在模块的功能进行扩展时,不应该影响或大规模影响已有的程序模块。

    一个模块在扩展性方面应该是开放的,在更改性方面应该是封闭的。

    在生活中,开放 - 封闭原则最简单的例子就是电脑,你可以很容易对电脑功能进行扩展,并且不对现有功能或者只是少量影响现有功能。

    该原则的核心是想是对抽象编程,而不是具体编程,因为抽象相对稳定。让类依赖于固定的抽象,这样的修改就是封闭的,通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。

    • 在设计方面充分应用 抽象 和 封装 的思想。
    • 在系统功能编程实现方面应用面向接口的编程。

    4.替换原则

    87年 Liskov女士在OOPSLA大会上发表《Data Abstraction and Hierarchy》中提出,主要阐述有关继承的一些原则,也称为里氏替换原则。02年Robert C.Martin出版的《Agile Software Development Principles Pattems and Practices》中对里氏替换原则进行简化为子类必须能够替换成它们的基类

    里氏替换原则(Liskov Substiution Principle, LSP)定义以及主要思想:子类型必须能够替换掉它们的父类型、并出现在父类能够出现的任何地方。

    LSP解决如果正确进行继承设计和合理地应用继承机制:

    • 如何正确地进行继承方面的设计?
    • 最佳的继承层次如何获得?
    • 怎样避免所设计的类层次陷入不符合OCP原则的状况?

    如何遵守替换原则:

    • 父类的方法都要在子类中实现或者重写, 派生类只实现其抽象类中声明的方法, 而不应当给出多余的方法定义或实现。
    • 在客户端程序中只应该出现父类对象,而不是直接使用子类对象, 这样可以实现运行期绑定(多态绑定)。

    php对LSP的支持不如其它语言, 因为其没有向上转型等概念, 只能通过一些其它手段来实现。

    5.依赖倒置原则

    将依赖关系倒置为依赖接口:

    • 上层模块不应该依赖下层模块, 它们共同依赖于一个抽象。
    • 抽象不能依赖于具体, 具体应该要依赖于抽象

    employee.php

    <?php
    interface employee
    {
        public function working();
    }
    
    class teacher implements employee
    {
        public function working()
        {
            echo 'teaching...';
        }
    }
    
    class cooder implements employee
    {
        public function working()
        {
            echo 'coding...';
        }
    }
    
    class workA
    {
        public function work()
        {
            $teacher = new teacher();
            $teacher->working();
        }
    }
    
    class workB
    {
        private $e;
        
        public function set(employee $e)
        {
            $this->e = $e;
        }
    
        public function work()
        {
            $this->e->working();
        }
    }
    
    $worka = new workA();
    $worka->work();
    
    $workb = new workB();
    $workb->set(new teacher());
    $workb->work();
    

    classA中,work方法依赖teacher实现; classB中,通过工厂模式实现了一定的解耦,但是这还是硬编码,进一步扩展可以将依赖写入配置文件中,专门由一个程序检测配置是否正确以及加载配置中所依赖的实现,这个检测程序,就叫IOC容器。

    IOC(Inversion of Control) 是依赖倒置原则(Dependence Inversion Principle, DIP)的同义词。

    • DI: 依赖注入
    • DS: 依赖查找

    以上几个概念都是IOC的两种实现。

    依赖倒置的核心原则是解耦,如果脱离这个最原始的原则,那就是本末倒置。

    如何满足DIP:

    1. 每个较高层次类都为它所需要的服务提出一个接口声明, 较低层次实现这个接口
    2. 每个高层类都通过该抽象接口使用服务

    相关文章

      网友评论

      本文标题:面向对象的5个设计原则

      本文链接:https://www.haomeiwen.com/subject/relzittx.html