面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”是那些可以满足“应对变化,提高复用”的设计。
OOPL没有回答面向对象的根本性问题——我们为什么要使用面向对象?我们应该怎样使用三大机制来实现“好的面向对象”?我们应该遵循什么样的面向对象原则?
任何一个严肃的面向对象程序员,都需要系统地学习面向对象的知识,单纯从编程语言上获得的面向对象知识,不能够胜任面向对象设计与开发。
设计模式描述了软件设计过程中某一类常见问题的一般性的解决方案。面向对象设计模式描述了面向对象设计过程中、特定场景下、类与相互通信的对象之间常见的组织关系。
客户无需知道所使用对象的特定类型,只需要知道对象拥有客户所期望的接口。(客户即调用你程序的程序)
类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。继承在某种程度上破坏了封装性,子类父类耦合度高;而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低
使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。
单一职责原则(SRP):一个类应该仅有一个引起它变化的原因。
开放封闭原则(OCP):类模块应该是可扩展的,但是不可修改(对扩展开放,对修改封闭)
里氏替换原则(LSP):所有引用基类的地方必须能透明地使用其子类的对象。
接口隔离原则(ISP):不应该强迫客户程序依赖于它们不用的方法
依赖倒置原则(DIP):
高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
抽象不应该依赖于实现细节,实现细节应该依赖于抽象。
(抽象就是抽象类或接口,细节就是实现类)
每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
变量的表面类型尽量是接口或者抽象类。
任何类都不应该从具体类派生。
尽量不要覆写基类的方法。
结合里氏替换原则使用。
依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维,我要开奔驰车就依赖奔驰车。
编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。
第一,通过接口实现对扩展开放,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
第三,抽象层尽量保持稳定,一旦确定即不允许修改。
是你还有你,一切拜托你。 是你——继承,还有你——关联,拜托你——调用父类方法
继承是为了后续的继续装饰,如果不继承,后续的加强将不能传递他们共有的父类作为参数,而是他们的实现类,耦合度太高。
动态地给一个对象增加一些额外的职责。就增加功能而言,装饰模式相比生成子类更加灵活。
Component 抽象构件
ConcreteComponent 具体构件
Decorator 装饰角色
ConcreteDecorator 具体装饰角色
通过采用组合、而非继承的手法,装饰模式实现了在运行时动态地扩展对象功能的能力,而且可以根据需要扩展多个功能。避免了单独使用继承带来的“灵活性差”和“多姿类衍生问题”。
Component类在装饰模式中充当抽象接口的角色,不应该去实现具体的行为。而且Decorator类对于Component类应该透明——换言之,Component类无需知道Decorator类,Decorator类是从外部来扩展Component类的功能。
Decorator类在接口上表现为 is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但是实现上又表现为 has-a Component的组合关系,即Decorator类又使用了另外一个Component类。我们可以使用一个或者多个Decorator对象来“装饰”一个Component对象,且装饰后的对象仍然是一个Component对象。
Decorator模式并非解决“多子类衍生的多继承”问题,Decorator模式应用的要点在于解决”主体类在多个方向上的扩展功能“——是为“装饰”的含义。
需要扩展一个类的功能,或给一个类添加附加功能
需要动态地给一个对象增加功能,这些功能可以在动态地撤销。
需要为一批的兄弟类进行改装或加功能,首选装饰模式。
抽象构件:
public abstract class Component {
public abstract void operate();
}
具体构件:
public class ConcreteComponent extends Component {
@Override
public void operate() {
System.out.println("do something!");
}
}
抽象装饰者:
public abstract Decorator extends Component {
private Component component = null;
//通过构造函数传递被修饰者
public Decorator(Cpmponent component){
this.component = component;
}
//委托给被修饰者执行
@Override
public void operate(){
this.component.operate();
}
}
具体装饰类:
public class ConcreteDecorator extends Decorator {
//传入被修饰者
public ConcreteDecorator(Component component){
super(component);
}
//被装饰方法之前调用
public void before(){
System.out.println("operate之前调用!");
}
//被装饰方法之后调用
public void later(){
System.out.println("operate之后调用!");
}
//重写父类的operate方法
public void operate(){
this.before();
super.operate();
this.later();
}
}
场景类:
public class Clent{
public static void main(Stirng[] args) {
Component component = new ConcreteComponent();
//第一次装饰
component = new ConcreteDecorator(component);
//可被多个装饰类装饰
......
//修饰后运行
component.operator();
}
}
在面向对象系统中,我们常会遇到一类具有“容器”特征的对象——即他们在充当对象的同时,又是其他对象的容器。
public class SingleBox extends IBox{
public void process() {....}
}
public class ContainerBox extends IBox{
public void process() {....}
public ArrayList getBoxes(){....}
}
//如果我们要对这样的对象容器进行处理:
IBox box = Factory.getBox();
if(box instanceof ContainerBox){
box.process();
ArrayList list = ((ContrainerBox)box).getBoxes();//包含递归
}else if (box instanceof SingleBox){
box.process();
}
思考上述问题的症结:事实上由于Tank类型的固有逻辑使得Tank类型具有两个变化的维度——一个变化的维度为“平台的变化”,一个为安华的维度为“型号的变化”。
如何应对这种“多维度的变化”?如何利用面向对象技术来使得Tank类型可以轻松地沿着”平台”和”型号”两个方向变化,而不引入额外的复杂度呢?
将抽象部分与实现部分分离,使他们都可以独立地变化。
将一个事物中多个维度的变化分离,两个维度将可以随意组合,简化代码。
可以理解为结构型模式的工厂方法模式。
Abstraction——抽象化角色
Impementor——实现化角色
RefinedAbstraction——修正抽象画角色
ConcreteImplementor——具体是鲜花角色
Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象(Tank的型号)和实现(不同的平台)可以沿着各自的维度来变化。
所谓抽象和实现沿着各自维度的变化,即“子类化”他们,比如,不同的Tank型号子类,和不同的平台子类)。得到各个子类之后,便可以任意组合他们,从而获得不同平台上的不同型号。
Bridge模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge模式是比多继承方案更好的解决方法。
Bridge模式的应用一般在“两个非常强的变化维度”,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换言之两个变化不会导致纵横交错的结果,并不一定要使用Bridge模式。
不希望或不适用使用继承的场景
接口或抽象类不稳定的场景
实现化角色:
public interface Implementor {
public void doSomething();
public void doAnything();
}
具体实现化角色:
public class ConcreteImplementor1 implements Implementor{
public void doSomething() {}
public void doAnything() {}
}
public class ConcreteImplementor2 implements Implementor{
public void doSomething() {}
public void doAnything() {}
}
抽象化角色:
public abstract class Abstraction {
//定义对实现化角色的引用
private Implementor imp;
//约束子类必须实现该构造函数
public Abstraction(Implementor imp){
this.imp = imp;
}
//自身的行为和属性
public void request(){
this.imp.doSomething();
}
//获得实现化角色
public Implementor getImp(){
return imp;
}
}
具体抽象化角色:
public class RefinedAbstraction extends Abstraction {
//覆写构造函数
public RefindAbsrtraction(Implementor imp){
super(imp);
}
//修正父类的行为
@Override
public void request(){
/*
*业务处理
*/
super.request();
super.getImp().doAnything();
}
}
场景类
public class Client {
public sttic void main(String[] args){
//定义一个实现化角色
Implementor imp = new ConcreteImplementor1();
//定义一个抽象化角色
Abstraction abs = new RefinedAbstraction(imp);
//执行行文
abs.request();
}
}
适配,即在不改变原有实现的基础上,将原先不兼容的接口转换为兼容接口。
在软件系统中,由于应用环境的变化,常常需要将“一些现存的现象”放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。
如何应对这种“迁移的变化”?如何既能利用现有对象的良好实现,同时又能满足新的应用环境所要求的接口?
将一个类的接口转换成客户端所期望的另一种接口,从而使原本由于接口不匹配而不能一起工作的那些类可以一起工作。
——对象适配器
——类适配器
角色:
Target目标角色
Adaptee源角色
Adapter适配器角色,将源角色转为客户希望的目标角色
适配器模式适用情形:
当适用一个已存在的类,而它的接口不符合所要求的情况;
想要创建一个可以复用的类,该类可以与原接口的类协调工作;
在对象适配中,当要匹配数个子类的时候,对象适配器可以适配它们的父类接口。
适配器模式特点
类适配器:
使得Adapter可以重定义Adaptee的部分行为。因为Adaptee是Adapter的一个子类;
仅仅引入了一个对象,并不需要额外的指针间接得到Adaptee。
对象适配器:
允许一个Adapter与多个Adaptee同时工作。Adapter也可以一次给所有的Adaptee添加功能;
使得重定义Adaptee的行为比较困难。需要生成一个Adaptee的子类,然后使Adapter引入这个子类而不是引用Adaptee本身
Adapter模式主要应用于“希望复用一些现存的类,但是接口又与复用环境要求不一致的情况”,在遗留代码复用、类库迁移等方面非常有用。
GoF定义了两种Adapter模式的实现结构:对象适配器和类适配器。但类适配器采用“多继承”的实现方式,带来了不良的高耦合,所以一般不推荐使用。对象适配器采用:对象组合“的方式,更符合松耦合精神。
Adapter模式可以实现的非常灵活,不必拘泥于GoF23中定义的两种结构。例如,完全可以将Adapter模式中“现存对象”作为新的接口方法参数,来达到适配的目的。
Adapter模式本身要求我们尽可能地使用“面向接口的编程”风格,这样才能在后期很方便地适配。
目标角色:
public interface Target{
//目标角色有自己的方法
public void request();
}
目标角色的实现类:
public class ConcreteTarget implements Target{
public void request(){
System.out.println("我是目标角色!");
}
}
源角色
public class Adaptee{
//原有的业务逻辑
public void doSomething(){
System.out.println("我是源角色!");
}
}
适配器角色
public class Adapter extends Adaptee implements Target{
public void request(){
doSomething();
}
}
场景类:
public class Client{
public static void main(String[] args) {
//原有的业务逻辑
Target target1 = new ConcreteTarget();
target1.request();
//增加了适配器角色后的业务逻辑
Target target2 = new Adapter();
target2.request();
}
}
当有动机修改一个已经投产中的接口时,适配器模式可能是最适合的模式。
适配器模式最好在详细设计阶段不要考虑它,它不是为了解决还处在开发阶段的问题,而是解决正在服役的项目问题。
抽象不应该依赖于实现细节,实现细节应该依赖于抽象。
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
Prototype(抽象原型类):**它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
ConcretePrototype(具体原型类):**它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
//原型模式通用源码
public class PrototypeClass implements Cloneable {
//重写父类Object方法
@Override
public PrototypeClass clone() {
PrototypeClass prototypeClass = null;
try{
prototypeClass = (PrototypeClass) super.clone();
}catch(CloneNotSupportedException e){
//异常处理
}
}
}
对象在拷贝时构造函数不会被执行
浅拷贝——只拷贝对象地址,深拷贝——拷贝对象的所有数据
要使用clone方法,类的成员变量上不要增加final关键字