常用设计模式
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,避免一个全局使用的类频繁的创建和销毁,节省系统资源,提高程序效率。怎么创建唯一的实例?Java是这么创建实例的 Person p = new Person();但是这么创建会创建多个实例,所以我们必须把构造器设为私有,这样其他类就不能使用new来实例化一个类。
//单线程实现单例模式
class Singleton {
private static Singleton instance;
public Singleton() {};
public static Singleton getInstance (){
instance = new Singleton();
return instance;
}
}
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//判断两个实例s1 s2是否为同一个实例
System.out.println(s1 == s2);
}
}public class Singleton {
//定义一个属性,用来保存Singleton类对象的实例
private static Singleton instance;
//私有构造器,该类不能被外部类使用new方式实例化
private Singleton(){
}
//外部通过该方法获取Singleton类的唯一实例
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种实现方式并不是线程安全的,当有多个线程同时调用Singleton.getInstance()方法时会产生多个实例。
Java多线程程序,线程执行顺序是不确定的,所以在同时多个线程调用Singleton.getInstance()方法时,存在创建多个实例的可能,会引起程序执行错误。那我们该如何实现多线程下安全的创建一个唯一的实例呢?锁,加锁。在线程调用Singleton.getInstance()方法时,判断instance == null ? 是,加锁,其他线程这时只能等待这个线程释放锁,才能进入临界区。那如何加锁,可以使用synchronized。
public static Singleton getInstance() {
//synchronized加锁同步会降低效率,这里先判断是否为空
//不为空则不需要加锁,提高程序效率
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
单例模式优点
- 在内存中只有一个对象,节省内存空间。
- 避免频繁的创建销毁对象,可以提高性能。
- 避免对共享资源的多重占用。
- 可以全局访问。
适用场景
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
- 以及其他我没用过的所有要求只有一个对象的场景。
工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。
很多工厂都有一些相同的行为,比如汽车工厂。我们需要抽象这些相同的行为成接口,每个工厂都实现这个接口。
public interface IFactory {
public void createProduct();
}
生产相同的产品每个工厂所使用的方法可能不同,所以具体如何生产产品由具体工厂实现。
public class Factory implements IFactory {
@Override
public void createProduct() {
}
}
工厂模式两要点
-
工厂接口是工厂方法模式的核心,与调用者直接交互用来提供产品。
-
工厂实现决定如何实例化产品,是实现扩展的途径,需要有多少种产品,就需要有多少个具体的工厂实现。
适用场景
-
在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。
-
工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。
-
当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。
抽象工厂模式
为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。
抽象工厂是工厂模式的升级版,他用来创建一组相关或者相互依赖的对象。
工厂模式种,类的创建依赖工厂类,程序需要扩展时,我们必须创建新的工厂类。工厂类是用来生产产品的,那我们也可以把“工厂类当成我们要生产的产品”,所以抽象工厂就是“工厂的工厂”,即生产工厂的工厂。如:
//CPU工厂接口
public interface CPUFactory {
public void createCPU();
}
//IntelCPU工厂
public class IntelCPU implements CPUFactory {
@Override
public void createCPU() {
System.out.println("Intel CPU");
}
}
//AMDCPU工厂
public class AMDCPU implements CPUFactory {
@Override
public void createCPU() {
System.out.println("AMD CPU");
}
}
//创建抽象工厂类接口
public interface Provider {
public CPUFactory createCPUFactory();
}
public class InterCPUFactory implements Provider {
@Override
public CPUFactory createCPUFactory() {
return new InterCPU();
}
}
public class AMDCPUFactory implements Provider {
@Override
public CPUFactory createCPUFactory() {
return new AMDCPU();
}
}
public static void main(String[] args) {
//创建一个生产CPU工厂的工厂
Provider cpufactory = new InterCPUFactory();
//通过CPU工厂的工厂创建一个IntelCPU工厂
CPUFactory intelcpu = cpufactory.createCPUFactory();
//IntelCPU工厂生产intelCPU
intelcpu.createCPU();
}
抽象工厂的优点
抽象工厂模式除了具有工厂方法模式的优点外,最主要的优点就是可以在类的内部对产品族进行约束。所谓的产品族,一般或多或少的都存在一定的关联(例如不同厂商生产CPU)。
适用场景
一个继承体系中,如果存在着多个等级结构(即存在着多个抽象类),并且分属各个等级结构中的实现类之间存在着一定的关联或者约束,就可以使用抽象工厂模式。
建造者模式
将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,由于需求的变化,这个复杂对象的某些部分经常面临着剧烈的变化,一些基本部件不会变。所以需要将变与不变分离。与抽象工厂的区别:在建造者模式里,有个指导者(Director),由指导者来管理建造者,用户是与指导者联系的,指导者联系建造者最后得到产品。即建造者模式可以强制实行一种分步骤进行的建造过程。
建造者模式四要素
1.产品类Product:一般是一个较为复杂的对象,也就是说创建对象的过程比较复杂,一般会有比较多的代码量。
2.抽象建造者类Builder: 将建造的具体过程交与它的子类来实现,这样更容易扩展。
3.建造者类ConcreteBuilder: 组建产品;返回组建好的产品。
4.指导类Director: 负责调用适当的建造者来组建产品,指导类一般不与产品类发生依赖关系,与指导类直接交互的是建造者类。
举个例子:前面你创建了一个生产保时捷的工厂,生产一台保时捷911需要的主要部件都一样(引擎,轮子,方向盘…)和流程是不变的,变的是引擎,轮子,控制系统等等部件具体实现,这些部件的生产交由具体的builder去生产。
/抽象生产者
public interface Builder {
void buildPartA();
void buildPartB();
void buildPartC();
Product buildProduct();
}
//具体生产者
public class ConcreteBuilder implements Builder {
Product product;
@Override
public void buildPartA() {
}
@Override
public void buildPartB() {
}
@Override
public void buildPartC() {
}
@Override
public Product buildProduct() {
return product;
}
}
//产品由各个组件组成
public class Product {
//partA
//partB
//partC
}
/指导者,产品生产流程规范
public class Director {
Builder builder;
//由具体的生产者来生产产品
public Director(Builder builder) {
this.builder = builder;
}
//生产流程
public void buildProduct(){
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
}
}
public static void main(String[] args) {
//只需要关心具体建造者,无需关心产品内部构建流程。
//如果需要其他的复杂产品对象,只需要选择其他的建造者.
Builder builder = new ConcreteBuilder();
//把建造者注入指导者
Director director = new Director(builder);
//指导者负责流程把控
director.buildProduct();
// 建造者返回一个组合好的复杂产品对象
Product product = builder.buildProduct();
}
建造者模式优点
-
建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指导者类中对整体而言可以取得比较好的稳定性。
-
建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成。
适用场景
需要生成的对象具有复杂的内部结构;需要生成的对象内部属性本身相互依赖。
适配器模式
适配器模式是将一个类的接口转换成客户希望的另外一个接口,身边很多东西都是适用于适配器模式的,笔记本的电源(也叫电源适配器),是将220V的交流电转换为笔记本电脑所需要的12V(电流先忽略),笔记本电脑的各种接口,VGA转Hdml,USB-TypeA 转 USB-TypeC,亦或者你在香港买了个手机,充电器是你生活中没见过的三孔插座通过一个转换头转换为国内常用的插头,很多例子都能很形象的解释这个设计模式。适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。
UML角色
Source:需要被适配的类、接口、对象,即Datas。
Destination:需要得到的类,Source通过适配得到的类对象,也就是我们期待得到的借口。
Adapter:适配器类,协调Source和Destination,使两者能够协同工作。
适用场景
- 系统需要使用现有的类,但现有的类却不兼容。
- 需要建立一个可以重复使用的类,用于一些彼此关系不大的类,并易于扩展,以便于面对将来会出现的类。
- 需要一个统一的输出接口,但是输入类型却不可预知。
示例
手机充电需要将220V的交流电转化为手机锂电池需要的5V直流电,我们的demo就是写一个电源适配器,将 AC220v ——> DC5V,其实适配器模式可以简单的分为三类:类适配器模式、对象的适配器模式、接口的适配器模式。我们就以这三种模式来实现上述步骤。
类适配器模式
//Source类
public class AC220 {
public int output220V(){
int output = 220;
return output;
}
}
//目标类Destination,只需要定义方法,由适配器来转化:
public interface DC5 {
int output5V();
}
//Adapter类
public class PowerAdapter extends AC220 implements DC5 {
@Override
public int output5V() {
int output = output220V();
return (output / 44);
}
}
测试类:
//类适配器使用场景
private void initClassAdapter() {
DC5 dc5 = new com.demo.adapter.classadapter.PowerAdapter();
dc5.output5V();
}
因为java单继承的缘故,Destination类必须是接口,以便于Adapter去继承Source并实现Destination,完成适配的功能,但这样就导致了Adapter里暴露了Source类的方法,使用起来的成本就增加了。
对象适配器模式
对于同样的逻辑,我们在以对象适配器模式实现。我们保留AC220和DC5两个基本类,我们让Adapter持有Destination类的实例,然后再实现DC5,以这种持有对象的方式来实现适配器功能:
public class PowerAdapter implements DC5{
private AC220 mAC220;
public PowerAdapter(AC220 ac220){
this.mAC220 = ac220;
}
@Override
public int output5V() {
int output = 0;
if (mAC220 != null) {
output = mAC220.output220V() / 44;
}
return output;
}
}
测试类:
//对象适配器模式使用场景
private void initObjAdapter() {
com.demo.adapter.objadapter.PowerAdapter adapter = new com.demo.adapter.objadapter.PowerAdapter(new AC220());
adapter.output5V();
}
对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。再回想装饰者模式,装饰者是对Source的装饰,使用者毫无察觉到Source被装饰,也就是用法不变。而对于适配器模式用法还是有改变的。
接口适配器模式
对于接口适配器模式,我们就不用担着眼于220->5,我们的接口可以有更多的抽象方法,这一点在android开发中有很多影子,动画的适配器有很多接口,但我们只需要关心我们需要的回调方法(详见AnimatorListenerAdapter类)
我们把接口比作万能适配器:
public interface DCOutput {
int output5V();
int output9V();
int output12V();
int output24V();
}
然后我们要用的是5V的电压,所以关心5V的适配:
public class Power5VAdapter extends PowerAdapter {
public Power5VAdapter(AC220 ac220) {
super(ac220);
}
@Override
public int output5V() {
int output = 0;
if (mAC220 != null) {
output = mAC220.output220V() / 44;
}
return output;
}
}
但是我们必须存在一个中间适配器,用于实现默认的接口方法,以至于减少我们适配器的代码量,让代码更加清晰:
public abstract class PowerAdapter implements DCOutput{
protected AC220 mAC220;
public PowerAdapter(AC220 ac220){
this.mAC220 = ac220;
}
@Override
public int output5V() {
return mAC220.output220V();
}
@Override
public int output9V() {
return mAC220.output220V();
}
@Override
public int output12V() {
return mAC220.output220V();
}
@Override
public int output24V() {
return mAC220.output220V();
}
}
这样一来我们就只需要重写父类我们关心的方法了,当然我们有时候可以省略Power5VAdapter类,因为内部类可以实现我们的方法,就跟使用setOnClickOnLintener(new OnClickOnLintener(){…})一样。
测试类:
//接口适配器模式使用场景
private void initinterfaceAdapter() {
// 已经实现了子类
com.demo.adapter.interfaceadapter.Power5VAdapter power5VAdapter = new Power5VAdapter(new AC220());
power5VAdapter.output5V();
// 直接实现子类
com.demo.adapter.interfaceadapter.PowerAdapter powerAdapter = new PowerAdapter(new AC220()) {
@Override
public int output5V() {
int output = 0;
if (mAC220 != null) {
output = mAC220.output220V() / 44;
}
return output;
}
};
powerAdapter.output5V();
}
这样也实现了这个适配功能,而且可以说易于扩展。
装饰模式
对客户透明的方式动态地给一个对象附加上更多的责任,同时又不改变其结构。装饰模式可以在不使用创造更多子类的情况下,将对象的功能加以扩展。
- 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类。
- 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
- 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。
Java IO中就是典型的装饰器
//InputStream提供的基本方法(Component)
public abstract class InputStream implements Closeable {
}
//默认目标实现类(ConcreteComponent)
public class FileInputStream extends InputStream {
}
/*装饰实现类(FilterInputStream)一定是继承或实现原始接口(InputStream)的,内部有包含一个原始接口的超类(其实就是某个默认目标实现类)*/
//Decorator
public class FilterInputStream extends InputStream {
/**
* The input stream to be filtered.
*/
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
}
//具体装饰类(ConcreteDecorator)
public class BufferedInputStream extends FilterInputStream {
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
}
//具体装饰类(ConcreteDecorator)
public class DataInputStream extends FilterInputStream implements DataInput {
public DataInputStream(InputStream in) {
super(in);
}
}
装饰器模式优点
- 装饰类和被装饰类可以独立发展,不会相互耦合。
- 装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。就增加功能来说,装饰器模式相比生成子类更为灵活。
适用场景
- 扩展一个类的功能。
- 动态增加功能,动态撤销。
代理模式
静态代理
如果目标类中所有的方法,我们已经知道了,而我们需要对目标类中的某些方法进行增强。
public class Father {
//没有办法扩展,只能帮儿子找对象了,没办法帮表妹找对象。
//为了扩展,就需要将这里的Son改成Person,而person是一个接口,里面维护一个findLove方法的声明
private Son son;
public Father(Son son){
//拿到目标对象的引用
this.son = son;
}
//代理对象完成目标对象的工作
public void findLove(){
System.out.println("根据儿子的要求物色");
son.findLove();
System.out.println("物色完了,看到结果");
}
}
public interface Person {
public void findLove();
public void buy();
public void findJob();
}
public class Son implements Person{
//这个东西很繁杂
//于是程序员就将这个任务交给了别人,交给了自己的父亲
@Override
public void findLove(){
System.out.println("找对象");
}
@Override
public void buy() {
System.out.println("买东西");
}
@Override
public void findJob() {
System.out.println("找工作");
}
}
public class StaticProxyTest {
public static void main(String[] args) {
Son son = new Son();
Father father = new Father(son);
father.findLove();
}
}
优点:可以做到在不修改目标对象的前提下,对目标功能进行修改
缺点:因为代理类需要与目标类实现相同的接口,所以会有很多代理类,类太多,占用内存,增加额外的开销。而且,一旦接口增加方法,目标对象和代理对象都需要维护。
所以解决静态代理的缺点,需要使用动态代理。
动态代理
动态代理其实相比静态代理而言,而且在代理之前,目标类中的方法可以为未知的。动态代理实现方式一般都有两种,一种就是JDK提供的动态代理的方式,另一种就是CGLib提供的方式,两者在实现方式上有着本质的不同,前一种是通过动态实现相关的接口来达到增强的作用,后一种是通过继承目标类的方式来实现代理。
CGLib代理
实现CGLib不需要手写大量代码,导入asm包可以完成大部分功能。这里直接给出实例代码:
目标类:
public class Person{
public void findLove(){
System.out.println("肤白,貌美,气质佳");
}
}
增强业务逻辑类:(也是获得代理类的入口)
public class PersonInterceptor implements MethodInterceptor{
public Object getInstance(Class<?> clazz) throws Exception{
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Person.class);
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//业务的增强
System.out.println("我是媒婆:我要给你找对象,现在已经拿到你的需求了");
System.out.println("开始物色.......");
methodProxy.invokeSuper(o,objects);
System.out.println("物色完成");
return o;
}
}
测试类:
public class CGLibTest {
public static void main(String[] args) {
try{
Person obj = (Person)new PersonInterceptor().getInstance(Person.class);
obj.findLove();
}catch (Exception e){
e.printStackTrace();
}
}
}
JDK动态代理
JDK的动态代理,采用的是就是动态植入的方式。
public class XiaoMing implements Person{
public void findLove(){
System.out.println("三观合,颜值过得去,能聊得来,有独立的工作能力");
}
public void buy() {
System.out.println("买东西");
}
public void zufangzi() {
System.out.println("租房子");
}
public void findJob() {
System.out.println("找工作");
}
public void buyHouse(){
System.out.println("土豪了,要买房子");
}
}
public class JDKFindJob implements InvocationHandler{
//需要被代理的对象
private Person target;
public Object getInstance(Person target){
this.target = target;
Class<?> clazz=target.getClass();
return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
}
//为了JDK代理方法的第三个参数,需要实现InvocationHandler接口
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我是猎头,告诉我你的工作需求");
System.out.println("开始物色.......");
method.invoke(this.target,args );
System.out.println("物色完成");
return null;
}
}
测试类:
public class JDKProxyTest {
public static void main(String[] args) {
Person findJobObj = (Person)new JDKFindJob().getInstance(new XiaoMing());
findJobObj.findJob();
System.out.println(obj.getClass()); //输出:class com.sun.proxy.$Proxy0,表名生成的是一个代理类
}}
代理类所在包:java.lang.reflect.Proxy
JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h );
注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:
ClassLoader loader:指定当前目标对象使用类加载器,获取加载器的方法是固定的
Class<?>[] interfaces:目标对象实现的接口的类型,使用泛型方式确认类型
InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
- Strategy:策略接口,用来约束一系列具体的策略算法。Context使用这个接口来调用具体的策略,实现定义的策略。
- ConcreteStrategy:具体的策略实现,也就是具体的算法实现。
- Context:上下文,负责与具体的策略交互,通常上下文会持有一个真正的策略实现。
策略模式是把一个类中经常改变或者将来可能改变的部分提取出来作为一个接口,然后在类中包含这个对象的实例,这样类的实例在运行时就可以随意调用实现了这个接口的类的行为。
现在根据不同需求,计算两个数的四则运算( + - * /)
//策略定义算法的接口
public interface Strategy {
int calculate(int num1,int num2);
}
//具体算法,加法
public class OperationAdd implements Strategy {
@Override
public int calculate(int num1, int num2) {
return num1 + num2;
}
}
//具体算法,减法
public class OperationSubstract implements Strategy {
@Override
public int calculate(int num1, int num2) {
return num1 - num2;
}
}
//具体算法,乘法
public class OperationMultiply implements Strategy {
@Override
public int calculate(int num1, int num2) {
return num1 * num2;
}
}
//具体算法,除法
public class OperationDivide implements Strategy {
@Override
public int calculate (int num1, int num2){
int res = 0;
try {
res = num1 / num2;
}catch (Exception e) {
e.printStackTrace();
}
return res;
}
}
//上下文
public class Context {
//持有一个具体策略对象
private Strategy strategy;
//传入一个具体策略对象
public Context(Strategy strategy) {
this.strategy =strategy;
}
public int calculate(int num1,int num2){
//调用具体策略对象进行算法运算
return strategy.calculate(num1,num2);
}
}
public static void main(String[] args) {
//计算 1 + 1
Context context = new Context(new OperationAdd());
System.out.println("1 + 1 = " + context.calculate(1,1));
//计算 1 - 1
context = new Context(new OperationSubstract());
System.out.println("1 - 1 = " +context.calculate(1,1));
}
策略模式优点
- 算法可以自由切换。
- 避免使用多重条件判断。
- 扩展性良好。
策略模式缺点
- 策略类会增多。
- 所有策略类都需要对外暴露。
适用场景
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 一个类定义了多种行为, 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
观察者模式
对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
- 抽象主题(Subject)角色:把所有对观察者对象的引用保存在一个集合中,每个抽象主题角色都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。
- 抽象观察者(Observer)角色:为所有具体的观察者定义一个接口,在得到主题的通知时更新自己。
- 具体主题(ConcreteSubject)角色:在具体主题内部状态改变时,给所有登记过的观察者发出通知。具体主题角色通常用一个子类实现。
- 具体观察者(ConcreteObserver)角色:该角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。通常用一个子类实现。如果需要,具体观察者角色可以保存一个指向具体主题角色的引用。
控件按钮、报警器等都是观察者模式。
public interface Subject {
//添加观察者
void attach(Observer o);
//删除观察者
void detach(Observer o);
//通知观察者
void notifyObservers();
//发生某事
void doSomeThings()
}
//观察者
public interface Observer {
void update();
}
public class ConcreteSubject implements Subject {
ArrayList<Observer> observers = new ArrayList<>();
@Override
public void attach(Observer o) {
observers.add(o);
}
@Override
public void detach(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer o : observers) {
o.update();
}
}
public void doSomeThings(){
//doSomeThings
notifyObservers();//通知观察者
}
}
//具体观察者
public class ConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("我观察到subject发生了某事");
}
}
public static void main(String[] args) {
Subject cs = new ConcreteSubject();
//添加观察者
cs.attach(new ConcreteObserver());
//subject发生了某事,通知观察者
cs.doSomeThings();
}
观察者模式优点
- 观察者和被观察者是抽象耦合的。
- 建立一套触发机制。
观察者模式缺点
- 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
适用场景
4. 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
5. 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
6. 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。
模板方法模式
定义一个操作中算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变算法的结构即可重定义该算法中的某些特定步骤。
模板方法模式是编程中经常用到的模式,其非常简单,AbstractClass叫抽象模板,其方法分为3类:
- 抽象方法:父类中只声明但不加以实现,而是定义好规范,然后由它的子类去实现。
- 模版方法:由抽象类声明并加以实现。一般来说,模版方法调用抽象方法来完成主要的逻辑功能,并且,模版方法大多会定义为final类型,指明主要的逻辑功能在子类中不能被重写。
- 钩子方法:由抽象类声明并加以实现。但是子类可以去扩展,子类可以通过扩展钩子方法来影响模版方法的逻辑。
实现类用来实现细节。抽象类中的模版方法正是通过实现类扩展的方法来完成业务逻辑。
现在要实现一个对无序数组从小到大排序并打印数组的类。排序算法有很多种,打印功能固定的。定义一个AbstractClass定义抽象排序方法由子类去实现;模板类实现打印方法。
//抽象模板类
public abstract class AbstractSort {
public abstract void sort(int[] array);
//防止子类覆盖使用final修饰
public final void printArray(int[] array) {
sort(array);
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}
//具体实现类
public class QuickSort extends AbstractSort {
@Override
public void sort(int[] array) {
//使用快排算法实现
}
}
public class MergeSort extends AbstractSort {
@Override
public void sort(int[] array) {
//使用归并排序算法实现
}
}
public static void main(String[] args) {
int[] arr = {3,5,2,45,243,341,111,543,24};
//AbstractSort s = new MergeSort();
AbstractSort s = new QuickSort();
s.printArray(arr);
}
模板方法模式优点
- 容易扩展。一般来说,抽象类中的模版方法是不易反生改变的部分,而抽象方法是容易反生变化的部分,因此通过增加实现类一般可以很容易实现功能的扩展,符合开闭原则。
- 便于维护。对于模版方法模式来说,正是由于他们的主要逻辑相同,才使用了模版方法。
适用场景
在多个子类拥有相同的方法,并且这些方法逻辑相同时,可以考虑使用模版方法模式。在程序的主框架相同,细节不同的场合下,也比较适合使用这种模式。