Java程序员必备,史上最详细常用的八种设计模式讲解丨建议收藏
微信公众号:慕容千语的架构笔记。欢迎关注一起进步。
一、Proxy代理模式
1. 什么是代理模式
抽象点说是一个类代表另一个类的功能,或者说一个对象为另一个对象提供一个代理或者占位符以控制对这个对象的访问。同样我也会举例子来说明,这里我举一个买车票的例子。通常我们我们买车票需要去车站买,但是这样会很麻烦,可能要坐很久的车去车站,然后在排队买票。但是如果我们去一个卖车票的代理点买车票可能就会省去这么多的事情。这样车票售卖处就代理你购买车票。
2. 代理模式结构
3. 应用
下面我们就用具体的代码来实现上面这个买车票的静态代理。
首先创建一个售票服务的接口,它有售票咨询和退票的业务可以供客户选择。
public interface TicketService {
//售票
void sellTicket();
//咨询
void Consultation();
//退票
void ReturnTicket();
}
然后创建一个售票服务接口实现类,就好比是车站。
public class Station implements TicketService {
@Override
public void sellTicket() {
System.out.println("售票");
}
@Override
public void Consultation() {
System.out.println("咨询");
}
@Override
public void ReturnTicket() {
System.out.println("退票");
}
}
然后创建一个代理售票点
public class StationProxy implements TicketService {
private Station station;
public StationProxy(Station station){
this.station = station;
}
@Override
public void sellTicket() {
System.out.println("欢迎使用车票代售点进行购票,每张票将会收取5元手续费!");
station.sellTicket();
System.out.println("欢迎下次光临");
}
@Override
public void Consultation() {
System.out.println("欢迎咨询,咨询不收取费用");
station.Consultation();
System.out.println("欢迎下次光临");
}
@Override
public void ReturnTicket() {
System.out.println("欢迎使用车票代售点进行退票,每张票将会收取5元手续费!");
station.ReturnTicket();
System.out.println("欢迎下次光临");
}
}
创建购买车票的角色,去代理点完成购买车票的需求
public class ProxyTest {
public static void main(String[] args) {
Station station = new Station();
StationProxy stationProxy = new StationProxy(station);
stationProxy.sellTicket();
}
}
可以看到这个购买车票的客户是去的车票代理点,直接购买车票,那代理点能否帮助他正常购买到车票呢?请看下面的执行结果
从结果看到售票代理点成功帮助客户购买到车票,节省了客户去车站排队等待的时间和精力。代理模式有点像是委派模式中的中介,前面的文章也提到过静态代理和策略模式是委派模式的一种组合。那当然除了静态代理还有动态代理和CGLIB代理,感兴趣的伙伴可以自己去研究研究。
二、Factory工厂模式
1. 什么是工厂模式
老规矩先用比较难理解的官方语言来表达,工厂模式是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。现在在以一个具体的例子来简单简化下工厂模式。假如我现在去餐馆吃饭,餐馆有回锅肉,有鱼,有烤鸭可供我选择,然后我点餐餐馆为我做出具体的菜。说完这个例子下面我也会用代码来实现这个例子,给大家简单理解下工厂模式。
首先我们来创建一个餐馆的接口,因为这里只要有做菜就行,所以写一个cook的方法。
public interface Resaurant {
void cook();
}
接下来写三个实现类,分别是做回锅肉的,做鱼的,做烤鸭的,用这三个实现类去实现餐馆的接口。
public class Duck implements Resaurant{
public void cook() {
System.out.println("来一份烤鸭");
}
}
public class Fish implements Resaurant{
public void cook() {
System.out.println("来一份红烧鱼");
}
}
public class Meet implements Resaurant {
public void cook() {
System.out.println("来一份回锅肉");
}
}
现在餐馆已经具备了做回锅肉,做鱼,做烤鸭的功能,但是客人来了并不知道餐馆有这些菜,这时候就需要我们来给餐馆做一个菜单,客人来了就可以根据菜单点餐,这样就省去了很多的麻烦对不对?
public class Wait {
public static final int MEAN_MEET = 1;
public static final int MEAN_FISH = 2;
public static final int MEAN_DUCK = 3;
public static Resaurant getMean(int meantype){
switch (meantype){
case MEAN_MEET :
return new Meet();
case MEAN_FISH :
return new Fish();
case MEAN_DUCK :
return new Duck();
default:
return null;
}
}
}
菜单也有了,现在客人来了可以点餐了,假如客人根据菜单点了一份烤鸭,那餐馆就可以直接给客人制作一份美味的烤鸭
public class Factory {
public static void main(String[] args) {
//简单工厂模式
Resaurant resaurant = Wait.getMean(Wait.MEAN_DUCK);
resaurant.cook();
}
}
来看看执行结果
通过上面的例子就给大家简单介绍了一下简单工厂模式。但是工厂模式除了简单工厂模式还有工厂方法模式和抽象工厂模式,下面我再已餐馆这个例子给大家扩展一下工厂方法模式。工厂方法模式就是把简单工厂中具体的工厂类,划分成两层:抽象工厂层+具体的工厂子类层。
首先我们来创建一个抽象工厂类
public abstract class CookFactory {
public abstract Resaurant createRestaurant();
}
创建两个具体需要的产品实现类去继承上面这个抽象类
public class DuckFactory extends CookFactory {
@Override
public Resaurant createRestaurant() {
return new Duck();
}
}
public class FishFactory extends CookFactory {
public Resaurant createRestaurant() {
return new Fish();
}
}
烤鸭和鱼都做好了,开始享用吧!
public class Cook {
//工厂方法模式
public static void main(String[] args) {
Resaurant duck = new DuckFactory().createRestaurant();
duck.cook();
Resaurant fish = new FishFactory().createRestaurant();
fish.cook();
}
}
看下执行结果
千呼万唤始出来,红烧鱼和烤鸭出锅咯!
2. 优点和缺点
优点:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
三、SingLeton单例模式
1. 单例模式
所谓单例,通俗来说,就是天上只能有一个太阳,只能有一个月亮,所以结合到代码中,就是我们不能每次创建对象都new一个新
的对象供我们使用。所以这个对象应该被设计为共享对象,当需要创建时,只需把这个共享对象的引用赋值给变量即可。所谓共享,
即对象被为static object,且共享对象参数改变时,应考虑对其他使用对象的影响,一般不轻易改变共享对象的内容。
设计单例方式并不唯一,应该考虑线程安全、效率等,根据自己的业务需求来设计。下面展示简单的设计思想。
2. 单例模式的几种实现方式
单例模式的实现有多种方式,如下所示:
①. 懒汉式,线程不安全
- 是否 Lazy 初始化:是
- 是否多线程安全:否
- 实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
实例
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
②. 懒汉式,线程安全
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:易
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。 优点:第一次调用才初始化,避免内存浪费。 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。 getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
实例
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
③. 饿汉式
- 是否 Lazy 初始化:否
- 是否多线程安全:是
- 实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
实例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
④. 双检锁/双重校验锁(DCL,即 double-checked locking) JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。 getInstance() 的性能对应用程序很关键。
实例
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
⑤. 登记式/静态内部类
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要
Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,
因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
实例
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
⑥. 枚举 JDK 版本:JDK1.5 起
- 是否 Lazy 初始化:否
- 是否多线程安全:是
- 实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,
防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。
实例
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
经验之谈:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果
时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,
可以考虑使用第 4 种双检锁方式。
四、Delegate委派模式
1. 什么是委派模式
这里我以一个简单的例子来形容,公司有大boss和部门leader以及部门里的员工,现在大boss给部门leader下达了任务,而作为部门leader肯定是对任务进行具体的规划然后委派给部门里的员工去完成。这中间的关系就类似于委派模式。作为大boss他可以不知道任务具体是哪个员工做的,甚至可以不知道员工的存在,只要以结果为导向,最终能完成任务就可以。作为部门leader相当于一个中介,全程跟进员工的工作进度,最终像大boss汇报工作。作为员工只要完成任务可以不知道任务最终是大boss 下达的。 接下来就以具体的代码来实现这样一个委派模式的关系
2. 创建一个员工干活的接口Target
public interface Target {
public void dosomething(String commond);
}
3. 创建两个员工类ATarget和BTarget
用这两个员工去实现干活的接口,但是现在员工还不知道他具体需要干什么,因为是部门leader给他们分配任务。
A员工的工作类
public class ATarget implements Target {
//A员工做具体的事情
@Override
public void dosomething(String commond) {
System.out.println("A员工做具体的事情"+commond + "");
}
}
B员工工作类
public class BTarget implements Target {
//B员工需要做的事情
@Override
public void dosomething(String commond) {
System.out.println("B员工做具体的事情"+commond);
}
}
创建一个部门leader的类去实现Target,用这个leader去给员工分配具体的工作任务。
public class Leader implements Target {
//领导委派员工做具体的事情
private Map<String,Target> target = new HashMap<String, Target>();
public Leader(){
//领导委派员工A和员工B分别做不同的事情
target.put("打印文件", new ATarget());
target.put("测试项目", new BTarget());
}
@Override
public void dosomething(String commond) {
target.get(commond).dosomething(commond);
}
}
接下来就是大boss上场,但是大boss不会去直接给做事的员工下达命令,而是给leader直接下达命令,比如下达一个打印文件的工作。
public class DelegateTest {
public static void main(String[] args) {
new Leader().dosomething("打印文件");
}
}
可以看出来部门leader类是一个至关重要的类,起到了承上启下的中介作用,这就是一个委派。然后我们来运行程序,来看看大boss没有给员工直接下达工作命令,员工能否完成任务。
可以清楚的看出A员工完成了大boss交给的打印文件的任务(顺利完成任务,可以去申请加薪了)
从上面可以看出来委派模式就是静态代理和策略模式的一种特殊组合,代理模式注重的是过程,委派模式注重的是结果。策略模式注重的是可扩展(外部扩展),委派模式注重的是内部的灵活和复用(委派模式以结果为导向)。
五、Strategy策略模式
1. 策略模式
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们之间可以相互替换,策略模式可以在不影响客户端的情况下发生变化。 策略模式是处理算法不同变体的一种成熟模式,策略模式通过接口或抽象类封装算法的标识,即在接口中定义一个抽象方法,实现该接口的类将实现接口中的抽象方法。策略模式把针对一个算法标识的一系列具体算法分别封装在不同的类中,使得各个类给出的具体算法可以相互替换。
策略模式的结构:
- 策略(Strategy):策略是一个接口,该接口定义若干个算法标识,即定义了若干个抽象方法。
- 具体策略(ConcreteStrategy):具体策略是实现策略接口的类。具体策略实现策略接口所定义的抽象方法,即给出算法标识的具体算法。
- 上下文(Context):上下文是依赖于策略接口的类,即上下文包含有策略声明的变量。上下文中提供了一个方法,该方法委托策略变量调用具体策略所实现的策略接口中的方法。
2. 举例
假如某个游乐场有几种购买门票的方式,普通的游客是不能享受优惠,经常来的游客可以办理年卡享受8折优惠,当然对于身高在1米2以下的儿童给与了5折的优惠。这是游乐场的一种营销策略。对于这三种营销策略我们可以使用if else的语句在一个类中就能实现,可是对于后续的维护可能就有其他的麻烦的地方了。如果后面又有更多的优惠策略难道我们就不停的加 else语句吗?这样就会造成代码看上去很复杂,且不容易维护。说了这些策略模式就可以解决这样的问题。
3. 实战
创建一个策略角色的接口,很简单的一个接口,写一个买票的方法。
public interface TicketStrategy {
void BuyTicket();
}
接下来创建三个具体策略类,对应的就是三种购票的方式。
普通的游客购票
public class Normal implements TicketStrategy {
@Override
public void BuyTicket() {
System.out.println("普通游客没有优惠");
}
}
办理年卡的用户购票
public class Vip implements TicketStrategy {
@Override
public void BuyTicket() {
System.out.println("办年卡游客享受8折优惠");
}
}
1米2以下儿童购票
public class Children implements TicketStrategy {
@Override
public void BuyTicket() {
System.out.println("1米2以下儿童享受5折优惠");
}
}
创建上下文类。
public class Context {
private TicketStrategy ticketStrategy;
public Context(TicketStrategy strategy){
this.ticketStrategy = strategy;
}
public void setTicketStrategy(TicketStrategy ticketStrategy) {
this.ticketStrategy = ticketStrategy;
}
public void BuyTicket(){
this.ticketStrategy.BuyTicket();
}
}
4. 测试
public class StrategyTest {
public static void main(String[] args) {
System.out.println("普通游客策略:");
Context context = new Context(new Normal());
context.BuyTicket();
System.out.println("年卡VIP游客策略:");
context.setTicketStrategy(new Vip());
context.BuyTicket();
System.out.println("1米2以下儿童策略:");
context.setTicketStrategy(new Children());
context.BuyTicket();
}
}
通过上面的例子可以看出这样写就能让代码很简洁明了。后面如果有新的售票方式我们只需要创建具体策略类,然后调用就好了。
5. 优点和缺点
优点:
- 上下文和具体策略是松耦合关系。因此上下文只知道它要使用某个实现Strategy接口类的实例,但不需要知道具体是哪个类。
- 策略模式满足“开-闭原则”。当增加新的具体策略时,不需要修改上下文类的代码,上下文就可以引用新的具体策略的实例。
缺点:通过上面Demo我们会发现调用者必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类并且由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。
六、Prototype原型模式
1. 原型模式
原型模式是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。 现在我们就用原型模式来模拟复制东西,来复制一份个人简历。
下面就是某某的求职简历
public class Resume implements Cloneable {
private String name;
private String birthday;
private String sex;
private String school;
private String timearea;
private String company;
public Resume(String name) {
this.name = name;
}
/**
* @desc 设定个人基本信息
* @param birthday 生日
* @param sex 性别
* @param school 毕业学校
* @return void
*/
public void setPersonInfo(String birthday,String sex,String school){
this.birthday = birthday;
this.sex = sex;
this.school = school;
}
/**
* @desc 设定工作经历
* @param timearea 工作年限
* @param company 所在公司
* @return void
*/
public void setWorkExperience(String timearea,String company){
this.timearea = timearea;
this.company = company;
}
/**
* 克隆该实例
*/
public Object clone(){
Resume resume = null;
try {
resume = (Resume) super.clone();
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return resume;
}
public void display(){
System.out.println("姓名:" + name);
System.out.println("生日:" + birthday + ",性别:" + sex + ",毕业学校:" + school);
System.out.println("工作年限:" + timearea + ",公司:" + company);
}
}
客户端的验证测试操作
public static void main(String[] args) {
//原型A对象
Resume a = new Resume("James");
a.setPersonInfo("2.16", "男", "家里蹲大学");
a.setWorkExperience("2018.09.05", "搬砖工地");
//克隆B对象
Resume b = (Resume) a.clone();
//输出A和B对象
System.out.println("----------------A--------------");
a.display();
System.out.println("----------------B--------------");
b.display();
/*
* 测试A==B?
* 对任何的对象x,都有x.clone() !=x,即克隆对象与原对象不是同一个对象
*/
System.out.print("A==B?");
System.out.println( a == b);
/*
* 对任何的对象x,都有x.clone().getClass()==x.getClass(),即克隆对象与原对象的类型一样。
*/
System.out.print("A.getClass()==B.getClass()?");
System.out.println(a.getClass() == b.getClass());
}
执行结果
2. 总结
优点:
- 性能提高。
- 逃避构造函数的约束。
缺点:
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
- 必须实现 Cloneable 接口。
七、Template模板模式
1. 模板模式
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。通俗的说的就是有很多相同的步骤的,在某一些地方可能有一些差别适合于这种模式,如大话设计模式中说到的考试场景中,每个人的试卷都是一样的,只有答案不一样。这种场景就适合于模板方法模式。我这次自己写的是一个汽车启动的过程,每一种汽车启动的过程都基本是一样的流程,无非是这一过程中存在一些细小差别。 总的来说,模板模式就是通过抽象类来定义一个逻辑模板,逻辑框架、逻辑原型,然后将无法决定的部分抽象成抽象类交由子类来实现,一般这些抽象类的调用逻辑还是在抽象类中完成的。这么看来,模板就是定义一个框架,比如造车子,需要造车轮,车身,发动机,车灯等部分组成,不同的车的区别就在于这些部件的优劣而已,但是总少不了这些东西。下面我就用一个例子来造一辆路虎越野车和一辆奥迪轿车。
2. 模板模式实战
首先需要创建一个模板抽象父类,将造车需要的模块创建好。
public abstract class CarTemplate {
protected String name;
protected CarTemplate(String name){
this.name = name;
}
protected abstract void buildWheel();
protected abstract void buildEngine();
protected abstract void buildCarbody();
protected abstract void buildCarlight();
public final void buildCar(){
buildWheel();
buildEngine();
buildCarbody();
buildCarlight();
}
}
然后创建一个造路虎车的子类,用这个子类去继承造车的公有模块类CarTemplate。然后在方法中完善造路虎车特有的功能,代码如下:
public class LandRover extends CarTemplate{
public LandRover(String name) {
super(name);
}
@Override
protected void buildWheel() {
System.out.println(name + "越野车轮");
}
@Override
protected void buildEngine() {
System.out.println(name + "柴油发动机");
}
@Override
protected void buildCarbody() {
System.out.println(name + "SUV越野车型");
}
@Override
protected void buildCarlight() {
System.out.println(name + "普通车灯");
}
}
再创建一个造奥迪的子类,同样用这个子类继承模板类,并完善造奥迪的方法,代码如下:
public class Audi extends CarTemplate {
public Audi(String name) {
super(name);
}
@Override
protected void buildWheel() {
System.out.println(name + "的普通轿车车轮");
}
@Override
protected void buildEngine() {
System.out.println(name + "的汽油发动机");
}
@Override
protected void buildCarbody() {
System.out.println(name + "的豪华舒适性车身");
}
@Override
protected void buildCarlight() {
System.out.println(name + "的独特魔力车灯");
}
}
2. 测试
public class TemplateTest {
public static void main(String[] args) {
CarTemplate car1 = new LandRover("路虎");
CarTemplate car2 = new Audi("奥迪");
car1.buildCar();
car2.buildCar();
}
}
3. 执行结果
所有步骤都已完成,奥迪和路虎车运用统一模板,但是通过在子类中完善各自不同的方法而达到目的
八、Observer观察者模式
1. 观察者模式
观察者模式也被称为发布-订阅(Publish/Subscribe)模式,它属于行为型模式的一种。观察者模式定义了一种一对多的依赖关系,一个主题对象可被多个观察者对象同时监听。当这个主题对象状态变化时,会通知所有观察者对象并作出相应处理逻辑。
2. 观察者模式包含四个角色
- 抽象被观察者角色:也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。
- 抽象观察者角色:为所有的具体观察者定义一个接口,在得到主题通知时更新自己。
- 具体被观察者角色:也就是一个具体的主题,在集体主题的内部状态改变时,所有登记过的观察者发出通知。
- 具体观察者角色:实现抽象观察者角色所需要的更新接口,一边使本身的状态与制图的状态相协调。
前面说到观察者模式也被称为发布-订阅模式,这里我就用一个微信公众号发布文章和用户订阅公众号接收文章的例子。用户订阅了公众号就可以接收公众号发布的信息文章,当用户对此类型公众号不感兴趣的时候就可以取消订阅,这时候就接收不到公众号发布的文章。用户就是一个观察者,公众号就是被观察者,被观察者与观察者之间的关系是一对多。接下来我将用具体代码来实现这个demo。
3. 具体实现
①.首先创建一个被观察者抽象接口,创建注册观察者,取消观察者和提醒所有观察者更新消息的方法,用途是用户订阅、取消订阅和接收消息。
public interface Observable {
//注册观察者
void registerObserver(Observer observer);
//取消观察者
void removeObserver(Observer observer);
//通知所有观察者更新消息
void notifyObserver();
}
②.定义一个抽象观察者接口
public interface Observer {
void update(String message);
}
③.定义被观察者,实现了Observable接口,对Observable接口的三个方法进行了具体实现,同时有一个List集合,用以保存注册的观察者,等需要通知观察者时,遍历该集合即可。
public class WeChatServer implements Observable{
private List<Observer> list;
private String message;
public WeChatServer(){
list = new ArrayList<Observer>();
}
@Override
public void registerObserver(Observer o) {
list.add(o);
}
@Override
public void removeObserver(Observer o) {
if(!list.isEmpty()){
list.remove(o);
}
}
@Override
public void notifyObserver() {
for (int i = 0; i < list.size(); i++){
Observer observer = list.get(i);
observer.update(message);
}
}
public void setInfomation(String s){
this.message = s;
System.out.println("公众号推送消息是"+s);
notifyObserver();
}
}
④.创建具体的观察者,这里具体的观察者也就是用户。
public class User implements Observer {
private String name;
private String message;
public User(String name) {
this.name = name;
}
@Override
public void update(String message) {
this.message = message;
read();
}
public void read(){
System.out.println(name + "接收到推送消息" + message);
}
}
⑤.接下来就是具体的测试类,假设现在有三个用户订阅了公众号,公众号发布了一条信息是PHP是世界上最好的语言,此时java开发工程师李四接收到信息后颇为不满,于是果断取消订阅。后来公众号又发布了一条信息是:java是世界上最好的语言,此时取消订阅的李四已经接收不到这条信息了。
public class ObserverTest {
public static void main(String[] args) {
WeChatServer server = new WeChatServer();
Observer userZhang = new User("ZhangSan");
Observer userLi = new User("LiSi");
Observer userWang = new User("WangWu");
server.registerObserver(userZhang);
server.registerObserver(userLi);
server.registerObserver(userWang);
server.setInfomation("PHP是世界上最好用的语言!");
System.out.println("----------------------------------------------");
//李四取消订阅
server.removeObserver(userLi);
server.setInfomation("JAVA是世界上最好用的语言!");
}
}
关于面试题的答案和笔者Java学习路线图,点击下方传送门即可免费领取!!!
传送门
微信公众号:慕容千语的架构笔记。欢迎关注一起进步。