没错,这就是面向对象编程(设计模式)须要遵循的 6 个基本原则

本文首发于 没错,这就是面向对象编程(设计模式)须要遵循的 6 个基本原则,转载请注明出处。

在讨论面向对象编程和模式(具体一点来讲,设计模式)的时候,咱们须要一些标准来对设计的好还进行判断,或者说应该遵循怎样的原则和指导方针。php

如今,咱们就来了解下这些原则:数据库

  • 单一职责原则(S)
  • 开闭原则(O)
  • 里氏替换原则(L)
  • 接口隔离原则(I)
  • 依赖倒置原则(D)
  • 合成复用原则
  • 及迪米特法则(最少知道原则)

本文将涵盖 SOLID + 合成复用原则的讲解及示例,迪米特法则以扩展阅读形式给出。编程

单一职责原则(Single Responsibility Principle[SRP]) ★★★★

一个类只负责一个功能领域中的相应职责(只作一类事情)。或者说一个类仅能有一个引发它变化的缘由。json

来看看一个功能太重的类示例:设计模式

/**
 * CustomerDataChart 客户图片处理
 */
class CustomerDataChart
{
    /**
    * 获取数据库链接
    */
    public function getConnection()
    {
    }

    /**
    * 查找全部客户信息
    */
    public function findCustomers()
    {
    }

    /**
    * 建立图表
    */
    public function createChart()
    {
    }

    /**
    * 显示图表
    */
    public function displayChart()
    {
    }
}

咱们发现 CustomerDataChart 类完成多个职能:缓存

  • 创建数据库链接
  • 查找客户
  • 建立和显示图表

此时,其它类若须要使用数据库链接,没法复用 CustomerDataChart;或者想要查找客户也没法实现复用。另外,修改数据库链接或修改图表显示方式都须要修改 CustomerDataChart 类。这个问题挺严重的,不管修改什么功能都须要多这个类进行编码。架构

因此,咱们采用 单一职责原则 对类进行重构,以下:函数

/**
 * DB 类负责完成数据库链接操做
 */
class DB
{
    public function getConnection()
    {
    }
}

/**
 * Customer 类用于从数据库中查找客户记录
 */
class Customer
{
    private $db;

    public function __construct(DB $db)
    {
        $this->db = $db;
    }

    public function findCustomers()
    {
    }
}

class CustomerDataChart
{
    private $customer;

    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
    }

    /**
    * 建立图表
    */
    public function createChart()
    {
    }

    /**
    * 显示图表
    */
    public function displayChart()
    {
    }
}

重构完成后:优化

  • DB 类仅处理数据库链接的问题,挺提供 getConnection() 方法获取数据库链接;
  • Customer 类完成操做 Customers 数据表的任务,这其中包括 CRUD 的方法;
  • CustomerDataChart 实现建立和显示图表。

各司其职,配合默契,完美!ui

开闭原则(Open-Closed Principle[OCP]) ★★★★★

开闭原则最重要 的面向对象设计原则,是可复用设计的基石。

「开闭原则」:对扩展开放、对修改关闭,即尽可能在不修改原有代码的基础上进行扩展。要想系统知足「开闭原则」,须要对系统进行 抽象

经过 接口抽象类 将系统进行抽象化设计,而后经过实现类对系统进行扩展。当有新需求须要修改系统行为,简单的经过增长新的实现类,就能实现扩展业务,达到在不修改已有代码的基础上扩展系统功能这一目标。

示例,系统提供多种图表展示形式,如柱状图、饼状图,下面是不符合开闭原则的实现:

<?php

/**
 * 显示图表
 */
class ChartDisplay
{
    private $chart;

    /**
     * @param string $type 图标实现类型
     */
    public function __construct(string $type)
    {
        switch ($type) {
            case 'pie':
                $this->chart = new PieChart();
                break;

            case 'bar':
                $this->chart = new BarChart();
                break;

            default:
                $this->chart = new BarChart();
        }

        return $this;
    }

    /**
     * 显示图标 
     */
    public function display()
    {
        $this->chart->render();
    }
}

/**
 * 饼图
 */
class PieChart
{
    public function render()
    {
        echo 'Pie chart.';
    }
}

/**
 * 柱状图
 */
class BarChart
{
    public function render()
    {
        echo 'Bar chart.';
    }
}

