设计模式_0_面向对象原则

对于面向对象的三大特性(封装、继承、多态),几乎是所有人都能张口就来的,但学习设计模式之前,依旧应该好好复习一下面向对象的设计原则,不仅是由于拘泥于背定义的前提下去理解的原则永远只是纸面功夫,更是因为这六大原则几乎贯穿所有设计模式之中,甚至可以基于他们来构建属于自己的设计模式。

首先来认识一下,对象到底是什么?

  • 从语言层面来讲,对象是封装了代码和数据的结合体
  • 从规格层面来讲,对象是一系列可以被使用的公共接口
  • 从概念层面来讲,对象是一种拥有责任的抽象体

基于上述的理解,这种面向对象的形式,从宏观层面看来,更能适应软件的变化,减小变化所带来的影响,提升稳定性和可复用性;而从微观看来,对象更强调每个类所负责的那部分责任,只需要每个类负责好自己的工作即可。
复习过对象的概念,就可以开始设计模式之旅的准备工作了:所有设计模式都要遵守的设计原则


1.依赖倒置原则(Dependence Inversion Principle)

定义: 高层模块(稳定)不应该依赖于底层模块(变化),二者都应该依赖抽象(稳定);抽象(稳定)不应该依赖细节(变化),细节应该依赖抽象(稳定)

这点通过虚函数的例子也就很好理解:例如画图类Draw需要描画各种类型Circle、Square…,而在Draw类内去一个添加每一个形状的方法,这样不仅让Draw类“瞎操心”,做了自己不该做的事,也让以后有更多的形状想被书写时的修改添加了困难。

所以上述提到的有缺陷的方式即可表示为:

class Draw{
      //+每增加一个形状,需要在这里增加一类vector
      std::vector<Circle> circles;
      std::vector<Square> squares;

      void draw_all(){
          //+每增加一个形状,需要在这里增加一种属于其类的单独的draw方法
          for(auto circle:circles){
              //how to draw...
          }
          
          for(auto square:squares){
              //how to draw...
          }
      }
  };
  //+每增加一个字母,需要在这里增加一个类
  class Circle{
        //args...
  };
  class Square{
        //args...
  };

可见Draw直接依赖抽象的形状类,通过依赖倒置原则的规划,将上述代码优化为下方格式将会改观许多:

class Draw{
    std::vector<Shape*> vec;

    void draw_all(){
        //增加新类,不再需要修改draw类的过程
        for(auto shape:vec){
            shape->draw();
        }
    }
};

//增加抽象类作为接口,让每个形状“各司其职”
class Shape{
    //args...
    virtual void draw() = 0;
}

//+每增加一个形状,需要在这里增加一个类,并实现自己的纯虚方法
class A:public Shape{
    void draw(){
        //...
    }
};

从原来的Draw动作类(稳定)直接依赖每一个形状(变化),变为Draw动作类(稳定)依赖一个抽象符号类(稳定),每一个形状(实现细节)再去依赖于抽象,
通过这种方式,我们便可以实现隔离变化

2.开放封闭原则(Open Closed Principle)

定义:一个类模块应该对扩展开放(可扩展),对更改封闭(不可修改)。

在升级和维护软件库的时候,会对旧代码流程进行修改,这会可能破坏原有功能,更坏的情况下可能会导致原模块进行重构并重新测试。
所以尽可能遵循开闭原则,使新功能不影响旧功能。

3.单一职责原则(Single Responsibility Principle)

定义:一个类应该仅有一个引起他变化的原因,变化的方向隐藏着这个类的责任。

每个类应尽量有且仅有他自身的职责,尽量避免一个类中出现过多的责任。
这样做的好处较为明显,控制类的粒度大小、降低类的复杂度、可读性和可维护性,对于变更引起的风险也较低。避免修改引起其他功能的影响。

在上面的优化后的例子中各种继承自Shape类的子类和Draw就都遵循了该原则。

4.里氏替换原则(Liskov Substitution Principle)

定义:子类必须能够替换基类,可以扩展父类的功能,但不能改变父类原有的功能。

在实际应用场景中,如子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法和重载父类的方法。

举个较容易理解的例子:企鹅、鸵鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。

5.接口隔离原则(Interface Segregation Principl)

定义:用户不应该依赖它不需要的接口;接口应该小而完备

在实际场景中,尽量只给用户提供他们所需求的接口,减少暴露没必要的接口将其设为protect/private,防止用户对过多的功能形成依赖,即需要保持更多接口的稳定,对后续修改/升级带来更多工作量,也防止自身更改影响用户稳定性

6.迪米特原则(Least Knowlegde Principle)

定义:一个对象应当尽可能少的去了解其他对象。

类与类之间的关系越密切时,其耦合度也就越大,这是改变其中一个类,另一个类也容易受到影响,
迪米特原则从原理上看,他是和依赖倒置原则相辅相成的,违背了依赖倒置往往也就违背了迪米特原则。
所以通过减少各部分之间的依赖关系,来实现高内聚,低耦合的设计结构。

7.合成复用原则(Composite Reuse Principle)

定义:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

类的继承通常是“白箱复用”,对象组合通常为“黑箱复用”
虽然继承复用相对来讲比较容易实现,但他可能存在以下缺点:

  1. 破坏了类的封装性,将父类的实现细节暴露给子类,所以这个过程父类对子类是透明的(白箱复用)
  2. 父子类间耦合度高,父类的实现的任何改变都会导致子类的实现发生变化,不利于类的扩展与维护。
  3. 限制了复用的灵活性,由于子类静态继承父类,在编译时已定义,所以在运行时不可能发生变化。

而采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 维持了类的封装性。此时源对象的内部细节是新对象看不见的(黑箱复用)
  2. 新旧类之间的耦合度低,复用所需的依赖较少。
  3. 复用的灵活性高,可以在运行时动态进行

该原则在实际应用中,主要通过现有对象作为新对象的成员对象来实现的,以新对象调用现有对象方法来实现复用。


写在前面

再讲设计模式之前,还有些关键的点需要我们注意:

设计模式的应用不宜先入为主,一上来就是用设计模式或者是为了使用设计模式而强行使用,这本身或许是设计模式的最大误用!

现代软件的设计特征是“需求频繁变化”,而设计模式的作用就是在稳定中寻找变化,变化处应用适合的设计模式。所以什么时候,什么场景选用什么模式,比理解设计模式本身更为重要。

换句话说,没有一步到位的设计模式,敏捷开发提倡的是"Refactoring to Patterns"即“通过重构选择模式”,这也是目前普遍被公认为好的设计模式的使用方式。




另外在日后的相关学习中,也推荐两本经典书籍:《重构》、《重构与模式》

下面本专栏将根据黑皮书的顺序,将GOF中23种设计模式的三大类(创建型模式、结构型模式、行为型模式)的每一种设计模式依次解析并举例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值