单一职责原则和开放封闭原则

单一职责原则:

所谓单一职责原则,简单点说就是,每个类,每个方法最好是只做一件事情,只具备一个功能。这样做有什么好处呢,举个简单的例子。


A方法具有两个功能,一个功能是计算N个数的平均数,另一个功能是睡眠10秒钟,那这个时候,我的B方法需要一个功能,就是计算平均数,但B方法又不想在计算了以后去睡眠10秒钟,那这个时候B方法就没办法使用现成的A方法进行计算。现在有什么办法可以解决这个问题呢。把A分离成两个独立的,且只有一个功能的函数A1和A2,A1只做一件事情就是计算N个数的平均数,A2也只做一件事情,就是睡眠十秒钟,那现在A可以改写成依次调用A1,A2,而B函数则可以直接调用A1函数去计算N个数的平均数。

void A(int[] A, int num)
{
//计算平均数
//睡眠10秒
}

如果一个类或者方法承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。

单一职责提高了代码的颗粒度,提高了每个方法的内聚性。


生活中,有很多类似的现象,比如有两种玩具,这两种玩具都是组合积木类玩具,共同点是总体积,总重量相同,每两块积木之间都可以任意的拼装在一起,不同点是第一种积木玩具,总共只有三块,第二种积木由很多小的积木组成。
我们之前说过,每两块积木之间都可以任意的拼装在一起,那可想而知,那么第二种积木就可以拼出非常多的样式,有非常多的组合方式,而第一种则只有简单的几种组合方式。

这就相当于是我们降低了方法的颗粒度,让每一个方法只做一件事所带来的好处,这会大大的提高方法的可重复利用性,带来更多的组合方式。

在单一职责原则中,我们把职责定义为“变化的原因”。如果你能想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。有时我们很难注意到这一点。再举一个例子


Rectangle长方形类具有两个方法,一个是draw(),可以绘制出一个长方形,还有一个方法area(),可以用来计算长方形的体积。由于draw需要绘制,所以就必须把GUI的代码链接进来。
现在有一个A模块,它需要调用Rectangle类的area方法,去计算一个长方形的体积,但由于draw方法和area方法耦合在了一起,所以在A这个单纯的计算模块内,却需要把GUI相关的内容链接进来,这样就会大大的浪费链接时间,编译时间以及内存占用。

还有一种情况,A 模块和B模块分别要使用Rectangle的area方法和draw方法,由于B模块需求的原因,需要对draw方法进行修改,这个改变会迫使我们必须重新对A模块进行构建,测试和部署,如果忘记了这样做,就可能会以不可预测的方式失败。

一个较好的设计是把这两个职责分离到两个完全不同的类中。

开放封闭原则:

官方说法是 软件实体(模块、类、函数等)应该可以扩展,但是不可以修改。也就是说软件对扩展开放,对修改关闭。为什么要这样呢,因为修改原有代码这个行为在软件开发的过程中是一个非常危险的行为,每次修改都可能会带来不可预知的错误。因此每次修改都要伴随着重新的构建,测试和部署。

但是需要特殊说明的是,对修改关闭不是说软件设计不能做修改,而是尽量不要做不必要的修改。而且不能说每个地方都有扩展。这样反而会造成了代码的臃肿。所以这里的扩展与修改关闭是有限制的,有度的。

开闭原则,可以说是其他原则的实现,也是面向对象设计的终极目标。我们也可以说开闭原则是其他设计模式原则的核心。


比如说有一个client 类,需要依赖server 类,如图:

这里写图片描述

那随着时间的推移,需求的变化,client不在需要server类,而是需要一个其他类型的server类,但由于之前的设计client类已经和server类耦合在了一起,新的需求导致我们不得不去重新修改client类,伴随每次修改,我们都要重新的构建,测试,这是一件很令人头疼的事情,很明显这种做法没有做到对修改关闭。

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

下面我们就用抽象编程的思想去解决上面的问题
Client不再依赖一个具体的实现,而是改成依赖一个接口,这个接口暂且叫做clientinterface,我们后面再说说为什么这个接口叫做clientinterface而不是叫做serverinterface。那这个接口就代表这client类的功能需求,凡是实现了这个clientinterface接口的类,都可以被client拿去使用。Client不再依赖一个具体的类,这就叫做针对接口编程,而不是针对实现编程。这种稳定的设计让我们不再需要修改和重新构建client和clientinterface部分的2进制代码。

