DOF23 设计模式
谈一谈设计模式本身
一,什么是设计模式?
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性,可维护性,可读性,稳健性以及安全性的解决方案。
二,设计模式的意义
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性,继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点:
- 可以提高程序员的思维能力,编程能力和设计能力
- 是程序更加标准化,代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
- 使设计的代码可重用性高,可读性强,可靠性高,灵活性好,可维护性强。
GoF23(23种设计模式)
一种思维,一种态度,一种进步
创建型模式(特点创建对象跟使用的分离):单例模式,工厂模式,抽象工厂模式,建造者模式,原型模式。
结构型模式(主要描述类或对象按照某种布局组成更大的结构):适配器模式,桥接模式,装饰模式,组合模式,外观模式,享元模式,代理模式。
行为模式(主要是描述类或对象之间怎么去相互协作去完成单个对象无法完成的操作):模板方法模式,命令模式,迭代器模式,观察者模式,中介者模式,备忘录模式,解释器模式,状态模式,策略模式,职责链模式,访问者模式
OOP七大原则(设计模式都是围绕着它来进行的)
一,开闭原则
对扩展开放,对修改关闭。
根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有模块(修改关闭)的基础上,能扩展其功能(扩展开放)。
- 扩展开放:某模块的功能是可以扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
- 修改关闭:某模块被其他模块调用,如果该模块的源代码不能被修改,则对修改是关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的。
系统设置需要遵循开闭原则的原因:
- 稳定性:开闭原则要求扩展功能不修改原有的代码,这可以让软件系统在变化中保持稳定。
- 扩展性:开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。
遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。
二,里式替换原则
继承必须确保超类所拥有的的性质在子类中仍然成立
也就是说必须满足以下两个条件的设计,才能被认为是满足了里式替换原则
- 不应该在代码中出现if/else之类对派生类类型进行判断的条件。
- 派生类应该可以替换基类,并且出现在基类可以出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常的工作。
里式替换原则(LSP)是使代码符合开闭原则的一个重要保证
- 类的继承原则:如果一个派生类的对象可能会在基类出现的地方运行错误,则派生类不应该从基类继承,或者说,应该重新设计他们的关系。
- 动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入错误。
历史替换原则为我们是否使用继承提供了判断的依据,不再是简单的根据两者之间是否有相同之处来说使用继承。
里式替换原则的引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。
具体来说:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类中可以添加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入\入参)要比父类方法的输入\入参更宽松。
- 当子类的方法实现父类的方法时(重载\重写或实现抽象方法)的后置条件即方法的(输出\返回值)要比父类更严格或相等。
里式替换原则的优点:
- 约束继承泛滥,是开闭原则的一种体现。
- 加强程序的健壮性,同时变更时也可以做到非常好的提高程序的维护性,扩展性。降低需求变更时的引入风险。
重构违反LSP的设计:
如果两个具体的类A,B之间的关系违反了LSP的设计,假设是从B到A 的继承关系,那么根据具体的情况可以在下面的两种情况中选择一种:
- 创建一个新的抽象类C,做为两个具体类的基类,把A,B的共同行为移到C中来解决问题。
- 从B到A的继承关系改为关联关系。
三,迪米特原则
只与你直接朋友通信,不跟陌生人讲话
- 一个软件实体应当尽可能的少与其他实体发生相互作用。
- 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的单位。
朋友的条件:
- 当前对象本身(this)。
- 以参量形式传入到当前对象方法中的对象。
- 当前对象的实例变量直接引用的对象。
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友。
- 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,都是朋友,否则就是陌生人。
迪米特原则的优缺点:
迪米特原则的初衷在于降低类之间的耦合度。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块独立,相互之间不存在或很少有依赖关系。
迪米特原则不希望类建立直接的接触。如果真需要建立联系,也希望通过它的友元类来转达,因此使用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度。
使用迪米特原则需要注意的是:
一个类公开的public属性方法越多,修改时设及的面也就越大,变更引起的风险扩散也就越大。因此为了保持朋友类之间的距离,在设计时需要反复衡量:是否public方法和属性还可以少一点,是否可以修改为private属性。
迪米特原则尽量要求类不要对外公开太多的public方法和非static的public变量,尽量多使用private,protected等访问权限。
四,单一职责原则
永远不要让一个类存在多个改变的理由
如果一个类需要改变,改变它的理由永远只有一个,如果有多个就要重新设计该类。
单一职责原则的核心是:只能让一个方法或类或接口有且只有一个职责,就是一个类、接口、方法只做一件事,保证它是单一的
如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类的变化,而这个变化将会影响到该类不同职责的使用者:
- 一方面,如果一个职责使用了外部类库,则使用另一个职责的用户却也不得不包含这个未被使用的外部类库。
- 另一方面,某个用户由于某个原因需要修改其中一个职责,另一个职责的用户也将收到影响,他将不得不重新编译和配置。
这违反了设计的开闭原则
单一职责原则从职责(改变理由)的侧面为我们对类(接口)的抽象的颗粒度建立了判断基准:在为系统设计类(接口)的时候应该保证它们的单一职责性。
降低了类的复杂度,提高了类的可读性,提高了系统的可维护性,降低变更引起的风险。
五,接口隔离原则
要为各个类建立它们需要的专用接口
- 接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能单一的接口。
- 接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b的接口a也应当遵循上述原则:不应该包含用户不使用的方法,如果有则说明a被b污染了应该重新设计他们之间的关系。
如果用户被迫依赖他们不使用的接口,当接口发生改变时,他们也不得不跟着改变。换而言之,一个用户依赖了未使用但被其他用户使用的接口,当其他用户修改该接口时,依赖该接口的所有用户都将受到影响。这违反了开闭原则
接口隔离原则应当:
- 一类对一个类的依赖应当建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
接口隔离原则的优点:
- 接口隔离原则从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准:在为系统设计接口的时候,使用多个专门的接口代替单一的胖接口。
- 符合高内聚,低耦合的设计思想,从而使得类具有很好的可读性,可扩展性和可维护性。
- 注意适度原则,接口分隔需要适度,避免产生大量的细小接口。
单一职责原则和接口隔离原则的区别:
单一职责原则是强调类、接口、方法的职责是单一的,强调职责,方法可以多,针对程序中的实现细节。
接口隔离原则主要约束接口,针对抽象,整体的框架。
六,依赖倒置原则
- 要面向接口编程,不要面向实现编程
- 高层模块不应该依赖于底层模块,二者都应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
概念理解:
依赖:在程序设计中,模块a调用\使用了模块b,我们称为a依赖于b
高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本或低层的操作,我们称之为低层模块。另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层次模块。
依赖倒置:面向对象程序设计相当于面向过程(结构化)程序设计而言,依赖关系被倒置了。因为传统结构化程序设计当中,高层模块总是依赖于低层模块。
抽象接口是对低层次模块的抽象,低层次模块继承或实现该抽象接口
这样高层模块不直接依赖低层模块,而是依赖于抽象接口。抽象接口也不依赖于低层模块的实现细节,而是低层模块依赖抽象接口。
类和类之间都通过抽象接口层来建立关系
怎么使用依赖倒置:
依赖于抽象:
任何一个变量都不应该持有一个指向具体类的指针或引用
任何类都不应该从具体类派生
设计接口而非实现接口:
使用继承避免对类的直接绑定
抽象类\接口:倾向于较少的变化;抽象是关键点,它易于修改和扩展;不要强制修改那些抽象接口\类
避免传递依赖:
避免高层依赖于低层
使用继承和抽象类来有效的清除传递依赖
依赖倒置原则的优点:
可以减少类之间的耦合性,提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的的风险。
七,合成复用原则
尽量使用组合或聚合关联关系来实现,其次才考虑使用继承关系来实现
概念理解:
即使在一个新的对象里使用一些已有的对象,使之成为新对象的一部分,新对象通过这些对象的委派达到复用已有功能的目的。就是说要尽量使用合成/聚合,而不是继承关系达到复用的目的。
聚合表示整体和部分的关系,表示拥有。组合是一种更强的拥有,部分和整体的生命周期一样。
一个组合关系的对象是不能和另外一个组合关系共享的。
组合是值的聚合,而一般说的聚合是引用的聚合。
在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。
通过组合/聚合复用的优缺点:
优点:
- 新对象存取子对象的唯一方法是通过子对象的接口
- 这种复用是黑箱复用,因为子对象的内部细节是新对象所看不见的
- 这种复用更好地支持封装性
- 这种复用实现上的相互依赖性比较小
- 每一个新的类可以将焦点集中在一个任务上
- 这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象
- 作为复用手段可以应用到几乎任何环境中去
缺点:就是系统中会有比较多的对象需要管理
通过继承来进行复用的优缺点:
优点:
- 新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类
- 修改和扩展继承而来的实现较为容易。
缺点:
- 继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,有称为“白箱”复用。
- 如果基类发生改变,那么派生类的实现也不得不发生改变。
- 从基类继承而来的实现时静态的,不可能在运行时间内发生改变,没有足够的灵活性。