观察者模式
来源:互联网 发布:淘宝设计价格表 编辑:程序博客网 时间:2024/05/16 12:45
概述
有时被称作发布/订阅模式,观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
问题的提出
在生活当中,经常会遇到多种数据关注一个对象数据变化的情况。举个例子:生活中有温度记录仪,当温度发生变化时,需要完成一下功能:记录温度日志,显示温度变化曲线,当温度越界是触发扬声器发出声音等等。。。伪代码大致如下:
while(温度变化){ 记录温度日志; 显示温度变化曲线; 当温度越界是触发扬声器发出声音......}
这种方法将所有的功能集中到一块,当需求分析发生变化时,例如增加新的检测功能 或者舍弃某一检测功能,程序都要修改,这是我们不希望的结果,观察者设计模式是解决这类问题的有效方法。
解决的问题
将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。观察者就是解决这类的耦合关系的。
观察者模式
观察者模式适合解决多种对象跟踪一个对象数据变化的程序结构问题, 有一个称作“主题”的对象和若干个称作“观察者的对象”。
延续上面的例子:主题对象—–温度,3个观察者——–温度日志、温度曲线、温度告警。
因此设计观察者模式的只有两种角色:主题和观察者
从观察者模式中我们可以从以下递推中得出一些重要的结论
- 主题要知道有哪些观察者对其进行监测,因此主题类中一定有一个集合类成员变量,包含了观察这的对象集合。
- 既然包含了观察者对象的集合,那么观察者一定是多态的,有共同的父类接口
- 主题完成的主要功能是:可以添加观察者,可以撤销观察者,可以向观察者发消息,引起观察者相应。 这三个功能是固定的,因此主题类可以从固定的接口中派生。
因此,编写观察者模式,要完成以下功能的开发:
1. 主题ISubject接口定义
2. 主题类编写
3. 观察者接口IObserver接口定义
4. 观察者类实现
主题接口
public interface ISubject { /** * 注册观察者 * @param subject */ void register(IObserver obs); /** * 撤销观察者 * @param subject */ void unRegister(IObserver obs); /** * 通知观察者 * @param subject */ void notifyObservers();}
观察者接口
public interface IObserver { /** * 响应操作 * @param data */ void refresh(String data);}
主题实现类
public class Subject implements ISubject { // 观察者维护变量 private Vector<IObserver> vec = new Vector<IObserver>(); // 主题中心数据 private String data ; public String getData() { return data; } /** * 主题注册(添加) * @param data */ public void setData(String data) { this.data = data; } @Override public void register(IObserver obs) { vec.add(obs); } @Override public void unRegister(IObserver obs) { if(vec.contains(obs)){ vec.remove(obs); } } @Override public void notifyObservers() { for(int i=0 ;i<vec.size(); i++){ IObserver obs = vec.get(i); obs.refresh(data); } }}
主题实现类Subject是观察者设计模式中最重要的一个类,包含了观察者对象的维护变量vec 以及主题中心数据data变量 与具体观察者对象的关联方法(通过notifyObservers()).
也就是说,从此类出发,可以更加深刻的理解为什么ISubject为什么定义了3个方法,IObserver接口为什么定义了1个方法。
观察者实现类
public class Observer implements IObserver { @Override public void refresh(String data) { System.out.println("I have received the data :" + data); }}
public class Observer2 implements IObserver { @Override public void refresh(String data) { System.out.println("Observer2 have received the data :" + data); }}
测试
public class Test { public static void main(String[] args) { // 主题实现类 Subject subject = new Subject(); // 观察者接口实例化 IObserver obs = new Observer(); IObserver obs2 = new Observer2(); // 注册观察者 subject.register(obs); subject.register(obs2); // 设置数据 subject.setData("OOOOOOOOOOOOOOOOOOOOOO"); // 通知观察者更新数据 subject.notifyObservers(); }}
输出:
Observer1 have received the data :OOOOOOOOOOOOOOOOOOOOOO
Observer2 have received the data :OOOOOOOOOOOOOOOOOOOOOO
该段代码的含义是:当主题中心数据变化(通过setData())后,主题类subject要调用notifyObservers()方法,通知所有的观察者接收数据并进行数据相应。
深入理解观察者模式
深入理解ISubject 和IObserver接口
推数据和拉数据
增加抽象类Abstract Subject
避免添加统一类型的观察者对象
反射技术的应用
1. 深入理解ISubject 和IObserver接口
上文中的Subject类中的中心数据data是String类型的,这也就决定了IObserver接口中定义的refresh()方法参数类型必须是String类型的。
若data改为其他类型,着IObserver接口等相关代码都需要修改。
事实上,我们只要把ISubject、IObserver接口改为泛型接口就可以了。
主题泛型接口ISubject
public interface ISubject<T> { public void register(IObserver<T> observer); public void unRegister(IObserver<T> observer); public void notifyObservers();}
观察者泛型接口
public interface IObserver<T>{ public void refresh(T data)}
当把ISubject、IObserver接口修改为泛型接口后,要求参数T必须是类类型,不能是基本数据类型,比如不能是int ,但可以是Integer类型。
2. 推数据和拉数据
推数据的方式是指:具体主题将变化后的数据全部交给具体观察者,即将变化后的数据直接传递给具体观察者用于更新数据。
从上面观察者的接口定义中就可以很明显的看出
public interface IObserver{ public void refresh(String data);}
可以看出,主题对象直接将数据传递给观察者对象,这是“推”数据方式最大的特点。
与之相比,“拉”数据方式的特点:观察者对象可间接获取变化后的主题数据,观察者自己把数据拉过来。
将程序修改为拉数据的方式:
主题接口定义(同推数据的方式):
public interface ISubject { public void register(IObserver observer); public void unRegister(IObserver observer); public void notifyObservers();}
观察者接口定义
public interface IObserver { // 参数为 ISubject接口类 public void refresh(ISubject subject);}
主题实现类 (区别在于notifyObservers方法):
public class Subject implements ISubject { // 主题实现类中维护的观察者接口对象 private Vector<IObserver> vec = new Vector<>(); // 主题数据 private String data ; /** * 获取主题 拉的方式 * @return */ public String getData() { return data; } /** * 主题数据添加 适用于推的方式 * @param data */ public void setData(String data) { this.data = data; } @Override public void register(IObserver observer) { vec.add(observer); } @Override public void unRegister(IObserver observer) { if(vec.contains(observer)){ vec.remove(observer); } } @Override public void notifyObservers() { for(int i=0 ;i < vec.size();i++){ IObserver observer = vec.get(i); // 代替原来的 refresh(data) observer.refresh(this); } }}
观察者实现类
public class Observer implements IObserver { @Override public void refresh(ISubject iSubject) { // 需要转化成 具体的实现类 Subject s = (Subject) iSubject; String data = s.getData(); System.out.println(data); }}
测试
public class Test { public static void main(String[] args) { Subject subject = new Subject(); IObserver observer = new Observer(); subject.setData("999999999999"); subject.register(observer); subject.notifyObservers(); }}
拉数据的方式:主要将观察者接口IObserver中的refresh(String data)修改为refresh(ISubject subject)。
可推测出:具体观察者子类对象一定能获取主题Subject对象,当然也可以间接的访问主题对象中的变量了。
从此观点出发就可以很好地理解 notifyObservers(ISubject) 和 refresh(ISubject) 方法代码的修改情况了。
3.增加抽象类Abstract Subject
假设有很多个主题类,按照以上的写法,每个主题类都要重写register()、unRegister()、notifyObservers()方法。
又假设这三个方法的代码恰巧是相同的(这种可能性是很大的,因为他们都是通用的方法),那么每个主题类的代码就显得重复了,用中间成类来解决代码重复问题是一个较好的办法。
代码如下:
主题接口:
public interface ISubject { public void register(IObserver obs); public void unRegister(IObserver obs); public void notifyObservers();}
观察者接口:
public interface IObserver { /** * 采用拉数据的方式 * @param obj */ public void refresh(ISubject obj);}
增加的抽象层类AbsSubject
/** * 抽象類 * * @author Mr.Yang * */public abstract class SubjectAbs implements ISubject { // 维护观察者接口集合的成员变量 private Vector<IObserver> vec = new Vector<IObserver>(); @Override public void register(IObserver obs) { vec.add(obs); } @Override public void unRegister(IObserver obs) { if (vec.contains(obs)) { vec.remove(obs); } } @Override public void notifyObservers() { for (int i = 0; i < vec.size(); i++) { IObserver obs = vec.get(i); // 采用拉数据的方式,让观察者主动从主题实例这里获取数据 obs.refresh(this); } }}
派生主题类Subject
/** * 派生主题类 Subject * @author Mr.Yang * */public class Subject extends SubjectAbs { // 继承了抽象类 SubjectAbs // 子类独有的 private String data ; public String getData() { return data; } public void setData(String data) { this.data = data; }}
一个具体的观察者类
public class Observer implements IObserver { @Override public void refresh(ISubject obj) { // 强制转换类型 Subject subject = (Subject)obj; // 从主题实现类中通过拉数据的方式 获取 主题 String data = subject.getData(); System.out.println("拉数据的方式获取:" + data); }}
测试:
public class Test { public static void main(String[] args) { Subject subject = new Subject(); IObserver obs = new Observer(); subject.setData("XXXXXXXXXXXX"); subject.register(obs); subject.notifyObservers(); }}
有了中间抽象层abstract class SubjectAbs,灵活了具体主题类的开发:
- 使用class XXXSubject extends SubjectAbs{….}表名直接继承父类的register(),unRegister(),notifyObserver()[当然了也可以重写任意方法,非必选]。
- 还可以使用 class XXXSubject implements ISubject{…….},表名3个方法必须重写。
虽然SubjectAbs类中并没有抽象方法,但定义成了抽象类。这是因为从语义角度上来讲,该类并不是一个完整的主题类,它缺少主题数据,因此把它定义为抽象类。
4.避免添加统一类型的观察者对象
如果我们将测试类代码改为如下方式:
public class Test{ public static void main(String[] args){ IObserver obs = new Observer(); IObserver obs2 = new Observer(); Subject subject = new Subject(); subject.register(obs); subject.register(obs2); subject.setData("dddddddd"); subject.notifyObservers(); }}
可以看出obs 和 obs2观察者对象类型是相同的,都是Observer。
这两个相同类型的观察者对象那个都能正确的添加到主题中。
但是有些情况这是不允许的,要求禁止主题对象添加相同的观察者对象。
因此,在主题对象添加观察者对象前,应该先进行查询,然后判断是否添加观察者对象,register()方法修改后的代码如下:
public void register(IObserver obs){ if(!vec.contains(obs)){ vec.add(obs); }}// 其他代码略
但是,我们知道Vector中类中的contains()方法默认的是物理查询。 由于Test类中的obs、obs2虽然都是Observer对象,但是他们的物理地址是不同的,因此任然添加到了vec向量中。这种方式也是不可行的。
与我们的初衷不符合,怎么办呢?
我们查看下Vector的contains()方法的源码:
public boolean contains(Object o) { return indexOf(o, 0) >= 0; }
我们发现contains方法调用了indexOf():
public synchronized int indexOf(Object o, int index) { if (o == null) { for (int i = index ; i < elementCount ; i++) if (elementData[i]==null) return i; } else { for (int i = index ; i < elementCount ; i++) if (o.equals(elementData[i])) return i; } return -1; }
我们发现o.equals(elementData[i]),两个元素相等是由equals()决定的,只要重载传入参数o中的equals()就可以了。
由于elementData[i]也是观察者类的对象,所以equals()方法中的参数原型也明确了。以两个具体观察者为例,功能类代码如下。
主题接口(同上)
public interface ISubject{ public void register(IObserver obs); public void unRegister(IObserver obs); public void notifyObservers(); }
观察者接口(重点是增加了getMark())
public interface IObserver{ public void getMark(); // 推数据的方式 public void refresh(String data);}
主题实现类(重点是register方法)
public class Subject implements ISubject{// 观察者维护变量 private Vector<IObserver> vec = new Vector<IObserver>(); // 主题中心数据 private String data ; public String getData() { return data; } /** * 主题注册(添加) * @param data */ public void setData(String data) { this.data = data; } /** * 主题注册(添加)观察者 */ @Override public void register(IObserver obs) { if(!vec.contains(obs)){ vec.add(obs); System.out.println("添加成功" ); }else{ System.out.println("物理地址相同,请不要添加重复观察者" ); } } /** * 主题撤销(删除)观察者 */ @Override public void unRegister(IObserver obs) { if(vec.contains(obs)){ vec.remove(obs); } } /** * 主题通知所有观察者进行数据响应 */ @Override public void notifyObservers() { for(int i=0 ;i<vec.size(); i++){ IObserver obs = vec.get(i); obs.refresh(data); } }}
观察者接口实现类(重点是重写equals方法)
public class Observer implements IObserver { private static final int MARK = 1 ; @Override public int getMark() { return MARK; } /** * 必须要重写equals方法 */ @Override public boolean equals(Object obj) { Observer obs = (Observer)obj; return obs.getMark() == MARK ; } @Override public void refresh(String data) { System.out.println("Observer1 have received the data :" + data); }}
测试类
public class Test { public static void main(String[] args) { // 主题实现类 Subject subject = new Subject(); // 观察者接口实例化 IObserver obs = new Observer(); IObserver obs_copy = new Observer(); // 注册观察者 subject.register(obs); subject.register(obs_copy); // 设置数据 subject.setData("OOOOOOOOOOOOOOOOOOOOOO"); // 通知观察者更新数据 subject.notifyObservers(); }}
输出结果:
添加成功物理地址相同,请不要添加重复观察者Observer1 have received the data :OOOOOOOOOOOOOOOOOOOOOO
关键思路:
在每个观察者类中增加一个标识常量MARK,不同的观察者对象中的MARK常量值是不同的。 本案例中 Observer中MARK为1 ,如果有Observer2,可以定义为2…….。 由于主题类Subject中register()方法参数是obs是IObserver类型, 是多态表示,因此在IObserver接口中需要增加多态方法getMark(),用于获取观察者对象中的MARK值。
5.反射技术的应用
将观察者类信息封装在XML配置文件中,从而利用反射技术可以动态的加载观察者对象。
配置文件采用键-值配对形式,值对应的是具体观察者的类名称。由于键是关键字,不能重复,为了编程方便,键采用“统一前缀+流水号”的形式。如下说所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"><properties> <comment>Observer</comment> <entry key="observer1">com.turing.designPattern.createPattern.Obsever_Reflect.impl.Observer</entry> <entry key="observer2">com.turing.designPattern.createPattern.Obsever_Reflect.impl.Observer2</entry></properties><!-- 约定好的规则 键前缀 “observer” 键流水号 1 2 3 ..... -->
主题接口:
/** * 变化主题可以是泛型的 使用拉数据的方式通知观察者更新数据 * * @author Mr.Yang * * @param <T> */public interface ISubject<T> { void register(String path); // 表示从配置文件中加载观察者 void unRegister(IObserver<T> observer); void notifyObservers();}
观察者接口:
public interface IObserver<T> { // 通过拉数据的方式通知观察者更新响应 void refresh(ISubject<T> subject);}
主题实现类:
/** * 主题实现类 * * @author Mr.Yang * */public class Subject implements ISubject { // 主题数据 GenericSubjectData data; // 观察者集合 Vector<IObserver> vec = new Vector<>(); public GenericSubjectData getData() { return data; } public void setData(GenericSubjectData data) { this.data = data; } @Override public void register(String path) { String prefix = "observer"; String observerClassName = null; try { Properties propertie = new Properties(); FileInputStream fin = new FileInputStream(path); propertie.loadFromXML(fin); int i = 1; while ((observerClassName = propertie.getProperty(prefix + i)) != null) { // Constructor c = Class.forName(observerClassName).getConstructor(); IObserver observer = (IObserver) Class.forName(observerClassName).newInstance(); vec.add(observer); System.out.println("添加" + observerClassName + "成功"); i++; } fin.close(); } catch (Exception e) { e.printStackTrace(); } } @Override public void unRegister(IObserver observer) { if (vec.contains(observer)) { vec.remove(observer); } } @Override public void notifyObservers() { for (int i = 0; i < vec.size(); i++) { IObserver observer = vec.get(i); // 拉数据的方式,refresh的是ISubject接口类型 observer.refresh(this); } }}
泛型的主题:
/** * 根据这一主题进行变化 * @author Mr.Yang * */public class GenericSubjectData { // TODO }
观察者实现类1:
public class Observer implements IObserver { @Override public void refresh(ISubject s) { Subject sj = (Subject) s; GenericSubjectData data = sj.getData(); System.out.println( "观察者【" + Observer.class.getSimpleName() + "】使用拉数据的方式获取主题,更新数据,主题类为:" + data.getClass().getSimpleName()); }}
观察者实现类2:
public class Observer2 implements IObserver { @Override public void refresh(ISubject s) { Subject sj = (Subject) s; GenericSubjectData data = sj.getData(); System.out.println( "观察者【" + Observer2.class.getSimpleName() + "】使用拉数据的方式获取主题,更新数据,主题类为:" + data.getClass().getSimpleName()); }}
测试类:
public class Test { public static void main(String[] args) { Subject subject = new Subject(); IObserver<GenericSubjectData> observer = new Observer(); subject.setData(new GenericSubjectData()); subject.register("D:/workspace/ws-java-base/designPatterns/src/observers.xml"); subject.notifyObservers(); }}
输出结果:
添加com.turing.designPattern.createPattern.Obsever_Reflect.impl.Observer成功添加com.turing.designPattern.createPattern.Obsever_Reflect.impl.Observer2成功观察者【Observer】使用拉数据的方式获取主题,更新数据,主题类为:GenericSubjectData观察者【Observer2】使用拉数据的方式获取主题,更新数据,主题类为:GenericSubjectData
JDK中的观察者和设计者模式
由于观察者模式中主题类功能以及观察者接口定义内容的稳定性,JDK的java.utils包提供了系统的主题类Observable以及观察者接口Observer.
UML类图如下:
很明显,Observer相当于上述的IObserver观察者接口,
查看JDK中Observer接口的源码:
/** * A class can implement the <code>Observer</code> interface when it * wants to be informed of changes in observable objects. * * @author Chris Warth * @see java.util.Observable * @since JDK1.0 */public interface Observer { /** * This method is called whenever the observed object is changed. An * application calls an <tt>Observable</tt> object's * <code>notifyObservers</code> method to have all the object's * observers notified of the change. * * @param o the observable object. * @param arg an argument passed to the <code>notifyObservers</code> * method. */ void update(Observable o, Object arg);}
其中update()方法的第一个参数Observable 类型,表名采用的是“拉”数据的方式;
Observer类(不是接口)相当于上述中的Subject主题类。
查看Observer类的源码结构:
我们可以看出addObserver(),deleteObserver(),notifyObservers()三个方法分别代表 “添加 删除 通知” 观察者对象功能。
XXXChanged()方法主要是设置或者获得changed成员变量的值或状态,changed为true时表明主题中心数据发生了变化。
利用JDK提供的Observer接口 、Observable类来完成观察者模式:
- 主题类编写(需要extends Observable类)
import java.util.Observable;/** * 主题实现类,需要继承Observable类 * 在此类中,设置主题数据,更新标识,通知观察者 * @author Mr.Yang * */public class Subject extends Observable { // 主题数据 private String data ; public String getData() { return data; } public void setData(String data) { this.data = data; // 更新数据 setChanged(); // 设置更新数据标志 notifyObservers(null); // 通知具体观察者 }}
- 具体观察者类(需要implements Observer接口)
import java.util.Observable;import java.util.Observer;/** * 具体的观察者, * 主要是通过拉数据的方式获取主题,响应数据 * @author Mr.Yang * */public class ObserverImpl implements Observer { @Override public void update(Observable o, Object arg) { Subject s = (Subject)o; // 获取主题数据,刷新数据 String data = s.getData(); System.out.println(data); }}
- 测试
import java.util.Observer;public class Test { public static void main(String[] args) { Subject subject = new Subject(); // 定义主题实现类 Observer obs = new ObserverImpl(); // 定义观察者 subject.addObserver(obs); // 主题添加观察者 subject.setData("JDK中观察者的实现");// 主题更新数据 }}
输出结果:
JDK中观察者的实现
利用JDK中提供的系统类Observable和Observer接口,大大简化了观察者设计模式的程序编码。当然了我们之前提到的那些自定义实现的设计者模式也并非无效的,这些知识是从底层的接口讲起直至最高层,对于理解观察者模式的本质是有必要的。
值得注意的是,JAVA API中给出的Observeable是一个类,不是接口。
尽管该类为它的子类提供了很多可以直接使用的方法,但是有一个问题:Observable的子类无法使用继承方式复用其他类的方法,因为java不支持多继承。 这种情况下,使用我们自定义主题接口ISubject就可以轻松实现了。
Observable 类 和 Observer接口代码分析:
Observable类、Observer接口均是专家级的代码,我们可以从中学到很多。其中有以下两点:
设置标识变量
主要体现在changed成员变量的设置。notifyObservers()是非常重要的一个方法,JDK源码如下:
public void notifyObservers(Object arg) { /* * a temporary array buffer, used as a snapshot of the state of * current Observers. */ Object[] arrLocal; synchronized (this) { /* We don't want the Observer doing callbacks into * arbitrary code while holding its own Monitor. * The code where we extract each Observable from * the Vector and store the state of the Observer * needs synchronization, but notifying observers * does not (should not). The worst result of any * potential race-condition here is that: * 1) a newly-added Observer will miss a * notification in progress * 2) a recently unregistered Observer will be * wrongly notified when it doesn't care */ if (!changed) return; arrLocal = obs.toArray(); clearChanged(); } for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); }
可以看出当changed为true时,观察者对象才做出响应。而我们自己设计的程序中,只要调用notifyObservers()方法,即使主题中心数据在没有更新的情况下,观察者对象也能响应。经过对比,我们可以体会到:若某方法非常关键,一定要考虑到它有几种状态,从而定义标识变量来予以控制。
- 形参的设定
这里的形参主要是指Observable类中的notifyObservers(Object arg)方法参数和Observer接口中定义的update(Observerable ,Object arg) 方法中的第二个形式参数。
有了arg参数对象,我们可以把一些比较信息由主题动态传递给观察者,使编程更加灵活。
举个例子说明一下:
有两个观察者,一个负责统计满足“data=arg”出下的次数,(arg是动态传入的字符串),另一个在屏幕上显示data字符串。
程序如下:
主题类:
import java.util.Observable;public class SubjectArg extends Observable { // 中心数据 private String data ; // 形参 Object factor ; // 增加条件变量 public void setFactor(Object factor) { this.factor = factor; } public String getData() { return data; } /** * 中心数据改变 * @param data */ public void setData(String data) { this.data = data; setChanged(); // 更新标识 notifyObservers(factor);// 通知具体观察者 }}
观察者类1:
import java.util.Observable;import java.util.Observer;/** * 统计中心数据出现的次数 * @author Mr.Yang * */public class OneObserver implements Observer { int i = 0 ; public int getI() { return i; } @Override public void update(Observable o, Object arg) { SubjectArg subjectArg = (SubjectArg)o; if(subjectArg.getData().equals(arg.toString())){ i++; } }}
观察者类2:
import java.util.Observable;import java.util.Observer;public class TwoObserver implements Observer { @Override public void update(Observable o, Object arg) { SubjectArg subjectArg = (SubjectArg)o; String msg = subjectArg.getData() ; System.out.println("中心数据:" + msg); }}
测试:
public class TestArg { public static void main(String[] args) { // 实例化主题类 SubjectArg subjectArg = new SubjectArg() ; // 为主题设置条件参数 subjectArg.setFactor("hello"); // 实例化观察者类 OneObserver observer = new OneObserver(); TwoObserver observer2 = new TwoObserver(); // 添加观察者 subjectArg.addObserver(observer); subjectArg.addObserver(observer2); // 改变中心数据 subjectArg.setData("hello"); subjectArg.setData("hello"); subjectArg.setData("hello"); subjectArg.setData("How are you"); System.out.println("符合条件的次数:"+observer.getI()); }}
输出:
中心数据:hello中心数据:hello中心数据:hello中心数据:How are you符合条件的次数:3
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- 观察者模式
- OpenFST notes
- 使用HTTP访问网络
- 2015年,我的创业记忆片段
- LeetCode 145:Binary Tree Postorder Traversal
- linux内核ipv4网络部分分层结构及涉入源文件
- 观察者模式
- 唐纳德.霍尔《是时候了》
- swift中的协议
- 算法设计题2.25-线性表-第2章-《数据结构习题集》-严蔚敏吴伟民版
- 大话设计模式之外观模式(Java版实现) 买股票还是买基金在此揭晓
- 有关sscanf()函数的知识
- javascript--iframe的JS方法,用法!contentWindow 、parent、top、onload
- OpenCV【3】---二值化图像
- online_judge_1149