$pie = new ChartDisplay('pie');
$pie->display(); //Pie chart.

$bar = new ChartDisplay('bar');
$bar->display(); //Bar chart.

在这里咱们的 ChartDisplay 每增长一种图表显示,都须要在构造函数中对代码进行修改。因此,违反了 开闭原则。咱们能够经过声明一个 Chart 抽象类(或接口),再将接口传入 ChartDisplay 构造函数,实现面向接口编程。

/**
* 图表接口
*/
interface ChartInterface
{
    /**
    * 绘制图表
    */
    public function render();
}

class PieChart implements ChartInterface
{
    public function render()
    {
        echo 'Pie chart.';
    }
}

class BarChart implements ChartInterface
{
    public function render()
    {
        echo 'Bar chart.';
    }
}

/**
 * 显示图表
 */
class ChartDisplay
{
    private $chart;

    /**
     * @param ChartInterface $chart
     */
    public function __construct(ChartInterface $chart)
    {
        $this->chart = $chart;
    }

    /**
     * 显示图标 
     */
    public function display()
    {
        $this->chart->render();
    }
}


$config = ['PieChart', 'BarChart'];

foreach ($config as $key => $chart) {
    $display = new ChartDisplay(new $chart());
    $display->display();
}

修改后的 ChartDisplay 经过接收 ChartInterface 接口做为构造函数参数,实现了图表显示不依赖于具体的实现类即 面向接口编程。在不修改源码的状况下,随时增长一个 LineChart 线状图表显示。具体图表实现能够从配置文件中读取。

里氏替换原则(Liskov Substitution Principle[LSP]) ★★★★★

里氏代换原则:在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立。若是一个软件实体使用的是一个子类对象的话,那么它不必定可以使用基类对象。

示例,咱们的系统用户类型分为:普通用户(CommonCustomer)和 VIP 用户(VipCustomer),当用户收到留言时须要给用户发送邮件通知。原系统设计以下:

<?php

/**
 * 发送邮件
 */
class EmailSender
{
    /**
     * 发送邮件给普通用户
     *
     * @param CommonCustomer $customer
     * @return void
     */
    public function sendToCommonCustomer(CommonCustomer $customer)
    {
        printf("Send email to %s[%s]", $customer->getName(), $customer->getEmail());
    }
    
    /**
     * 发送邮件给 VIP 用户
     * 
     * @param VipCustomer $vip
     * @return void
     */
    public function sendToVipCustomer(VipCustomer $vip)
    {
        printf("Send email to %s[%s]", $vip->getName(), $vip->getEmail());
    }    
}

/**
 * 普通用户
 */
class CommonCustomer
{
    private $name;
    private $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }
}

/**
 * Vip 用户
 */
class VipCustomer
{
    private $name;
    private $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }
}

$customer = new CommonCustomer("liugongzi", "liuqing_hu@126.com");
$vip = new VipCustomer("vip", "liuqing_hu@126.com");

$sender = new EmailSender();
$sender->sendToCommonCustomer($customer);// Send email to liugongzi[liuqing_hu@126.com]
$sender->sendToVipCustomer($vip);// Send email to vip[liuqing_hu@126.com]

这里,为了演示说明咱们经过在 EmailSender 类中的 send* 方法中使用类型提示功能,对接收参数进行限制。因此若是有多个用户类型可能就须要实现多个 send 方法才行。

依据 里氏替换原则 咱们知道,可以接收父类的地方 必定 可以接收子类做为参数。因此咱们仅需定义 send 方法来接收父类便可实现不一样类型用户的邮件发送功能:

<?php

/**
 * 发送邮件
 */
class EmailSender
{
    /**
     * 发送邮件给普通用户
     *
     * @param CommonCustomer $customer
     * @return void
     */
    public function send(Customer $customer)
    {
        printf("Send email to %s[%s]", $customer->getName(), $customer->getEmail());
    }
}

/**
 * 用户抽象类
 */
abstract class Customer
{
    private $name;
    private $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }

}


/**
 * 普通用户
 */
class CommonCustomer extends Customer
{
}

/**
 * Vip 用户
 */
class VipCustomer extends Customer
{
}

$customer = new CommonCustomer("liugongzi", "liuqing_hu@126.com");
$vip = new VipCustomer("vip", "liuqing_hu@126.com");