这里写图片描述

上面展示了一个遵循OCP的设计,在这个设计中,ClientInterface类是一个拥有抽象成员函数的抽象类。Client类对象使用ClientInterface类的派生类的对象,如果我们希望client对象使用一个不同的服务器类,那么只需要从clientinterface类派生一个新的类,无需对client类做任何改动。

那为什么叫做clientinterface呢,因为这个接口就是专门针对于client定制的接口,是client需要的服务,它与client的关系要比实现它与server的关系亲密的多。


举个生活里的的例子,生活里处处都有这种对抽象编程的思想,比如一个电脑,
他由许许多多的零部件组成,部件与部件之间都是通过一定的接口,插口进行契合,进行连接,这就是一种依赖接口的思想,只要接口一致,那么不论你的部件是怎么实现的,他们都是可以组装在一起的。比如同一个主板上既可以插i5的处理器,也可以插i6的处理器,因为他们的接口都是相同的,相当于他们都扩展了同一个与主板的接口。


再举了一个加减乘除法的例子。
最开始的需求是做一个加法的操作。后来继续加入减法、乘法、除法。
开始我们想加法以后可能会做一个需求变更:加入其它的算法法则。所以我们要有一个预判性,这个预判性会导致我们项目以后的扩展性,也会导致如果需求发生变更,程序修改的难易程度。
所以,我们要做一个算法法则的操作类,加减乘除法都继承此操作接口。再加一个算法法则的客户端类类操作此算法。

// ConsoleApplication9.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>

///
//下面是一个没有遵循OCP原则的实现方式

class addOperate
{
public:
    int getResult(int a, int b) { return a + b; }
};

//目前这个client只具备计算加法的能力,如果想实现,减法,乘法,除法的话
//必须要修改这个client类,就没有办法做到对修改关闭

//既不对扩展开放,也不修改封闭

class client
{
public:
client(addOperate* addOp, int a, int b)
:m_addOp(addOp) , m_a(a), m_b(b){}

    //最初的需求,只有一个加法计算
    //随着时间的推移,需求发生了变化
    int getAddResult()
    {
        return m_addOp->getResult(m_a, m_b);
    }

private:
    addOperate* m_addOp = nullptr;

    int m_a = 0;
    int m_b = 0;
};

///
//下面是一个遵循OCP原则的实现方式,可以应对未来的变化

class operateInterface
{
public:
    virtual int getResult(int a, int b) = 0;
};

//最初的需求,只有一个加法计算
class addOperate : public operateInterface
{
public:
    virtual int getResult(int a, int b) override { return a + b; }
};

class client
{
public:
    client(operateInterface* op, int a, int b) 
        :m_op(op), m_a(a), m_b(b){}

    int getResult()
    {
        return m_op->getResult(m_a, m_b);
    }
private:
    operateInterface* m_op = nullptr;

    int m_a = 0;
    int m_b = 0;
};

//如果后续想加入加法,乘法,除法
//只需要分别扩展实现不同的子类算法

class subOperate : public operateInterface
{
public:
    virtual int getResult(int a, int b) override { return a - b; }
};

class multiOperate : public operateInterface
{
public:
    virtual int getResult(int a, int b) override { return a * b; }
};

int _tmain(int argc, _TCHAR* argv[])
{
    int res = client(new addOperate (), 3, 2).getResult();
    std::cout << res;

    return 0;
}

那目前的设计只支持2元运算,如果想支持多元运算,就可以把传2个int参数,改为传一个int[] 数组, 这样就支持了对运算元个数的扩展。支持任意个数的操作运算。

但到底有没有需求设计成这样还是要根据我们对未来需求的预测和对工作量以及带来的代码复杂度的分析与评估。

我们来上一下大话设计模式中的图:

这里写图片描述


开放封闭原则是面向对象的核心所在。遵循这个原则可以带来面向对象技术所生成的很多的好处,也就是可维护,可扩展,可复用,灵活性好。我们应该仅对程序中呈现出频繁变化的那部分做出抽象,然而,对于应用程序中的每一个部分都可以的进行抽象同样不是一个好注意。拒绝不成熟的抽象,和抽象本身一样重要。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值