面向对象
文章目录
- 面向对象
- 04 类
- 封装
- 接口 抽象类
- 05 消息,实例化,静态变量方法
- 消息
- 动/静态类型语言
- 对象创建
- 类及实例具有下面特征
- 对象数组的创建
- 静态数据成员
- 构造函数
- 06_0 继承
- 继承是向下传递的
- JAVA为什么不支持多重继承
- 继承的形式
- 特殊化继承
- 替换原则
- 规范化继承
- 构造继承
- 泛化继承
- 扩展继承
- 限制继承
- 变体继承
- 合并继承
- 06_1 多态,动态静态,替换
- 静态类,动态类
- 重写
- 向下造型
- 多态变量
- 重载
- 重定义
- 重写和重载的区别
- 分配方案
- 06_2 多态
- 多态变量
- 多态的运行机制
- 方法的动态绑定过程
- 06_3 多重继承
- 多重继承(了解)
- 多重继承中存在的问题:名字冲突
- 消除二义性的方法
- 07 软件复用
- 继承和组合的比较
- 设计原则
- 开闭原则OCP: Open-Closed Principle
- 里氏代换原则LSP: Liskov Substitution Principle
- 依赖倒置原则DIP: Dependency Inversion Principle
- 通过构造函数传递依赖对象
- 通过setter方法传递依赖对象
- 接口声明实现依赖对象
- 接口隔离原则ISP: Interface Segregation Principle
- 组合优先原则
- 单一职责原则(SRP)
- 针对接口编程
- 设计模式
- 简单工厂
- 工厂方法
- 单例模式
- 桥梁模式
- 装饰者模式
- 适配器模式
- 观察者模式
- 策略模式
本文章基于面向对象课程的PPT整理而成,只整理了部分重点内容,以供期末复习时使用,愿大家期末考出好成绩
04 类
封装
具体地,封装就是指利用抽象数据类型将数据和基于数据的操作封装在一起
数据被保护在抽象数据类型的内部
系统的其他部分只有通过包裹在数据外面的被授权的操作,才能够与这个抽象数据类型交流和交互。
封装特性把类内的数据保护得很严密,模块与模块间仅通过严格控制的界面进行交互
使它们之间耦合和交叉大大减少,从而降低了开发过程的复杂性,提高了效率和质量,减少了可能的错误
接口 抽象类
接口的结构和抽象类非常相似,它也具有数据成员与抽象方法,但它与抽象类有两点不同:
- 接口的数据成员必须初始化。
- 接口里的方法全部都是abstract。
不相同的地方:
- 抽象类是对类的抽象。而接口只是一个行为的规范或规定。
- 接口不具备任何继承的特点。它仅仅是承诺该完成的方法。
- 类可以实现多个接口,但只能继承一个父类。
- 抽象类可以包含方法的实现(非抽象方法),而接口中所有的方法都没有实现。
05 消息,实例化,静态变量方法
消息
考虑对象A向对象B发送消息,也可以看成对象A向对象B请求服务
- 对象A要明确知道对象B提供什么样的服务
- 根据请求服务的不同,对象A可能需要给对象B一些额外的信息,以使对象B明确知道如何处理该服务
- 对象B可将最终的执行结果以报告形式反馈回去
动/静态类型语言
动态类型语言(Dynamically Typed Language ):变量看作名称标识,类型和数值联系在一起
- 类型的检查是在运行时做的;
- 一般在变量使用之前不需要声明变量类型,而变量的类型通常是由被赋的值的类型决定。
- 优点是方便阅读,不需要写非常多的类型相关的代码
- 缺点是不方便调试,命名不规范时会造成读不懂,不利于理解等
静态类型语言(Statically Typed Language ):类型和变量联系在一起
- 类型的检查是在编译时做的.
- 在编译时,便需要确定类型的语言。即写程序时需要明确声明变量类型。
- 优点在于其结构非常规范,便于调试,方便类型安全;
- 缺点是为此需要写更多的类型相关代码,导致不便于阅读、不清晰明了
对象创建
类及实例具有下面特征
- 同一个类的不同实例具有相同的数据结构,承受的是同一方法集合所定义的操作,因而具有相同的行为
- 同一个类的不同实例可以持有不同的值,因而可以有不同的状态
- 实例的初始状态(初值)可以在实例化中确定
对象数组的创建
C++:结合
- 对象使用缺省构造函数来初始化。
- 数组由对象组成,每个对象则使用缺省(即无参数)构造函数来进行初始化
PlayingCard *cardArray[52];
Java:new仅创建数组。数组包含的对象必须独立创建。
PlayingCard cardArray[ ] = new PlayingCard[13];
for (int i = 0; i < 13; i++)cardArray[i] = new PlayingCard(Spade,i+1);
静态数据成员
利用static关键字修饰的数据成员称为静态数据成员/静态数据字段/静态变量/类成员,也称为静态成员(全局成员)。
利用static关键字修饰的成员方法为类方法/静态成员方法,类方法可以由类直接调用
静态成员不需要实例化就存在,而非静态成员是实例化后才有的成员,在没有实例化之前非静态成员并不存在。因此可以利用仅仅在某一时刻存在的对象访问普遍存在的对象;而不能用一个普遍存在的对象访问仅仅在某一时刻存在的对象。
对象本身不对共享字段初始化。内存管理器自动将共享数据初始化为某特定值,每个实例去测试该特定值。第一个进行测试的做初始化。
Java中,静态数据字段的初始化是在加载类时,执行静态块来完成。
静态数据字段(静态变量)与静态方法都是在类从磁盘加载至内存后被创建的,与类同时存在,同时消亡。
构造函数
缺省构造方法:没有参数的构造方法。如果程序员不提供任何来构造方法,则编译程序自动提供一个构造方法;只要程序员提供了一个构造方法,系统不再提供缺省构造方法
编译程序提供的缺省构造方法只做一件事:调用父类的缺省构造方法
06_0 继承
继承是向下传递的
子类所具有的数据和行为总是作为与其相关的父类的属性的扩展( extension)(即更大的集合)。
子类具有父类的所有属性以及其他属性。
继承总是向下传递的,因此一个类可以从它上面的多个超类中继承各种属性 。
派生类可以覆盖从基类继承来的行为。
JAVA为什么不支持多重继承
因为当一个类同时继承两个父类时,两个父类中有相同的功能,则子类对象调用该功能时,无法确定运行的是哪一个?
但是java支持多级继承。A继承B B继承C C继承D。
多级继承的出现,就有了继承体系。
体系中的顶层父类是通过不断向上抽取而来的。它里面定义的该体系最基本最共性内容的功能。
继承的形式
特殊化继承
很多情况下,都是为了特殊化才使用继承。
在这种形式下,新类是基类的一种特定类型,它能满足基类的所有规范。
用这种方式创建的总是子类型,并明显符合可替换性原则。
替换原则
子类可以扩展父类的功能,但不能改变父类原有的功能
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重写父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
规范化继承
基类中既有已实现的方法,也有只定义了方法接口、留待派生类去实现的方法。
派生类只是实现了那些定义在基类却又没有实现的方法。
派生类并没有重新定义已有的类型,而是去实现一个未完成的抽象规范。
构造继承
子类使用父类提供的行为,但并不是父类的子类型
子类通过继承父类就可以实现其需要的行为,只需要对方法名称、方法参数等进行修改
泛化继承
子类修改或改写父类的某些方法
扩展继承
子类只是往父类中添加新行为,并不修改从父类继承来的任何属性
由于父类的功能仍然可以使用,而且没有被修改,因此扩展继承并不违反可替换性原则,用这种方式构建的子类还是子类型
限制继承
如果子类的行为比父类的少或更严格,就出现了限制继承
变体继承
子类和父类之间都是对方的变体,可以任意选择两个之间的父子关系
例如,用为控制鼠标的代码与用来控制图形输入板的代码几乎完全相同。在概念上没有理由让一个类作为另外一个类的父类,因此可以选择任何一个类作为另外一个类的父类
合并继承
可以通过合并两个或者更多的抽象特性来形成新的抽象。
一个类可以继承自多个基类的能力被称为多重继承 。
06_1 多态,动态静态,替换
静态类,动态类
变量的静态类是指用于声明变量的类。静态类在编译时就确定下来,并且再也不会改变。类型由声明该变量时使用的类型决定。
变量的动态类指与变量所表示的当前数值相关的类。动态类在程序的执行过程中,当对变量赋新值时可以改变。类型由实际赋给该变量的对象决定。
如果变量的动态类和静态类的类型不一致,会出现所谓的多态。
多态的四种形式:重载,改写,多态变量,泛型
重写
在继承情况下,子类中定义了与其基类中方法具有相同名称、相同类型签名(相同返回值类型或兼容类型和相同参数类型)的方法,但重新编写了方法体
两种不同的关于改写/覆盖/重写的解释方式:
- 代替(replacement):在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行
- 改进(refinement):实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充。
向下造型
把指向子类对象的父类引用赋给子类引用叫向下转型(downcasting),要显式转型。
做出数值是否属于指定类的决定之后,通常下一步就是将这一数值的类型由父类转换为子类。
这一过程称为向下造型,或者反多态,因为这一操作所产生的效果恰好与多态赋值的效果相反。
class Person { public void fun1() { System.out.println(“1.Person{fun1()}”); } public void fun2() { System.out.println(“2.Person{fun2()}”); }
}
//继承了父类Person,自然继承了方法fun1、fun2
class Student extends Person { public void fun1() {//覆写了父类中的方法fun1()System.out.println(“3.Student{fun1()}”); } public void fun3() { System.out.println(“4.Student{fun3()}”); }
}
class TestJavaDemo2 { public static void main(String[] args) { Person p = new Person(); //父类对象由自身实例化 Student s = (Student)p; //将p对象向下转型 p.fun1(); p.fun2();
}
//Exception in thread "main" java.lang.ClassCastException: Person cannot be cast to Student编译无错,但是运行出错!class TestJavaDemo2 { public static void main(String[] args) { Person p = new Student(); //让父类知道有这么一个子类 Student s = (Student)p; //将p对象向下转型 p.fun1(); p.fun2(); p.fun3();//编译错误 ,用父类引用调用父类不存在的方法 }
}
多态变量
如果方法所执行的消息绑定是由最近赋值给变量的数值的类型来决定的,那么就称这个变量是多态的。
Java,Smalltalk等语言变量都可以是多态的。
Animal pet;
pet = new Dog();
pet.speak();pet = new bird();
pet.speak();
重载
同一个类定义中有多个同名的方法,但有不同的形参,而且每个方法有不同的方法体,调用时根据形参的个数、顺序和类型来决定调用的是哪个方法
重载是在编译时执行的,而改写是在运行时选择的。
对于 Java 语言,如果两个或者更多的方法具有相同的名称和相同的参数数目,则编译器将使用下面的算法来确定如何匹配
- 找到所有可能进行调用的方法,亦即,各个参数(实参)可以合法地赋值给各个参数类型(形参)的所有方法。如果找到一个在调用时可以完全匹配所使用的参数类型的方法,那么就执行这个方法。
- 对于第一步所产生的集合中的方法,两两进行比较,如果一个方法的所有参数类型都可以赋值给另外一个方法,那么就将第二个方法从集合中移走。重复以上操作,直至无法实现进一步的缩减为止。
- 如果只剩下一个方法,那么这个方法就非常明确了,调用这个方法即可。如果剩余的方法不止一个,那么调用就产生歧义了,此时编译器将报告错误。
重定义
当子类定义了一个与父类具有相同名称但类型签名不同的方法时,发生重定义。
类型签名的变化是重定义区别于改写/覆盖/重写的主要依据。
重写和重载的区别
方法的改写/覆盖是子类和父类之间的关系,而重载一般是同一类内部多个方法间的关系
方法的改写/覆盖一般是两个方法间的,而重载时可能有多个重载方法
改写/覆盖的方法有相同的方法名和形参表,而重载的方法只能有相同的方法名,不能有相同的形参表
改写/覆盖时区分方法的是根据被调用方法的对象,而重载是根据参数来决定调用的是哪个方法
用final修饰的方法是不能被子类覆盖的,只能被重载
重写方法不能使用比被重写的方法更严格的访问权限,重载可以有不同的访问修饰符
重写子类方法不能抛出比父类方法更多的异常。子类方法抛出的异常必须和父类方法抛出的异常相同,或者子类方法抛出的异常类是父类抛出的异常类的子类,重载可以抛出不同的异常
分配方案
最小静态空间分配:只分配基类所需的存储空间。
最大静态空间分配:无论基类还是派生类,都分配可用于所有合法数值的最大的存储空间。
动态内存分配:只分配用于保存一个指针所需的存储空间。在运行时分配对象所需的存储空间,同时将指针设为相应的合适值(地址)。
06_2 多态
多态变量
多态变量是指可以引用多种对象类型的变量。
这种变量在程序执行过程可以包含不同类型的数值。
对于动态类型语言,所有的变量都可能是多态的。对于静态类型语言,多态变量则是替换原则的具体表现。
多态的运行机制
Java多态机制是基于“方法绑定(binding)”,就是建立method call(方法调用)和method body(方法本体)的关联。如果绑定动作发生于程序执行前(由编译器和连接器完成),称为“先期绑定”。对于面向过程的语言它们没有其他选择,一定是先期绑定。比如C编译器只有一种method call,就是先期绑定。(C++有先期联编和后期联编)
当有多态的情况时,解决方案便是所谓的后期绑定(late binding):绑定动作将在执行期才根据对象型别而进行。后期绑定也被称为执行期绑定(run-time binding)或动态绑定(dynamic binding)。
Java的所有方法,只有final,static,private和构造方法是前期绑定,其他都使用后期绑定。 将方法声明为final型可以有效防止他人覆写该函数。但是或许更重要的是,这么做可以“关闭”动态绑定。或者说,这么做便是告诉编译器:动态绑定是不需要的。于是编译器可以产生效率较佳的程序代码。
方法的动态绑定过程
-
编译器检查对象的声明类型和方法名。假设我们调用x.f(args)方法,并且x已经被声明为C类的变量,那么编译器会列举出C类中所有的名称为f的方法和从C类的超类继承过来的f方法
-
接下来编译器检查方法调用中提供的参数类型。如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就确定调用这个方法( 重载解析)
-
当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。假设实际类型为D(C的子类),如果D类定义了f(args)那么该方法被调用,否则就在D的超类中搜寻方法f(args),依次类推
注:
- 第一步和第二步在编译时期完成,它们确定了可能的方法调用集合和具体的签名,但这只是静态分析的结果。
- 第三步在运行时期完成,它根据对象的实际类型确定最终调用哪个方法实现。这是动态绑定的核心,它补充了静态绑定的不足,允许程序在运行时表现出不同的行为,具体取决于对象的实际类型。
public class Bird{public void fly(Bird p) {System.out.println(“Bird fly with Bird”);}
}
public class Eagle extends Bird {public void fly(Bird p){System.out.println(“Eagle fly with Bird!”);}public void fly(Eagle e) {System.out.println(“Eagle fly with Eagle!”);}
}
Bird p1 = new Bird () ;
Bird p2 = new Eagle () ;
Eagle p3 = new Eagle () ;
p1.fly( p1 ) ;
p1.fly( p2 ) ;
p1.fly( p3 ) ;
p2.fly( p1 ) ;
p2.fly( p2 ) ;
p2.fly( p3 ) ;
p3.fly( p1 ) ;
p3.fly( p2 ) ;
p3.fly( p3 ) ;Bird fly with Bird
Bird fly with Bird
Bird fly with Bird
Eagle fly with Bird
Eagle fly with Bird
Eagle fly with Bird
Eagle fly with Bird
Eagle fly with Bird
Eagle fly with Eagle
class A {public String show(D obj){return ("A and D");}public String show(A obj){return ("A and A");}
}
class B extends A{public String show(B obj) {return ("B and B");}public String show(A obj) {return ("B and A");}
}
class C extends B...{}
class D extends B...{} A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println(a1.show(b)); ①System.out.println(a1.show(c)); ②System.out.println(a1.show(d)); ③System.out.println(a2.show(b)); ④System.out.println(a2.show(c)); ⑤System.out.println(a2.show(d)); ⑥System.out.println(b.show(b)); ⑦System.out.println(b.show(c)); ⑧System.out.println(b.show(d)); ⑨ ① A and A
② A and A
③ A and D
④ B and A
⑤ B and A
⑥ A and D
⑦ B and B
⑧ B and B
⑨ A and D
06_3 多重继承
多重继承(了解)
一个对象可以有两个或更多不同的父类,并可以继承每个父类的数据和行为。
派生的分类对每个父类仍然符合“是一个”规则,或“作为一个”关系。
多重继承中存在的问题:名字冲突
在多重继承下,若多个基类具有相同的成员名,可能造成对基类中该成员的访问出现不是唯一的情况,则称为对基类成员访问的二义性。
int main()
{DeviceNew device(0.7,3,false,10,250,80);cout<<“The weight of the device:”<<device.getWeight();device.showPower();device.showProperty();return 0;
}
class DeviceNew:public Device1,public Device2 {}
//Device1和Device2中都定义了showPower()方法
消除二义性的方法
- 使用相应的类名来标识
device.Device1::showPower();
- 在派生类中重定义有名称冲突的成员
07 软件复用
继承和组合的比较
继承描述的是一种一般性和特殊性的关系,使用继承可创建已存在类的特殊版本。
组合描述的是一种组成关系,使用组合可用已存在的类组装新的类。
动态与静态复用:
- 继承是在编译时刻静态定义的,即是静态复用,在编译后子类和父类的关系就已经确定了。
- 组合中类之间的关系是在运行时候才确定的,即在对象没有创建运行前,整体类是不会知道自己将持有特定接口下的哪个实现类。在扩展方面组合比继承更具有广泛性。
封装性:
- 继承中父类定义了子类的部分实现,而子类中又会重写这些实现,修改父类的实现,设计模式中认为这是一种破坏了父类的封装性的表现。这个结构导致结果是父类实现的任何变化,必然导致子类的改变。
- 组合有助于保持每个类被封装,并被集中在单个任务上(类设计的单一原则)。这样类的层次结构不会扩大,一般不会出现不可控的庞然大类。而类的继承就可能出现这些问题,所以一般编码规范都要求类的层次结构不要超过3层。组合是大型系统软件实现即插即用时的首选方式。
组 合 关 系 | 继 承 关 系 |
---|---|
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 | 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 |
优点:具有较好的可扩展性 | 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 |
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 | 缺点:不支持动态继承。在运行时,子类无法选择不同的父类 |
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 | 缺点:子类不能改变父类的接口 |
缺点:整体类不能自动获得和局部类同样的接口 | 优点:子类能自动继承父类的接口 |
缺点:创建整体类的对象时,需要创建所有局部类的对象 | 优点:创建子类的对象时,无须创建父类的对象 |
设计原则
开闭原则OCP: Open-Closed Principle
软件组成实体应该是对扩展开放的(可扩展的),但是对修改是关闭的。
可给系统定义一个一劳永逸,不再更改的抽象设计,此设计允许有无穷无尽的行为在实现层被实现。抽象层预见所有扩展。
class Part {protected double basePrice;private PricePolicy pricePolicy;public void setPricePolicy(PricePolicy policy) {pricePolicy = policy;}public void setPrice(double price) {basePrice = price;}public double getPrice() {if (pricePolicy == null)return basePrice;elsereturn pricePolicy.getPrice(basePrice);}
}class PricePolicy {public double getPrice(double basePrice) {return basePrice;}
}
class Sale extends PricePolicy {private double discount;public void setDiscount(double discount) {this.discount = discount;}public double getPrice(double basePrice) {return basePrice * discount;}
}public class TestOCP {public static void main(String[] args) {// TODO Auto-generated method stubPart p1 = new Memory();p1.setPrice(599);Part p2 = new Disk();p2.setPrice(499);Sale sale = new Sale();sale.setDiscount(0.75);p2.setPricePolicy(sale);Part[] com = { p1, p2 };System.out.println(totalprice(com));}
}
里氏代换原则LSP: Liskov Substitution Principle
一个软件如果使用的是一个父类的话,如果把该父类换成子类,它不能察觉出父类对象和子类对象的区别
凡是父类适用的地方子类也适用
继承只有满足里氏代换原则才是合理的
子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
依赖倒置原则DIP: Dependency Inversion Principle
抽象(抽象类或接口)不应该依赖于细节(具体实现类)
细节(具体实现类)应该依赖抽象
抽象:即抽象类或接口,两者是不能够实例化的。
细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化。
而依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。
通过构造函数传递依赖对象
public interface IDriver {//是司机就应该会驾驶汽车public void drive();
}
public class Driver implements IDriver{private ICar car;
//构造函数注入public Driver(ICar _car){this.car = _car;}//司机的主要职责就是驾驶汽车public void drive(){this.car.run();}
}
通过setter方法传递依赖对象
public interface IDriver {
//车辆型号public void setCar(ICar car);
//是司机就应该会驾驶汽车public void drive();
}
public class Driver implements IDriver{private ICar car;public void setCar(ICar car){this.car = car;}
//司机的主要职责就是驾驶汽车public void drive(){this.car.run();}
}public interface ICar {
//是汽车就应该能跑public void run();}
public class Benz implements ICar{public void run(){System.out.println("奔驰汽车开始运行...");}
}public class Client {public static void main(..) {IDriver zhangSan = new Driver();ICar benz = new Benz();
//张三开奔驰车zhangsan.setCar(benz);}
}
接口声明实现依赖对象
public interface ICar {
//是汽车就应该能跑public void run();}
public class Benz implements ICar{
//汽车肯定会跑public void run(){System.out.println("奔驰汽车开始运行...");}
}public interface IDriver {
//是司机就应该会驾驶汽车public void drive(ICar car);
}
public class Driver implements IDriver{
//司机的主要职责就是驾驶汽车public void drive(ICar car){car.run();}
}
public class Client {public static void main(String[] args) {IDriver zhangSan = new Driver();ICar benz = new Benz();
//张三开奔驰车zhangSan.drive(benz); }
}
司机类和奔驰车类之间是一个紧耦合的关系,其导致的结果就是系统的可维护性大大降低。
代码可读性降低,两个相似的类需要阅读两个文件。
系统稳定性降低,这里只是增加了一个车类就需要修改司机类,这不是稳定性,这是易变性。被依赖者的变更让依赖者来承担修改的成本。
接口隔离原则ISP: Interface Segregation Principle
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。
我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
使用多个专门的接口比使用单一的总接口好(客户端不应该依赖它不需要的接口)。
组合优先原则
当需要应对变化的时候,应该首先使用组合的方式,而不是继承
单一职责原则(SRP)
一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
针对接口编程
要针对接口编程不要针对实现编程
针对接口编程”的一些建议
变量、参数、返回值等应声明为抽象类
不要继承非抽象类
不要重载父类的非抽象方法
设计模式
简单工厂
/** 接口的定义,该接口可以通过简单工厂来创建*/
public interface Api { /** * 示意,具体的功能方法的定义 */ public void operation(String s);
}
/*** 接口的具体实现对象A . */
public class ImplA implements Api{ public void operation(String s) { //实现功能的代码,示意一下 System.out.println("ImplA s=="+s); }
} /** * 工厂类,用来创造Api对象 */
public class Factory { /** * 具体的创造Api对象的方法 * @param condition 示意,从外部传入的选择条件 ; @return 创造好的Api对象 */ public static Api createApi(int condition){ //根据某些条件去选择究竟创建哪一个具体的实现对象, 这些条件可以从外部传入,也可以从其它途径获取。如果只有一个实现,可省略条件。 Api api = null; if(condition == 1){ api = new ImplA(); }else if(condition == 2){ api = new ImplB(); } return api; }
} /** * 客户端,使用Api接口 */
public class Client { public static void main(String[] args) { //通过简单工厂来获取接口对象 Api api = Factory.createApi(1); api.operation("正在使用简单工厂"); }
}
缺点:由于是从客户端在调用工厂的时候,传入选择的参数,这就说明客户端必须知道每个参数的含义,也需要理解每个参数对应的功能处理。这就要求必须在一定程度上,向客户暴露一定的内部实现细节。
工厂方法
不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。
/* 工厂方法所创建的对象的接口*/
abstract class Product {//可以定义Product的属性和方法
}
/** * 具体的Product对象 */
public class ConcreteProduct extends Product {//实现Product要求的方法
}/*** 客户端使用Facotry对象的情况下,Factory的基本实现结构*/
abstract class Factory { public abstract Product factoryMethod();
} /**具体的创建器实现对象*/
class ConcreteFactory extends Factory { public Product factoryMethod() { return new ConcreteProduct(); }
} /*客户端程序*/
Factory factory;
factory = new ConcreteFactory();
//可通过配置文件实现
Product product;
product = factory.factoryMethod();
……
优点
- 封装了创建具体对象的工作
- 使得客户代码“针对接口编程”,保持对变化的“关闭”(产品)
缺点
- 虽然保证了工厂方法内的对修改关闭,但对于使用工厂方法的类,如果要更换另外一种产品,仍然需要修改实例化的具体工厂类
单例模式
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
// 懒汉式
public class Singleton {private static Singleton uniqueInstance;// other useful instance variables hereprivate Singleton() {}public static Singleton getInstance() {if (uniqueInstance == null) {uniqueInstance = new Singleton();}return uniqueInstance;}// other useful methods here
}// 饿汉式
public class Singleton {// 直接为该单例类创建一个实例对象 private static Singleton uniqueInstance = new Singleton();private Singleton() {}// 直接返回唯一的单例对象public static Singleton getInstance() {return uniqueInstance;}
}//双重检查加锁
public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getInstance() {if (uniqueInstance == null) {synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();}}}return uniqueInstance;}
}
//被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确地处理该变量。
桥梁模式
将抽象部分与它的实现部分分离,使它们都可以独立地变化
//抽象化角色类
public abstract class Abstraction {protected Implementor impl;public Abstraction(Implementor impl){this.impl = impl;}//示例方法public abstract void operation();
}//修正抽象化角色类
public class RefinedAbstraction extends Abstraction {public RefinedAbstraction(Implementor impl) {super(impl);}public void operation(){impl.operationImpl();}//其他的操作方法public void otherOperation(){ }
}//实现化角色类
public abstract class Implementor {/*** 示例方法,实现抽象部分需要的某些具体功能*/public abstract void operationImpl();
}
//具体实现化角色类
public class ConcreteImplementorA extends Implementor {public void operationImpl() {//具体操作}
}
public class ConcreteImplementorB extends Implementor {public void operationImpl() {//具体操作}
}
装饰者模式
抽象构件(Component)角色:组件对象的接口,可以给这些对象动态地添加职责
具体构件(Concrete Component)角色:实现组件对象接口,通常就是被装饰者装饰的原始对象,也就是可以给这个对象添加职责。
装饰者(Decorator)角色:所有装饰者的抽象父类,需要定义一个与组件接口一致的接口,并持有一个Component对象,其实就是持有一个被装饰的对象。
具体装饰者(Concrete Decorator)角色:实际的装饰者对象,实现具体要向被装饰对象添加的功能。
public abstract class Beverage {protected String description; public String getDescription() { return description; }public abstract double cost();
}
public class DarkRoast extends Beverage {public DarkRoast() {description = "Dark Roast Coffee";}public double cost() {return .99;}
}public abstract class CondimentDecorator extends Beverage {protected Beverage beverage;}public class Mocha extends CondimentDecorator {public Mocha(Beverage beverage) {this.beverage = beverage;}public String getDescription() {return beverage.getDescription() + ", Mocha";}public double cost() {return .20 + beverage.cost();}
}public class StarbuzzCoffee {public static void main(String args[]) {Beverage beverage = new Decaf();System.out.println(beverage.getDescription() + " $" + beverage.cost());Beverage beverage1 = new Decaf();beverage1 = new Milk(beverage1);beverage1 = new Mocha(beverage1);System.out.println(beverage1.getDescription() + " $" + beverage1.cost());Beverage beverage2 = new DarkRoast();beverage2 = new Mocha(beverage2);beverage2 = new Milk(beverage2);System.out.println(beverage2.getDescription() + " $" + beverage2.cost());}
}
继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能。(继承在扩展功能是静态的,必须在编译时就确定好,而使用装饰者可以在运行时决定,装饰者也建立在继承的基础之上的)
通过使用不同装饰类以及这些类的排列组合,可以实现不同的效果。
松耦合
符合开闭原则
适配器模式
将一个类的接口转换成客户希望的另外一个接口使得原来由于接口不兼容而不能一起工作的那些类可以一起工作
实现方式:两个类(接口)A,B,定义一个类C来实现B的接口,然后在其内部实现时,转调A类已经实现的功能,这样可以通过对象组合的方式,既复用了A的功能,同时又在接口上满足了B调用的要求
// 已存在的、具有特殊功能、但不符合我们既有的标准接口的类
class Adaptee {public void specificRequest() {System.out.println("被适配类具有 特殊功能...");}
}// 目标接口,或称为标准接口
interface Target {public void request();
}// 具体目标类,只提供普通功能
class ConcreteTarget implements Target {public void request() {System.out.println("普通类 具有 普通功能...");}
}// 适配器类,继承了被适配类,同时实现标准接口
class Adapter extends Adaptee implements Target{public void request() {super.specificRequest();}
}// 测试类
public class Client {public static void main(String[] args) {
// 使用普通功能类Target concreteTarget = new ConcreteTarget();concreteTarget.request();
// 使用特殊功能类,即适配类Target adapter = new Adapter();adapter.request();}
}
观察者模式
观察者模式把多个订阅者称为观察者:Observer;多个观察者观察的对象被称为目标:Subject。
一个目标可以有任意多个观察者对象,一旦目标的状态发生了改变,所有注册的观察者都会得到通知,然后各个观察者会对通知作出相应的响应,执行相应的业务功能处理,并使自己的状态和目标对象的状态保持一致。
import java.util.*;
public abstract class Subject {//定义一个观察者集合用于存储所有观察者对象protected ArrayList<Observer> observers= new ArrayList();//注册方法,用于向观察者集合中增加一个观察者public void attach(Observer observer) {observers.add(observer);}//注销方法,用于在观察者集合中删除一个观察者public void detach(Observer observer) {observers.remove(observer);}//声明抽象通知方法public abstract void notifyAllObservers();
}public class ConcreteSubject extends Subject {//实现通知方法public void notifyAllObservers() {//遍历观察者集合,调用每一个观察者的响应方法for(Object obs:observers) {((Observer)obs).update();}}
}public interface Observer {//声明响应方法public void update();
}public class ConcreteObserver implements Observer {//实现响应方法public void update() {//具体响应代码}
}//客户端代码:
Subject subject=new ConcreteSubject();
Observer observer=new ConcreteObserver();
subject.attach(observer);
subject.notifyAllObservers();
当修改目标对象的状态的时候,就会触发相应的通知,然后会循环调用所有注册的观察者对象的相应方法,其实就相当于联动调用这些观察者的方法。
这个联动还是动态的,可以通过注册和取消注册来控制观察者,因而可以在程序运行期间,通过动态地控制观察者,来变相地实现添加和删除某些功能处理,这些功能就是观察者在update的时候执行的功能。
目标对象和观察者对象的解耦,又保证了无论观察者发生怎样的变化,目标对象总是能够正确地联动过来。
策略模式
策略模式属于对象的行为模式。
其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。
策略模式使得算法可以在不影响到客户端的情况下发生变化。
public class Context {//持有一个具体策略的对象private Strategy strategy;/*** 构造函数,传入一个具体策略对象* @param strategy 具体策略对象 */public Context(Strategy strategy){this.strategy = strategy;}/*** 策略方法*/public void contextInterface(){strategy.strategyInterface();}
}//抽象策略类
public interface Strategy {
//* 策略方法public void strategyInterface();
}
//具体策略类
public class ConcreteStrategyA implements Strategy {public void strategyInterface() {//相关的业务}
}
public class ConcreteStrategyB implements Strategy {public void strategyInterface() {//相关的业务}
}
组合优先:尽量用组合,而不是继承
开-闭原则:增加新功能,要做到只增加新代码,而不改动老代码
封装可变性:把变化的代码从不变的代码中分离出来,限制变化的影响范围
针对接口编程而不是具体类(定义了策略接口)
实现添加和删除某些功能处理,这些功能就是观察者在update的时候执行的功能。
目标对象和观察者对象的解耦,又保证了无论观察者发生怎样的变化,目标对象总是能够正确地联动过来。