$sender = new EmailSender();
$sender->send($customer);// Send email to liugongzi[liuqing_hu@126.com]
$sender->send($vip);// Send email to vip[liuqing_hu@126.com]

修改后的 send 方法接收 Customer 抽象类做为参数,到实际运行时传入具体实现类就能够轻松扩展需求,再多客户类型也不用担忧了。

依赖倒置原则(Dependence Inversion Principle[DIP]) ★★★★★

依赖倒转原则:抽象不该该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

在里氏替换原则中咱们在未进行优化的代码中将 CommonCustomer 类实例做为 sendToCommonCustomer 的参数,来实现发送用户邮件的业务逻辑,这里就违反了「依赖倒置原则」。

若是想在模块中实现符合依赖倒置原则的设计,要将依赖的组件抽象成更高层的抽象类(接口)如前面的 Customer 类,而后经过采用 依赖注入(Dependency Injection) 的方式将具体实现注入到模块中。另外,就是要确保该原则的正确应用,实现类应当仅实如今抽象类或接口中声明的方法,不然可能形成没法调用到实现类新增方法的问题。

这里提到「依赖注入」设计模式,简单来讲就是将系统的依赖有硬编码方式,转换成经过采用 设值注入(setter)构造函数注入接口注入 这三种方式设置到被依赖的系统中,感兴趣的朋友能够阅读我写的 深刻浅出依赖注入 一文。

举例,咱们的用户在登陆完成后须要经过缓存服务来缓存用户数据:

<?php

class MemcachedCache
{
    public function set($key, $value)
    {
        printf ("%s for key %s has cached.", $key, json_encode($value));
    }
}

class User
{
    private $cache;

    /**
     * User 依赖于 MemcachedCache 服务(或者说组件)
     */
    public function __construct()
    {
        $this->cache = new MemcachedCache();
    }

    public function login()
    {
        $user = ['id' => 1, 'name' => 'liugongzi'];
        $this->cache->set('dp:uid:' . $user['id'], $user);
    }
}

$user = new User();
$user->login(); // dp:uid:1 for key {"id":1,"name":"liugongzi"} has cached.

这里,咱们的缓存依赖于 MemcachedCache 缓存服务。然而因为业务的须要,咱们须要缓存服务有 Memacached 迁移到 Redis 服务。固然,现有代码中咱们就没法在不修改 User 类的构造函数的状况下轻松完成缓存服务的迁移工做。

那么,咱们能够经过使用 依赖注入 的方式,来实现依赖倒置原则:

<?php

class Cache
{
    public function set($key, $value)
    {
        printf ("%s for key %s has cached.", $key, json_encode($value));
    }
}

class RedisCache extends Cache
{
}

class MemcachedCache extends Cache
{
}

class User
{
    private $cache;

    /**
     * 构造函数注入
     */
    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    /**
     * 设值注入
     */
    public function setCache(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function login()
    {
        $user = ['id' => 1, 'name' => 'liugongzi'];
        $this->cache->set('dp:uid:' . $user['id'], $user);
    }
}

// use MemcachedCache
$user =  new User(new MemcachedCache());
$user->login(); // dp:uid:1 for key {"id":1,"name":"liugongzi"} has cached.

// use RedisCache
$user->setCache(new RedisCache());
$user->login(); // dp:uid:1 for key {"id":1,"name":"liugongzi"} has cached.

完美!

接口隔离原则(Interface Segregation Principle[ISP]) ★★

接口隔离原则:使用多个专门的接口,而不使用单一的总接口,即客户端不该该依赖那些它不须要的接口。

简单来讲就是不要让一个接口来作太多的事情。好比咱们定义了一个 VipDataDisplay 接口来完成以下功能:

  • 经过 readUsers 方法读取用户数据;
  • 可使用 transformToXml 方法将用户记录转存为 XML 文件;
  • 经过 createChart 和 displayChart 方法完成建立图表及显示;
  • 还能够经过 createReport 和 displayReport 建立文字报表及现实。
abstract class VipDataDisplay
{
    public function readUsers()
    {
        echo 'Read all users.';
    }

    public function transformToXml()
    {
        echo 'save user to xml file.';
    }

    public function createChart()
    {
        echo 'create user chart.';
    }

