有读者在问我是否会写和设计模式相关的面试题,我想了很久都不知道怎么下笔。关于设计模式,我并没有刻意去用,有时候用了,也不会去计较刚刚用的是什么模式。这样再去面试别人就有些困难了。
问简单的吧,单列模式,人人都会。继续问Android的单列模式怎么才能保证一定是单个实列,感觉又有点吹毛求疵。
所以,我很少问设计模式,光谈某某模式很有纸上谈兵的味道,设计模式是要“付诸实践”的面试题,光靠介绍还有点难于判断。关于设计模式,我比较欣赏的是Thoughtworks面试的方式:结对编程!面试前先给面试者布置“家庭作业”,然后Thoughtworks会派工程师和面试者进行结对编程,编程的内容和面试者之前的作业相关,让面试者通过测试驱动和代码重构表现他/她的编程规范、设计和重构的能力。这个时候可以很容易看到面试者对设计模式的掌握和运用情况。
传言:在国内,ThoughtWorks被称为“最难面试的IT公司”。貌似在国外也被评为全球最难面试的IT司。
但是一般公司显然没有这样的条件来选拔面试者。关于结对编程下来我和ThoughtWorks的朋友做个交流再和大家分享,还是回到设计模式的面试题,这章就讲一下我在求职的过程中印象还比较深刻的一道题吧。
面试题:回调函数和观察者模式的区别?
当时听到这样的题时我也是一脸懵逼,暗叹出题人居心叵测,冷不丁的还真难回答这样的问题,这哪里是考设计模式,完全是考反应能力嘛。
观察者模式
网上很容易查到观察者模式的定义:
观察者模式定义了对象间的一种一对多依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
Android中大量的使用了观察者模式。你可能已经用过ListView的adapter.notifyDataSetChanged来触发ListView的列表界面进行更新。notifyDataSetChanged的内部实现就是基于观察者模式。
跟进这段代码你会发现:BaseAdapter中的DataSetObserver(观察者)实现Observer接口,DataSetObservable(被观察者)继承Observable类。
标准的观察者模式的写法应该照下面的UML图:
有几个概念(抽象主题(Subject)、具体主题(ConcreteSubject)、抽象观察者(Observer)和具体观察者(ConcreteObserver)),好在Java帮我实现了相关的代码,可以通过Observable类和Observer接口实现了观察者模式。Observer对象是观察者,Observable对象是被观察者。
还有EventBus, RxJava等常见的开源库也是居于观察者模式设计的,只是它们实现的方式各有不同。
回调函数
那回调函数和这又有什么关系呢?看看这段再熟悉不过的代码片段:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// do something
}
});
View的Listener监听会通过setOnClickListener给View传递一个Listener对象,当相关的事件发生时是触发onClick(回调onClick)。这其实也是一种观察者模式,OnClickListener是观察者,View是被观察者,当View收到Click事件是会通知观察者执行onClick()。
关于设计模式的反思
模式的外在形式其实是“套路”,这些套路来源于现实中生产实践的总结,但要清楚认识到不是所有“套路”都会合适你的。
设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式的初衷是用经过检验的“套路”来提高代码的生产效率,人们也容易理解约定成俗的“套路”。从面向对象设计的角度来看,其实就是要做到高内聚低耦合。
所以,在考虑使用什么样的模式或模式组合时,我们不妨先冷静下来回忆一下面向对象设计的SOLID原则,我们要遵循一定的原则,而不是为了模式而模式。
面向对象设计的SOLID原则:
- S 单一功能原则:对象应该仅具有一种单一功能。
- O 开闭原则:软件体应该是对于扩展开放的,但是对于修改封闭的。
- L 里氏替换原则:程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。
- I 接口隔离原则:多个特定客户端接口要好于一个宽泛用途的接口。
- D 依赖反转原则:依赖于抽象而不是一个实例,依赖注入是该原则的一种实现方式。
“标准答案”
有读者反映我之前的面试题总是列出了试题却没有直接给出答案,而是说些有的没的“废话”,“很浪费”大伙的时间。我用题“骗来”读者,却让读者自己去思考怎么回答,如果从面试刷题的角度来说我确实是“罪大恶极”。
反省了一下,这些读者的要求不无道理,我决定做一个改善吧。从今天这题开始,“Android面试一天一题”的最后一节都会奉上相关的面试题和我认为的“标准答案”,需要节省时间的读者可以直接翻到最后。
注意:标准答案都加了“”,我不能保证自己的理解和描叙百分百正确,参考时可以留个“心眼”。
面试题: 回调函数和观察者模式的区别?
“标准答案”:观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。观察者模式完美的将观察者和被观察的对象分离开,一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新。
回调函数其实也算是一种观察者模式的实现方式,回调函数实现的观察者和被观察者往往是一对一的依赖关系。
所以最明显的区别是观察者模式是一种设计思路,而回调函数式一种具体的实现方式;另一明显区别是一对多还是多对多的依赖关系方面。
面试题:Android的单列模式如何保证一定单列的情况?
分析:如下代码,我们一般写单列模式是这样的:
public class Singleton{
private static Singleton instance;
private Singleton() {};
public static Singleton getInstance() {
if (instance==null)
instance=new Singleton();
return instance;
}
}
如果不同的线程同时执行“if (instance==null)”,因为instance还未赋值,是会存在多个instance实例的。所以保险的一点的写法:
public class Singleton{
private static Singleton instance;
private Singleton() {};
public static Singleton getInstance() {
if (instance==null) {
synchronized(Singleton.class) {
if (instance==null)
instance=new Singleton();
}
}
return instance;
}
}
但这种两次判断的方式还是有可能出问题。因为“instance=new Singleton();”这段代码并不是一条唯一的指令,实际上这段代码会编译成多条指令,大致上做了3件事:
(1)给Singleton实例分配内存
(2)调用Singleton()构造函数,初始化成员字段
(3)将instance对象指向分配的内存
而且上面的(2)和(3)的顺序无法得到保证的,虚拟机可能先初始化实例字段再把instance指向具体的内存实例,也可能先把instance指向内存实例再对实例进行初始化成员字段。
当然这请情况很少见,不过我还是听一个同事讲过,他有遇到了用这种两次判断的方法还是有多个实例。
标准答案:我们可以在两次判断的基础上,使用“volatile”关键字来修饰instance,保证instance实例的唯一。
public class Singleton{
private volatile static Singleton instance;
private Singleton() {};
public static Singleton getInstance() {
if (instance==null) {
synchronized(Singleton.class) {
if (instance==null)
instance=new Singleton();
}
}
return instance;
}
}
面试题: Android较常用到的设计模式?
标准答案:
适配器模式:GridView、ListView的Adapter;
建造者模式:AlertDialog.Builder;
观察者模式:ListView的adapter.notifyDataSetChanged;
责任链模式:View的事件分发;
(当然还有很多,列出你熟悉的就好。)
相关面试题:
Android面试一天一题(Day 37:一套高级工程师的面试题)
Android面试一天一题(Day 33:Android开发的套路MVP & MVVM)