0.面向对象的设计模式与原则

设计模式与面向对象

  • 面向对象设计模式解决的是“类与相互通信的对象之间的组织关系”,包括它们的角色、职责、协作方式几个方面;
  • 面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”是哪些可以满足==“应对变化,提高复用”==的设计
  • 面向对象设计模式是建立在对”面向对象“纯熟、深入的理解的基础上的经验性认识。掌握面向对象设计模式的前提是掌握”面向对象“。

面向对象编程语言的三大机制

  • 封装:隐藏内部实现
  • 继承:复用现有代码
  • 多态:改写对象行为

面向对象编程语言(OOPL)并非面向对象的全部

OOPL 没有回答面向对象的本质性问题:

  1. 我们为什么要使用面向对象?
  2. 我们应该怎样使用三大机制来实现”好的面向对象“?
  3. 我们应当遵循什么样的面向对象原则?

重新认识面向对象

  • 面向对象的构建方式更能适应软件的变化,能将变化所带来的影响减为最小;
  • 面向对象的方式更强调各个类的==“责任”==,“新增员工”类型不会影响原来员工类型的实现代码;
  • 对象是什么?
    • 从概念层面讲:对象是某种拥有责任的抽象;
    • 从规格层面讲:对象是一系列可以被其它对象使用的公共接口;
    • 从语言实现层面来看:对象封装了代码和数据
  • 怎么才能设计“好的面向对象”
    • 遵循一定的面向对象设计原则
    • 熟悉一些典型的面向对象设计模式

从设计原则到设计模式

  1. 针对接口编程,而不是针对实现编程
    • 客户无需知道所使用对象的特定类型,只需要知道对象拥有客户所期望的接口;
  2. 优先使用对象组合,而不是类继承
    • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。继承在某种程度上破坏了封装性,子类父类耦合度高;而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低;
  3. 封装变化点
    • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
  • **使用重构得到设计模式——设计模式的应用不宜先入为主。**一上来就使用设计模式是对设计模式最大的误用。没有一步到位的设计模式。
  • 不要把设计模式当成技巧

几条更具体的设计原则

1.单一职责原则(SRP)

定义:一个类应该仅有一个引起它变化的原因;

优点:

  1. 类的复杂性降低,实现什么职责都有清晰明确的定义;
  2. 可读性提高,复杂性降低,那当然可读性提高了;
  3. 可维护性提高,可读性提高,那当然更容易维护了;
  4. 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修
    改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大
    的帮助。
  5. 单一职责适用于接口、类,同时也适用于方法

**PS:**单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

2.里式替换原则(LSP)

定义:所有引用基类的地方必须能透明地使用其子类的对象(子类必须能够替换它们的基类)。

在面向对象的语言中,继承是必不可少的、非常优秀的语言机制。它存在非常明显优缺点。

优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的重用性;
  • 子类可以形似父类,但又异于父类;
  • 提高代码的可扩展性:很多开源框架的扩展接口都是通过继承父类来完成的;
  • 提高产品或项目的开放性。

缺点:

  • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类多了些约束;
  • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。

  1. 子类必须完全实现父类的方法

    注意:

    • 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
    • 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
  2. 子类可以有自己的个性:以子类对象作为参数传递的地方,用父类对象作为参数传递可能产生类型异常;

    someObject.doSomething(new Son());  //可行
    someObject.doSomething((Son)new Father());  //不可行
    
  3. 子类在覆盖或实现父类的方法时前置条件(输入参数类型)应相同或者更宽松:在一个Invoker类中关联了一个父类,调用了一个父类的方法,子类可以覆写(Override)这个方法,也可以重载(Overload)这个方法,前提是要扩大这个前置条件,就是输入参数的类型宽于父类的类型覆盖范围。否则,代码逻辑可能发生混乱。

  4. 子类覆写或实现父类的方法时输出结果可以被缩小:父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类。

采用里氏替换原则的目的:采用里氏替换原则的目的就是增强程序的健壮性,即使在项目升级时增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。

3.依赖倒置原则(DIP)

定义:高层模块不应该依赖底层模块,二者都应该依赖于抽象;抽象不应该依赖于实现细节,实现细节应该依赖于抽象;更简单地可以理解为“面向接口编程”

具体表现为:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。

实践

  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备;接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。
  • 变量的表面类型尽量是接口或者是抽象类;
  • 任何类都不应该从具体类派生;
  • 尽量不要覆写基类的方法;如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。
  • 结合里氏替换原则使用:接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

4.接口隔离原则(ISP)

定义:不应该强迫客户程序依赖于它们不用的方法(客户端不应该依赖它不需要的接口/类间的依赖关系应该建立在最小的接口上)。

接口隔离原则是对接口进行规范约束,其包含以下4层含义:

  • 接口要尽量小:这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则。
  • 接口要高内聚:高内聚就是提高接口、类、模块的处理能力,减少对外的交互。具体到接口隔离原则就是,要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也越少,同时也有利于降低成本。
  • 定制服务:定制服务就是单独为一个个体提供优良的服务。采用定制服务就必然有一个要求:只提供访问者需要的方法。
  • 接口设计是有限度的:接口的设计粒度越小,系统越灵活。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,所以接口设计一定要注意适度。