    public function displayChart()
    {
        echo 'display user chart.';
    }

    public function createReport()
    {
        echo 'create user report.';
    }

    public function displayReport()
    {
        echo 'display user report.';

    }
}

class CommonCustomerDataDisplay extends VipDataDisplay
{

}

如今咱们的普通用户 CommonCustomerDataDisplay 不须要 Vip 用户这么复杂的展示形式,仅须要进行图表显示便可,可是若是继承 VipDataDisplay 类就意味着继承抽象类中全部方法。

如今咱们将 VipDataDisplay 抽象类进行拆分,封装进不一样的接口中:

interface ReaderHandler
{
    public function readUsers();
}

interface XmlTransformer
{
    public function transformToXml();
}

interface ChartHandler
{
    public function createChart();

    public function displayChart();
}

interface ReportHandler
{
    public function createReport();

    public function displayReport();
}

class CommonCustomerDataDisplay implements ReaderHandler, ChartHandler
{
    public function readUsers()
    {
        echo 'Read all users.';
    }

    public function createReport()
    {
        echo 'create user report.';
    }

    public function displayReport()
    {
        echo 'display user report.';

    }
}

重构完成后,仅需在实现类中实现接口中的方法便可。

合成复用原则(Composite Reuse Principle[CRP]) ★★★★

合成复用原则:尽可能使用对象组合,而不是继承来达到复用的目的。

合成复用原则就是在一个新的对象里经过关联关系(包括组合和聚合)来使用一些已有的对象,使之成为新对象的一部分;新对象经过委派调用已有对象的方法达到复用功能的目的。简言之:复用时要尽可能使用组合/聚合关系(关联关系) ,少用继承。

什么时候使用继承,什么时候使用组合(或聚合)?

当两个类之间的关系属于 IS-A 关系时,如 dog is animal,使用 继承;而若是两个类之间属于 HAS-A 关系,如 engineer has a computer,则优先选择组合(或聚合)设计。

示例,咱们的系统有用日志(Logger)功能,而后咱们实现了向控制台输入日志(SimpleLogger)和向文件写入日志(FileLogger)两种实现:

<?php

abstract class Logger
{
    abstract public function write($log);
}

class SimpleLogger extends Logger
{
    public function write($log)
    {
        print((string) $log);
    }
}

class FileLogger extends Logger
{
    public function write($log)
    {
        file_put_contents('logger.log', (string) $log);
    }
}

$log = "This is a log.";

$sl = new SimpleLogger();
$sl->write($log);// This is a log.

$fl = new FileLogger();
$fl->write($log);

看起来很好,咱们的简单日志和文件日志可以按照咱们预约的结果输出和写入文件。很快,咱们的日志需求有了写加强,如今咱们须要将日志同时向控制台和文件中写入。有几种解决方案吧:

  • 从新定义一个子类去同时写入控制台和文件,但这彷佛没有用上咱们已经定义好的两个实现类:SimpleLogger 和 FileLogger;
  • 去继承其中的一个类,而后实现另一个类的方法。好比继承 SimpleLogger,而后实现写入文件日志的方法;嗯,没办法 PHP 是单继承的语言;
  • 使用组合模式,将 SimpleLogger 和 FileLogger 聚合起来使用。

咱们直接看最后一种解决方案吧,前两种实在是有点。

class AggregateLogger
{
    /**
     * 日志对象池
     */
    private $loggers = [];

    public function addLogger(Logger $logger)
    {
        $hash = spl_object_hash($logger);
        $this->loggers[$hash] = $logger;
    }

    public function write($log)
    {
        array_map(function ($logger) use ($log) {
            $logger->write($log);
        }, $this->loggers);
    }
}

$log = "This is a log.";

$aggregate = new AggregateLogger();

$aggregate->addLogger(new SimpleLogger());// 加入简单日志 SimpleLogger
$aggregate->addLogger(new FileLogger());// 键入文件日志 FileLogger

$aggregate->write($log);

能够看出,采用聚合的方式咱们能够很是轻松的实现复用。

迪米特法则

设计模式六大原则(5):迪米特法则

感谢 Liuwei-Sunny 大神在「软件架构、设计模式、重构、UML 和 OOAD」领域的分享,才有此文。

相关文章
相关标签/搜索