说明
每个设计模式都配有实例,实例使用java实现,对于实例部分嫌弃篇幅大的可以跳过,今天只写第一部分,后续的会慢慢补充。
一、适配器模式
适配器大家都很熟悉,可以把对适配器模式的理解代入适配器中就很容易理解记忆了。到底什么是适配器模式呢,就是把一个接口转换成另外一个接口。
例子:
1.有一套电视遥控器接口,接口包含startTV开电视、stopTV关电视、nextShow下一个节目、prevShow上一个节目;
2.但是客户想通过键盘控制电视,要求ctrl+s用来开电视,ctrl+x用来关电视,ctrl+n打开下一个节目、ctrl+p打开上一个节目;
3.客户调用的的是键盘接口,而提供的服务接口是电视遥控器的接口,为了是客户能无缝接入,实现一个适配器,适配器对客户展现键盘接口,键盘接口内部实际调用了遥控器接口。
电视控制接口:
public interface TVControl {
void startTV();
void stopTV();
void nextShow();
void prevShow();
}
电视远程遥控器控制实现:
import java.util.Map;
public class TVRemoteControl implements TVControl {
private Map<Integer, Object> showMap;
private int nowShowNumber = 0;
private int totalShowNumber;
public TVRemoteControl(Map<Integer, Object> showMap) {
this.showMap = showMap;
totalShowNumber = showMap.size();
}
@Override
public void startTV() {
System.out.print("TV has been started.");
}
@Override
public void stopTV() {
System.out.print("TV has been stop.");
}
@Override
public void nextShow() {
nowShowNumber++;
if (nowShowNumber >= totalShowNumber) {
nowShowNumber = 0;
}
System.out.print("see the next show, the show number is " + nowShowNumber);
System.out.print("the show is " + showMap.get(nowShowNumber).toString());
}
@Override
public void prevShow() {
nowShowNumber--;
if (nowShowNumber < 0) {
nowShowNumber = totalShowNumber - 1;
}
System.out.print("see the previous show, the show number is " + nowShowNumber);
System.out.print("the show is " + showMap.get(nowShowNumber).toString());
}
}
键盘控制接口:
public interface KeyboardControl {
void ctrlsButton();
void ctrlxButton();
void ctrlnButton();
void ctrlpButton();
}
键盘控制电视遥控适配器:
public class KeyboardTVControlAdapt implements KeyboardControl {
private TVControl control;
public KeyboardTVControlAdapt(TVControl control) {
this.control = control;
}
@Override
public void ctrlsButton() {
control.startTV();
}
@Override
public void ctrlxButton() {
control.stopTV();
}
@Override
public void ctrlnButton() {
control.nextShow();
}
@Override
public void ctrlpButton() {
control.prevShow();
}
}
就是这样一个简单的适配器,将键盘和遥控器接口完美结合起来,无需针对键盘接口实现另一套重复的控制逻辑,并且遥控器接口变更时可以在适配器中做对应的修改,对应客户的键盘接口来说是无感知的。
好处:
1.通过转换,把不兼容的接口变成兼容,对客户实现解耦;
2.如果对提供的接口进行了修改,可以通过适配器模式,提供给客户的适配器接口不变更,实际内部接口的变更不影响旧接口客户的使用。
福利:
还有一个模式叫做外观模式,和适配器模式非常类似,对于外观模式:如果有庞大接口体系,使接口的调用过于复杂可以通过外部模式把接口调用封装成一个更高层的接口,使接口使用更加容易。简单点来说就是根据客户需求把需要多个接口调用的逻辑弄成一个统一的接口,省的客户说太复杂喷人。
二、策略模式
对于策略模式的理解需要明白组合和封装变化这两概念,在写代码的时候要将可能会变化的逻辑独立成接口,并且根据接口实现所需的各种变化逻辑的类,在使用此部分逻辑时只需根据接口实例化对应所需的类就好了。简单来说就是针对可变的部分封装起来,对于使用的客户来说这部分的变化是独立于使用者的。
例子:
1.同样以电视为例子,首先所有电视都有都可以播放节目(palyShow),可以控制电视如开关机、换台等(controlTV);
2.可以肯定的是播放电视节目这个功能对于电视是不会变的,但是发展到现在电视的控制确变得多种多样,最早款式的电视控制只能通过电视上的按钮进行控制,现在发展到了遥控器、手机、手势、声音控制等等。多种多样的控制方式甚至多种控制方式同时支持,应该将电视机的控制行为独立出来以适配各类控制方式;
电视控制接口:
/**
* 将电视的控制行为独立成接口
*/
public interface TVControlBehavior {
void controlTV();
}
按键控制电视类:
public class ButtonTVControl implements TVControlBehavior{
@Override
public void controlTV() {
System.out.println("Can use button to control TV");
}
}
遥控器控制电视类:
public class RemoteTVControl implements TVControlBehavior{
@Override
public void controlTV() {
System.out.println("Can use 'TV remote control device' to control TV");
}
}
手机控制电视类:
public class PhoneTVControl implements TVControlBehavior{
@Override
public void controlTV() {
System.out.println("Can use phone to control TV");
}
}
电视基类:
public class BaseTV {
//通过传入的控制行为随意的定制或者改变电视机的控制方式
private TVControlBehavior controlBehavior;
public BaseTV(TVControlBehavior controlBehavior) {
this.controlBehavior = controlBehavior;
}
/**
* 通过调用控制行为的接口进行电视控制
*/
public void controlTV() {
controlBehavior.controlTV();
}
public void playShow() {
System.out.print("Start playing TV shows");
}
/**
* 可动态的随意改变电视控制方式
* @param controlBehavior
*/
public void setControlBehavior(TVControlBehavior controlBehavior) {
this.controlBehavior = controlBehavior;
}
}
现在可以通过继承电视基类,根据不同电视品牌和类型随意的选择电视的控制方式,甚至可以在功能支持的情况下对电视控制方式进行切换。
好处:
1.将变化部分与不变部分解耦,可变部分在后续的系统需求有非常大的可能会出现新的特性或者较大的逻辑变更,如果我们不注意此部分将可变的逻辑代码死死的绑定到整个系统代码中,只要这一部分一出现变动这将是灾难,相信有实际工作经验的程序员都有这样的惨痛经历。各种重复的寻找引用、修改、测试甚至出现遗漏和引入新的bug(变动越大引入bug的可能性越大);
2.减少重复的代码,对可变逻辑接口的相同行为只要使用同一类型的实例就好;
3.甚至可以在运行时根据需要动态的调整方法的运行逻辑(动态的改变接口实例的引用);
三、装饰者模式
装饰者模式在可以从字面上理解,对现有对象包装上外衣来扩展对象的新的功能,是区别于继承另外一种扩展功能的选择(如java的BufferedInputStream)。
通常会有一直以为装饰者模式能做的事继承也能做,那为什么还要选择使用装饰者。继承是静态的继承父类的所有功能但是在弹性方面有所不如,比如有个human的基类,现在需要对这个human进行易容,使用继承就需要为每一种容妆封装一个类,充斥者大量的生硬的重复和各种各样的类,使程序变的无比复杂。如果使用装饰者呢,只需要对每个基础易容进行封装,然后可以通过基础易容组合成所需的容妆,减少了类的数量增加了弹性。
例子:
1.使用human易容的例子,human基类有disguise易容这个功能;
2.我们要对disguise易容的基础类型进行抽象,基础类有男、女、年龄阶段等,再通过基础类型进行扩展男的是不是有小男孩、老头,对应女的同样可以小女孩,老婆婆。
human基类:
public interface Human {
String disguise();
}
年龄阶段:
public class Age implements Human {
public String disguise() {
return "年龄阶段:";
}
}
中年:
public class MiddleAge extend Age {
public String disguise() {
return super.disguise() + "中年";
}
}
老年:
public class OldAge extend Age {
public String disguise() {
return super.disguise() + "老年";
}
}
男人:
public class MaleHuman implements Human {
private Human human;
public MaleHuman(Human human) {
this.human = human;
}
public String disguise() {
return human.disguise() + "男人";
}
}
女人:
public class FemaleHuman implements Human {
private Human human;
public FemaleHuman(Human human) {
this.human = human;
}
public String disguise() {
return human.disguise() + "女人";
}
}
这样就好了,男人和女人封装好了,那如果我要初始化一个老婆婆和一个中年男人对象怎么做呢:
import org.junit.Test;
public class HumanTest {
//实例化中年男士对象
@Test
public void middleMan() {
Human middle = new MiddleAge();
Human middleMan = new MaleHuman(middle);
System.out.print(middleMan.disguise());
}
//实例化老婆婆对象
@Test
public void oldWoman() {
Human old = new OldAge ();
Human oldWoman = new FemaleHuman(old);
System.out.print(oldWoman.disguise());
}
}
好处:
1.通过装饰者的组合和委托概念,能拥有类似继承的功能进行代码复用,同时相对继承又降低了复杂度,增加了弹性;
2.相对于继承的在编译时子类静态获取父类的功能,装饰者模式可以在运行时动态的组合进行功能的扩展和代码复用;
3.对于在超类设计时遗漏的功能在不修改超类代码的情况下附加到新的对象中。
缺点:
1.同继承一样,不合理的使用也会照成类爆炸,增加程序的复杂度。
对比:
初看装饰者模式和策略模式很类似,都是通过组合来达到其效果,但是仔细就比就会发现其不同之处,其通过组合而达到的目的是不同的:
策略模式是通过对可能可变的同一接口的不同行为做出多种实现,然后再使用的时候动态选择对应的行为接口。
装饰者模式是针对同一类的行为接口上装饰上额外的特性同时保留原有的特性。
四、工厂模式java作为面向对象编程的语言,不可避免的需要进行创建对象,一个程序中有着大量的对象,通常我们都知道要使用对象时new一个出来就好了,但是作为java有个设计原则面对接口编程而不是面对实现编程,如果代码充斥着大量的类实例,当类特性一旦变化或者有新的类型加入,和类实例耦合的代码都需要进行修改。
为了避免如上情况,针对对象的创建引入了工厂模式,工厂模式是指提供了一个创建对象的方法,然后具体的工厂子类来定义具体如何进行类对象(拥有共同父类的对象)的创建。
例子:
1.还是用电视做例子,有个生产电视工厂接口,定义了如何生产电视的方法manufactureTV;
2.但是对于生产出口给美国的电视和中国的电视在电源、规格和材料等上会有区别,同时电视也有不同的型号比如曲面屏电视、LED电视等。
3.这样就有两个不同的工厂分别用于生产美国电视和中国电视。
生产电视的工厂接口:
public interface TVFactory {
/**
* 制造电视
* @param type 用于指定生产哪种类型的电视比例OLED、LED等
*/
TV manufactureTV(String type);
}
电视接口:
public interface TV {
void power();
void materials();
}
美国LED电视:
public class AmericaLEDTV implements TV {
@Override
public void power() {
System.out.print("America power!");
}
@Override
public void materials() {
System.out.print("LED screen!");
}
}
美国OLED电视:
public class AmericaOLEDTV implements TV {
@Override
public void power() {
System.out.print("America power!");
}
@Override
public void materials() {
System.out.print("OLED screen!");
}
}
中国LED电视:
public class ChinaLEDTV implements TV {
@Override
public void power() {
System.out.print("China power!");
}
@Override
public void materials() {
System.out.print("LED screen!");
}
}
中国OLED电视:
public class ChinaOLEDTV implements TV {
@Override
public void power() {
System.out.print("China power!");
}
@Override
public void materials() {
System.out.print("OLED screen!");
}
}
美国电视工厂:
public class AmericaTVFactory implements TVFactory {
@Override
public TV manufactureTV(String type) {
TV tv = null;
switch (type) {
case "LED":
tv = new AmericaLEDTV();
break;
case "OLED":
tv = new AmericaOLEDTV();
break;
}
return tv;
}
}
中国电视工厂:
public class ChinaTVFactory implements TVFactory {
@Override
public TV manufactureTV(String type) {
TV tv = null;
switch (type) {
case "LED":
tv = new ChinaLEDTV();
break;
case "OLED":
tv = new ChinaOLEDTV();
break;
}
return tv;
}
}
现在中国电视工厂和美国电视工厂都封装好了,需要什么样的电视只需要实例化对应的工厂类然后调用制造电视方法就好了。
好处:
1.将对象创建封装到一个方法中,实现代码的复用,方便对象的管理容易维护;
2.使用接口实例化对象,易于扩展;
五、单例模式:
单例模式不用多说了,就是在一个程序里面针对这个类只有一个可供全局使用的实例对象,为啥要用单例呢,大家都知道每个实例对象都要占用堆内存的,并且创建对象是个比较耗时的工作,所有在没有必要每次都创建实例对象的情况下用单例模式是比较好的选择。比如上面的电视工厂类,整个程序只需要一个实例对象就好了。
例子:
1.电视工厂的单例模式
电视工厂单例:
public class ChinaTVFactory implements TVFactory {
/**
* volatile关键字声明保证多线程操作时变量的一致性问题,详情可以度娘
*/
private volatile static ChinaTVFactory instance;
/**
* 私有化构造方法,避免其他地方对这个工厂类进行实例化
*/
private ChinaTVFactory() {
}
public static ChinaTVFactory getInstance() {
if (instance == null) {
/**
* 为啥要在这边加锁呢不在方法上面加呢,首先加锁是为了防止多线程操作导致多次实例化对象,
* 在这边加是为了提高性能,只有在实例没有被实例化的情况下才会进入加锁逻辑。
*/
synchronized (ChinaTVFactory.class) {
if (instance == null) {
instance = new ChinaTVFactory();
}
}
}
return instance;
}
@Override
public TV manufactureTV(String type) {
TV tv = null;
switch (type) {
case "LED":
tv = new ChinaLEDTV();
break;
case "OLED":
tv = new ChinaOLEDTV();
break;
}
return tv;
}
}
好处:
1.减少大量实例的堆内存占用,同时能附带的减轻垃圾回收压力;
六、观察者模式
观察者模式就是针对一个主题对象,当主题对象变动时,依赖此主题对象的众多对象都会受到更新通知。在编程事件中观察者模式的使用是非常常见,比如消息的发布-订阅、各类的回调监听函数等等,虽然它们的叫法不同但是都使用到了观察者模式这个设计原则。
例子:
1.我们假设一个非常简单针对一个消息队列的发布-订阅模型;
2.有一个消息队列接口,其中包含接收消息、注册消费者、移除消费者、通知消费者消费消息方法;
3.定义一个消费者接口,包含一个接收到消息的回调方法,所有消费者都需要实现这个接口。
4.消息队列类实现消息队列接口,队列实例每收到一条消息都会触发通知消费者方法,将消息传递给消费者的接收消息回调方法并调用此方法。
主题接口:
public interface MsgObservable {
void removeConsumer(MsgConsumer consumer);
void registerConsumer(MsgConsumer consumer);
void notifyMsgReceived(String msg);
void receivedMsg(String msg);
}
消费者接口:
public interface MsgConsumer {
void msgArrivedCallBack(String msg);
}
队列主题类:
import java.util.Vector;
public class MsgQueue implements MsgObservable {
private Vector<MsgConsumer> obs;
public MsgQueue() {
obs = new Vector<>();
}
@Override
public synchronized void removeConsumer(MsgConsumer consumer) {
obs.removeElement(consumer);
}
@Override
public synchronized void registerConsumer(MsgConsumer consumer) {
if (consumer == null)
throw new NullPointerException();
if (!obs.contains(consumer)) {
obs.addElement(consumer);
}
}
@Override
public void notifyMsgReceived(String msg) {
Object[] arrLocal;
synchronized (this) {
arrLocal = obs.toArray();
}
for (int i = arrLocal.length-1; i>=0; i--)
((MsgConsumer)arrLocal[i]).msgArrivedCallBack(msg);
}
/**
* 队列接收到一条消息时,将消息通知给所有订阅这个队列的消费者
* @param msg
*/
@Override
public void receivedMsg(String msg) {
System.out.print("queue received a msg: " + msg);
notifyMsgReceived(msg);
}
}
消费者类:
public class DefaultConsumer implements MsgConsumer {
@Override
public void msgArrivedCallBack(String msg) {
System.out.println("I received a msg: " + msg);
}
}
好处:
1.实现主题对象和依赖对象解耦,主题对象的变更细节依赖对象完全不需要知道,只需等待通知就好。避免了主题对象和依赖对象重度耦合的复杂性和不可扩展性问题;
七、命令模式
命令模式:将执行请求封装成对象,也就是命令对象,程序运行时可以根据不同的请求执行不同命令对象。
例子:
1.设计一个空调遥控器,有开关空调,调节温度的功能。
2.封装一个命令接口,空调接收到遥控器命令后具体的执行内容在命令接口对应的实现对象中执行。
空调
public class AirConditioner {
public void on() {
System.out.println("turn on air conditioner");
}
public void regulate() {
System.out.println("regulate air conditioner temperature");
}
public void off() {
System.out.println("turn off air Conditioner");
}
}
命令接口
public interface Command {
void execute();
}
开空调功能
public class AirConditionerOnCmd implements Command{
private AirConditioner airConditioner;
public AirConditionerOnCmd(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
}
@Override
public void execute() {
airConditioner.on();
}
}
关空调功能
public class AirConditionerOffCmd implements Command{
private AirConditioner airConditioner;
public AirConditionerOffCmd(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
}
@Override
public void execute() {
airConditioner.off();
}
}
调节空调温度功能
public class AirConditionerRegulateCmd implements Command{
private AirConditioner airConditioner;
public AirConditionerRegulateCmd(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
}
@Override
public void execute() {
airConditioner.regulate();
}
}
遥控器
public class RemoteControl {
private Map<String, Command> commandMap;
public RemoteControl() {
commandMap = new HashMap<>();
}
public void setCommand(String cmdType, Command cmd) {
commandMap.put(cmdType, cmd);
}
public void pressButton(String cmdType) {
commandMap.get(cmdType).execute();
}
}
好处:
1.请求方和具体的执行方实现解耦,请求方不需要知道具体如何执行,只需下命令给执行方就行。