5.迪米特法则(LoD)(LKP)

**定义:迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least KnowledgePrinciple,LKP),描述的是:一个对象应该对其他对象有最少的了解。**即一个类应该对自己需要耦合或调用的类的内部情况知道得最少。

迪米特法则对类的低耦合提出了明确的要求,其包含以下4层含义:

  • 只和朋友(类)交流:

  • 每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等;

  • 朋友类的定义:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类;

    • 类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象。
  • **朋友间也是有距离的:**一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。迪米特法则要求类尽量不要对外公布太多的public方法和非静态的public变量,尽量多使private、package-private、protected等访问权限,以及final、readonly等关键字。

  • 是自己的就是自己的:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

  • 谨慎使用Serializable

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。

6.开放封闭原则(OCP):

定义:类模块应该是可以扩展的,但是不可修改(对扩展开放,对更改封闭)

  1. 开闭原则是最基础的一个原则,原则都是开闭原则的具体形态
  2. 开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。

软件开发中的变化可以归纳为以下三种类型:

  • 逻辑变化:只变化一个逻辑,而不涉及其他模块,比如原有的一个算法是a*b+c,现在需要修改为a*b*c,可以通过修改原有类中的方法的方式来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。
  • 子模块变化:各模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的。
  • 可见视图变化:一个展示数据的列表,按照原有的需求是6列,突然有一天要增加1列,而且这一列要跨N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但还是可以通过扩展来完成变化,这就要看我们原有的设计是否灵活。

开闭原则的重要性:

  1. 开闭原则对测试的影响:通过扩展来实现业务逻辑的变化,有利于单元测试。
  2. 开闭原则可以提高复用性:在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。
  3. 开闭原则可以提高可维护性:一款软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能要对程序进行扩展,维护人员最乐意做的事情就是扩展一个类,而不是修改一个类。
  4. 面向对象开发的要求:万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但是万物
    皆运动,有运动就有变化,有变化就要有策略去应对。这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。

如何使用开闭原则:

  1. 抽象约束:通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义
    1. 通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
    2. 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
    3. 抽象层尽量保持稳定,一旦确定即不允许修改。
  2. 元数据(metadata)控制模块行为:元数据是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
  3. 制定项目章程;
  4. 封装变化
    1. 第一,将相同的变化封装到一个接口或抽象类中;
    2. 第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

7.合成/聚合复用原则

尽量使用对象组合/聚合,而不是继承来达到复用:将已有的对象纳入新对象中,作为新对象的对象成员来实现,新对象可以调用已有对象的功能,从而达到复用。

类与类之间的六大关系

泛化

相当于继承的相对概念。如下,Tiger是对Animal的继承,Animal是对Triger的泛化

public class Animal
{
    private char _gender;
}
public class Tiger : Animal
{
    private string _name;
}

实现

类与接口的关系。类实现了接口。如下,Tiger实现了IClimb。

public interface IClimb
{
    void Climb();
}
public class Tiger : IClimb
{
    private string _name;
    public void Climb()
    {
        //具体实现老虎爬树
    }
}

组合

组合是整体与部分的关系,是强拥有关系。部分没有独立的生命周期,与整体的生命周期保持一致。组合是把部分作为整体类的对象。

如下,Tiger是整体,Leg是部分,二者是组合关系。

public class Tiger
{
    private Leg _leg;
    public Tiger(Leg leg)
    {
        this._leg = leg;
    }
}
public class Leg
{
    private int _count;
}

聚合

聚合也是整体与部分的关系,但是个体有独立的生命周期,是弱拥有关系。聚合是把个体对象的指针(引用)作为整体类的属性。

如下,TigerGroup是整体,Tiger是部分,二者是聚合关系。

public class Tiger
{
    private Leg _leg;
}
public class TigerGroup
{
    private Tiger[] tigers;
}

关联

关联是一种拥有关系,它使一个类知道另一个类的属性和方法。

如下,Tiger和Food是关联关系。

public class Tiger
{
    private Food food;
}
public class Food
{
    public string FoodName;
}

依赖

依赖是一种使用关系。如下,Tiger和Water是依赖关系。具体设计中常将Water写作接口,然后用接口注入。

public class Tiger
{
    public void Drink(Water water)
    {
        //喝水
    }
}
public class Water
{
    private float _weight;
}

模式分类

  • 从目的来看:
    1. 创建型模式:负责对象创建
    2. 结构型模式:处理类与对象间的组合
    3. 行为型模式:类与对象交互中的职责分配
  • 从范围来看:
    1. 类模式处理类与子类的静态关系
    2. 对象模式处理对象间的动态关系

如何使用设计模式

一个系统中该如何采用”设计模式“进行开发,需要对用户需求中各模块之间的关系不断分析而迭代演化出来的。

在软件开发过程中,随着不断分析和开发,对软件体系结构进行抽象和细节的划分,从而区分主次。系统的”主线“是抽象出来的主体结构(接口);”次(支线)“是对抽象进行具体实现部分。

在软件开发中,将”主线“接口稳定下来,那么在后续维护过程中,就只需更改针对接口的具体实现就可以。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值