声明:
①此篇博文参照《设计模式之禅》,并对其中的demo场景做了相应的修改
②此篇博文是分析观察者设计模式的从无到有。其中有四个版本的代码的演进,另外还包括java jdk提供的Obsevable.java
和Obsever.java的使用的一个版本以及对其源码的简单分析。
③如果只想使用ava jdk的观察者api,直接看demo源码中com.hym.observe;如果想自定义观察者设计模式的应用
直接看demo源码中的com.hym.myobserve4
④不能为了设计模式而使用设计模式,模式的使用一定是符合业务场景或者代码优化的需要,是为解决问题而生的
强烈建议:看四个版本的演进过程。这四个版本分别解决了不同的问题,涉及一些软件工程的设计思想,比如开闭原则
单一职责原则等。
一.
com.hym.myobserve实现
IHanFeiZi.java:
HanFeiZi.java:
代码编写场景:
韩非子和李斯的间谍故事
李斯是秦国的上尉,需要安插间谍到各个国家,以便获取信息。韩非子做为韩国的重量级人物,韩非子身边自然
有很多间谍。李斯对韩非子的信息,了如指掌,那么相隔千里,是如何实现的呢?
答案:安插间谍。
|
主程client.java:
package com.hym.myobserve;
/**
* 测试主程
* */
public class Client {
public static void main(String[] args) throws InterruptedException {
LiSi liSi=new LiSi();
HanFeiZi hanFeiZi=new HanFeiZi();
//观察吃早餐
Spy watchBreakfast=new Spy(hanFeiZi, liSi, "breakfast");
//观察娱乐
Spy watchFun=new Spy(hanFeiZi, liSi, "fun");
//此处的问题//
//开启了两个线程,两个线程开启了两个死循环,一直在执行实现监控
//这样会比较消耗cpu的资源。
/
watchBreakfast.start();
watchFun.start();
//主线程等待1s韩非子吃饭
Thread.sleep(1000);
hanFeiZi.haveBreakfast();
Thread.sleep(1000);
hanFeiZi.haveFun();
}
}
IHanFeiZi.java:
public interface IHanFeiZi {
/**吃早饭*/
public void haveBreakfast();
/**娱乐活动*/
public void haveFun();
}
HanFeiZi.java:
package com.hym.myobserve;
/**
*韩非子类,继承韩非子接口,添加两个标示状态的属性
* */
public class HanFeiZi implements IHanFeiZi{
private boolean isHaveBreakfast=false;
private boolean isHaveFun=false;
@Override
public void haveBreakfast() {
System.out.println("韩非子:开始吃饭啦");
isHaveBreakfast=true;
}
@Override
public void haveFun() {
System.out.println("韩非子:开始娱乐了");
isHaveFun=true;
}
public boolean isHaveBreakfast() {
return isHaveBreakfast;
}
public void setHaveBreakfast(boolean isHaveBreakfast) {
this.isHaveBreakfast = isHaveBreakfast;
}
public boolean isHaveFun() {
return isHaveFun;
}
public void setHaveFun(boolean isHaveFun) {
this.isHaveFun = isHaveFun;
}
}
ILisi.java:
Lisi.java:
Spy.java:
IHanFeiZi.java:
HanFeiZi.java:
ILisi.java:
Lisi.java:
优点:将观察者李斯和被观察者韩非子直接建立关系,直接触发观察者的更新。无死循环的spy类了
/**
*
* 抽象观察者对象李斯
* */
public interface ILisi {
/**发现别人有动静,自己也要行动起来*/
public void update(String context);
}
Lisi.java:
package com.hym.myobserve;
/**
* 观察者对象李斯
* */
public class LiSi implements ILisi{
//李斯是个观察者对象,一旦发现情况,需要要boss进行汇报
@Override
public void update(String context) {
System.out.println("李斯观察到韩非子有活动了,并向秦始皇汇报");
reportToQinShiHuang(context);
System.out.println("李斯报告秦始皇:汇报完毕");
}
private void reportToQinShiHuang(String reportContext){
System.out.println("李斯报告秦始皇:韩非子有活动了---->"+reportContext);
}
}
Spy.java:
package com.hym.myobserve;
/**
* 间谍程序,监控程序是一个线程类
* */
public class Spy extends Thread {
private HanFeiZi hanFeiZi;
private LiSi liSi;
private String type;
/**通过构造函数,构造谁被观察,观察谁,观察什么*/
public Spy(HanFeiZi hanFeiZi,LiSi liSi,String type){
this.hanFeiZi=hanFeiZi;
this.liSi=liSi;
this.type=type;
}
@Override
public void run() {
//用死循环来保证程序一直在监控状态
while(true){
//监控是否在吃早餐
if(this.type.equals("breakfast")){
if(hanFeiZi.isHaveBreakfast()){
this.liSi.update("韩非子在吃饭");
this.hanFeiZi.setHaveBreakfast(false);
}
}else{//监控是否在娱乐
if(hanFeiZi.isHaveFun()){
this.liSi.update("韩非子在娱乐");
this.hanFeiZi.setHaveFun(false);
}
}
}
}
}
基于以上场景,编写com.hym.myobserve 包中的相关实现。
com.hym.myobserve版本非最终版本,为实现版本的第一版。
在此版本中发现问题:spy类的间谍对象,使用一个死循环在不断的获取被观察者的状态的改变和触发观察者的upd
ate的更新。
缺点:死循环会严重的消耗性能
改进:李斯作为观察者,韩非子做为被观察者,韩非子有动作,就要触发李斯的更新行为。所以新的设计方式是直
接将李斯聚集到韩非子的类中。直接使用持有李斯的引用调用李斯的方法。
这样spy类就没有用了,修改成韩非子的对象中持有李斯对象的引用,通过业务代码直接实现间谍的作用。
com.hym.myobserve2为改进实现版 ,为实现版本的第二版
二.
com.hym.myobserve2实现
场景中去掉了间谍。
主程client.java:
public class Client {
public static void main(String[] args) throws InterruptedException {
//定义韩非子
HanFeiZi hanFeiZi=new HanFeiZi();
//韩非子吃饭
hanFeiZi.haveBreakfast();
//韩非子娱乐
hanFeiZi.haveFun();
//==>主程类非常简单,但是发现李斯都没有在主程中声明。而是随着韩非子的上场而上场的。
//但是这是不符合场景的,就好比一部电视剧一样,不是所欲相关联的人物,从第一集就都要
//上场,而且建立关系。所以有第三个版本的改造。
}
}
public interface IHanFeiZi {
/**吃早饭*/
public void haveBreakfast();
/**娱乐活动*/
public void haveFun();
}
HanFeiZi.java:
package com.hym.myobserve2;
import java.awt.List;
/**
*韩非子类,继承韩非子接口,添加两个标示状态的属性
* */
public class HanFeiZi implements IHanFeiZi{
//声明李斯
private ILisi lisi=new LiSi();
@Override
public void haveBreakfast() {
System.out.println("韩非子:开始吃饭啦");
lisi.update("韩非子在吃饭");
}
@Override
public void haveFun() {
System.out.println("韩非子:开始娱乐了");
lisi.update("韩非子在娱乐");
}
}
ILisi.java:
/**
*
* 抽象观察者对象李斯
* */
public interface ILisi {
/**发现别人有动静,自己也要行动起来*/
public void update(String context);
}
Lisi.java:
package com.hym.myobserve2;
/**
* 观察者对象李斯
* */
public class LiSi implements ILisi{
//李斯是个观察者对象,一旦发现情况,需要要boss进行汇报
@Override
public void update(String context) {
System.out.println("李斯观察到韩非子有活动了,并向秦始皇汇报");
reportToQinShiHuang(context);
System.out.println("李斯报告秦始皇:汇报完毕");
}
private void reportToQinShiHuang(String reportContext){
System.out.println("李斯报告秦始皇:韩非子有活动了---->"+reportContext);
}
}
优点:将观察者李斯和被观察者韩非子直接建立关系,直接触发观察者的更新。无死循环的spy类了
缺点:主程类非常简单,但是发现李斯都没有在主程中声明。而是随着韩非子的上场而上场的。
但是这是不符合场景的,就好比一部电视剧一样,不是所欲相关联的人物,从第一集就都要上
场,而且建立关系。所以有第三个版本的改造。
上述这种方式,是韩非子(被观察者)和李斯(观察者)之间存在较强的依赖,耦合性太强
优化:让被观察者和被观察者的关系建立的时机独立化。
三.
com.hym.myobserve3实现
主程client.java:
IHanFeiZi.java:
HanFeiZi.java:
ILisi.java:
Lisi.java:
主程Client.java:
IHanFeiZi.java:
HanFeiZi.java:
IObserver.java:
Lisi.java:
IObservable.java:
Wangsi.java:
ObservableMoudle.java:
ObserverMoudle .java:
package com.hym.myobserve3;
/**
* 测试主程
*
* */
public class Client {
/**韩非子生活的总次数*/
private static int sum=0;
public static void main(String[] args) throws InterruptedException {
// 定义韩非子
HanFeiZi hanFeiZi = new HanFeiZi();
startHanFeiziLive(hanFeiZi,2);
//韩非子生活了一段时间李斯出现了,并且开始监控
System.out.println("李斯开始监控韩非子");
LiSi liSi = new LiSi();
//设置监控
hanFeiZi.setILisi(liSi);
//韩非子被监视(观察)过后的一段生活
startHanFeiziLive(hanFeiZi,3);
//突然某一天秦王就把李斯给啥了
kill(liSi,hanFeiZi);
startHanFeiziLive(hanFeiZi,2);
System.out.println("韩非子一共娱乐或吃放了"+sum+"次");
}
/**
*
* @param hanFeiZi
* @param count 娱乐和吃饭的总次数
* @throws InterruptedException
*/
private static void startHanFeiziLive(HanFeiZi hanFeiZi,int count)
throws InterruptedException {
// 为模拟业务,此处用循环来模拟李斯10次吃饭和10次娱乐
for (int i = 0; i < count; i++) {
sum++;
// 前10次i是偶数的时候李斯吃饭
if (i < count/2) {
// 后10次i是奇数的时候李斯吃饭
if (i % 2 == 0) {
// 韩非子吃饭
hanFeiZi.haveBreakfast(sum);
} else {
hanFeiZi.haveFun(sum);
}
} else {
// 后10次i是奇数的时候李斯吃饭
if (i % 2 == 1) {
// 韩非子吃饭
hanFeiZi.haveBreakfast(sum);
} else {
hanFeiZi.haveFun(sum);
}
}
Thread.sleep(1000 * 2);
}
}
/**
* 李斯被杀害
* @param lisi
*/
public static void kill(ILisi lisi,HanFeiZi hanFeiZi){
lisi=null;
//韩非子不能再持有lisi的应用,否则java回收机制不能及时回收lisi实例
hanFeiZi.setILisi(null);//实际开发中可以提供remove方法
}
}
IHanFeiZi.java:
public interface IHanFeiZi {
/**吃早饭*/
public void haveBreakfast(int i);
/**娱乐活动*/
public void haveFun(int i);
}
HanFeiZi.java:
/**
* 韩非子类,继承韩非子接口,添加两个标示状态的属性
* */
public class HanFeiZi implements IHanFeiZi {
// 为了让李斯的对象不是随着HanFeiZi对象的创建而创建,所以不能直接创建对象
// 而是在李斯对象在实际业务中被创建的时候,让韩非子对象才真正持有李斯对象
// 所以使用全部变量和set方法实现
// private ILisi lisi=new LiSi();
private ILisi mILisi;
public void setILisi(ILisi lisi) {
mILisi = lisi;
}
@Override
public void haveBreakfast(int time) {
System.out.println("韩非子:开始吃饭啦"+time);
if (mILisi != null)
mILisi.update("韩非子在吃饭");
}
@Override
public void haveFun(int time) {
System.out.println("韩非子:开始娱乐了"+time);
if (mILisi != null)
mILisi.update("韩非子在娱乐");
}
}
ILisi.java:
public interface ILisi {
/**发现别人有动静,自己也要行动起来*/
public void update(String context);
}
Lisi.java:
public class LiSi implements ILisi{
//李斯是个观察者对象,一旦发现情况,需要要boss进行汇报
@Override
public void update(String context) {
System.out.println("李斯观察到韩非子有活动了,并向秦始皇汇报");
reportToQinShiHuang(context);
System.out.println("李斯报告秦始皇:汇报完毕");
}
private void reportToQinShiHuang(String reportContext){
System.out.println("李斯报告秦始皇:韩非子有活动了---->"+reportContext);
}
}
第三个版本我们把业务场景变化一下:
韩非子其实一直就具备吃饭和娱乐的能力,只是在很长一段时间,没有被李斯监视(观察)过。
突然秦国觉得李斯这个人很重要,于是李斯这么个人要观察韩非子的行为,从而告知秦王。
|
代码运行的结果:
第三版的实现运行后的场景表现:
韩非子:开始吃饭啦1
韩非子:开始吃饭啦2
李斯开始监控韩非子
韩非子:开始吃饭啦3
李斯观察到韩非子有活动了,并向秦始皇汇报
李斯报告秦始皇:韩非子有活动了---->韩非子在吃饭
李斯报告秦始皇:汇报完毕
韩非子:开始吃饭啦4
李斯观察到韩非子有活动了,并向秦始皇汇报
李斯报告秦始皇:韩非子有活动了---->韩非子在吃饭
李斯报告秦始皇:汇报完毕
韩非子:开始娱乐了5
李斯观察到韩非子有活动了,并向秦始皇汇报
李斯报告秦始皇:韩非子有活动了---->韩非子在娱乐
李斯报告秦始皇:汇报完毕
韩非子:开始吃饭啦6
韩非子:开始吃饭啦7
韩非子一共娱乐或吃放了7次
上述打印结果也说明了,观察者,可以在任意时刻开始观察,也可以在任意时刻不在观察。
对于韩非子和李斯这件事情的场景看似已经很完美了。
缺点:随着韩非子的历史地位越来越重要,想要观察韩非子活动的人越来越多。而且想要观察的行为也可能增多。
这时候,基于第三版,是不是我要每次都set观察者,然候韩非子的行为发生已有,要调用很多不同的观察者的
update的方法。如下:
lisi.update;
wangsi.update;
...
这样每次添加新的观察者的时候,都要去修改HanFeiZi类。
软件工程中的开闭原则:
遵循开闭原则设计出的模块具有两个主要特征:[1]
(1)对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我
们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。
(2)对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二
进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者.EXE文件,都无需改动。
所以当上面这种场景出现的时候,不仅代码上显得冗余,同时也违背软件工程设计的开闭原则。
优化方案:那么对上述场景的优化方案,需要满足两个条件,才算是合理的优化。
即
①可以对现有的模块进行扩展(添加新的观察者)
②不需要去修改被观察者源代码。
那么实现版本的第四个版本来解决上述问题。
四.
com.hym.myobserve4实现
那么在第三个版本中,主要违背开闭原则的地方,就是添加新的观察者的时候,李斯的对象内部,必须添加多个观察
者对象的update方法,来实现观察者被被观察者触发的行为。
思考:解决上述问题
①通过现有的模块进行扩展,接口的思想(面向接口编程,抽象耦合)
②多个观察者出现的时候,无需修改被观察者的源码,通过持有集合的方式解决
具体看com.hym.myoberve4
添加IObserver接口,新的观察者实现此接口。
添加IObservable接口,新的被观察者实现此接口。
package com.hym.myobserve4;
/**
* 测试主程
*
* */
public class Client {
/**韩非子生活的总次数*/
private static int sum=0;
public static void main(String[] args) throws InterruptedException {
// 定义韩非子
HanFeiZi hanFeiZi = new HanFeiZi();
startHanFeiziLive(hanFeiZi,2);
//韩非子生活了一段时间李斯出现了,并且开始监控
System.out.println("李斯开始监控韩非子");
LiSi liSi = new LiSi();
//设置监控
hanFeiZi.addObsever(liSi);
//韩非子被监视(观察)过后的一段生活
startHanFeiziLive(hanFeiZi,3);
//突然某一天秦王就把李斯给杀了
kill(liSi,hanFeiZi);
startHanFeiziLive(hanFeiZi,2);
System.out.println("韩非子一共娱乐或吃放了"+sum+"次");
//场景中有了新的业务,秦国的李斯被杀过后,大王派王思观察HanFeizi的吃饭娱乐行为,
//同时要观察HanFeiZi是否有把妹行为。
WangSi wangSi=new WangSi();
hanFeiZi.addObsever(wangSi);
hanFeiZi.baMeizi();
//扩展新的观查者,对被观察的行为进行扩展都非常方便,而且观察者和被观察者的通用接口
//无需做任何修改,代码维护和扩展都很便捷,符合了软件设计中遵循的开闭原则
}
/**
*
* @param hanFeiZi
* @param count 娱乐和吃饭的总次数
* @throws InterruptedException
*/
private static void startHanFeiziLive(HanFeiZi hanFeiZi,int count)
throws InterruptedException {
// 为模拟业务,此处用循环来模拟李斯10次吃饭和10次娱乐
for (int i = 0; i < count; i++) {
sum++;
// 前10次i是偶数的时候李斯吃饭
if (i < count/2) {
// 后10次i是奇数的时候李斯吃饭
if (i % 2 == 0) {
// 韩非子吃饭
hanFeiZi.haveBreakfast(sum);
} else {
hanFeiZi.haveFun(sum);
}
} else {
// 后10次i是奇数的时候李斯吃饭
if (i % 2 == 1) {
// 韩非子吃饭
hanFeiZi.haveBreakfast(sum);
} else {
hanFeiZi.haveFun(sum);
}
}
Thread.sleep(1000 * 2);
}
}
/**
* 李斯被杀害
* @param lisi
*/
public static void kill(IObserver lisi,HanFeiZi hanFeiZi){
System.out.println("======观察者李斯被杀害========");
//韩非子不能再持有lisi的引用,否则java回收机制不能及时回收lisi实例
//hanFeiZi.setILisi(null);//实际开发中可以提供remove方法
hanFeiZi.mIObservers.remove(lisi);
lisi=null;
}
}
IHanFeiZi.java:
public interface IHanFeiZi {
/**吃早饭*/
public void haveBreakfast(int i);
/**娱乐活动*/
public void haveFun(int i);
}
HanFeiZi.java:
package com.hym.myobserve4;
import java.util.ArrayList;
import java.util.List;
/**
* 韩非子类,继承韩非子接口,添加两个标示状态的属性
**/
public class HanFeiZi implements IObservable,IHanFeiZi {
// 为了让李斯的对象不是随着HanFeiZi对象的创建而创建,所以不能直接创建对象
// 而是在李斯对象在实际业务中被创建的时候,让韩非子对象才真正持有李斯对象
// 所以使用全部变量和set方法实现
// private ILisi lisi=new LiSi();
List<IObserver> mIObservers= new ArrayList<IObserver>();
@Override
public void haveBreakfast(int time) {
System.out.println("韩非子:开始吃饭啦"+time);
notifyObsevers("吃饭");
}
@Override
public void haveFun(int time) {
System.out.println("韩非子:开始娱乐了"+time);
notifyObsevers("娱乐");
}
/**
* 添加了把妹的新能力,如果把妹能力通用,可以提到接口中去
*/
public void baMeizi(){
System.out.println("韩非子正在把妹");
notifyObsevers("韩非子正在把妹");
}
@Override
public void addObsever(IObserver iObserver) {
mIObservers.add(iObserver);
}
@Override
public void deleteObsever(IObserver iObserver) {
mIObservers.remove(iObserver);
}
@Override
public void notifyObsevers(String context) {
if (mIObservers != null&&mIObservers.size()>0){
for (IObserver iObserver : mIObservers) {
iObserver.update("韩非子在"+context);
}
}
}
}
IObserver.java:
/**
*
* 抽象观察者对象李斯<br>
* 将接口修改为通用的观察者接口
*
* */
public interface IObserver {
/**发现别人有动静,自己也要行动起来*/
public void update(String context);
}
Lisi.java:
package com.hym.myobserve4;
/**
* 观察者对象李斯
* */
public class LiSi implements IObserver{
//李斯是个观察者对象,一旦发现情况,需要要boss进行汇报
@Override
public void update(String context) {
System.out.println("李斯观察到韩非子有活动了,并向秦始皇汇报");
reportToQinShiHuang(context);
System.out.println("李斯报告秦始皇:汇报完毕");
}
private void reportToQinShiHuang(String reportContext){
System.out.println("李斯报告秦始皇:韩非子有活动了---->"+reportContext);
}
}
IObservable.java:
package com.hym.myobserve4;
/**
* 被观察者接口,这样将实际业务和观察者模型也实现了分离。
* 此接口提供被观察者相关功能的声明
*/
public interface IObservable {
/**
* 给被观察者添加观察者对象
* @param iObserver
*/
public void addObsever(IObserver iObserver);
/**
* 给被观察者删除观察者对象
* @param iObserver
*/
public void deleteObsever(IObserver iObserver);
/**
* 通知所有观察者,触发观察者行为
* @param context
*/
public void notifyObsevers(String context);
}
Wangsi.java:
public class WangSi implements IObserver{
@Override
public void update(String context) {
System.out.println(context);
}
}
上述过程就是观察者设计模式代码演进的过程,在java jdk中已经提供了相关的api
五.com.hym.observe
Client.java:/**
* 测试主程
*
* */
public class Client {
public static void main(String[] args) throws InterruptedException {
ObservableModle observableModle=new ObservableModle();
ObserverModle observerModle=new ObserverModle();
observableModle.addObserver(observerModle);
observableModle.doSometing("做一些事情..");
}
}
ObservableMoudle.java:
package com.hym.observe;
import java.util.Observable;
import java.util.Observer;
/**
*
* @description:
* <p>
* 可被观察的模型,模式的运用场景分析
* </p>
* @author: HuYuMian
*
* @QQ:732476506
*/
public class ObservableModle extends Observable{
@Override
public synchronized void addObserver(Observer o) {
super.addObserver(o);
}
@Override
public synchronized void deleteObserver(Observer o) {
super.deleteObserver(o);
}
@Override
public void notifyObservers() {
super.notifyObservers();
}
public void doSometing(String context){
System.out.println("被观察者"+context);
/*if (!changed)
return;
*/
//上面这部分代码在observable.java的源码中,有判断处理。默认值是false,不执行update方法。
//所以使用java本身的observable.java的api,通知观察者对象update方法调用,必须先执行setChanged
//方法,并在执行notifyObservers方法后,会被重置,调用protected synchronized void clearChanged() {
//changed = false;
//}
setChanged();
notifyObservers(context);
}
}
ObserverMoudle .java:
public class ObserverModle implements Observer{
//参数说明:Observable o:被观察者对象
//Object arg:数据对象
@Override
public void update(Observable o, Object arg) {
System.out.println("观察者"+arg.toString());
}
}
//相关源码的分析
jdk中的observable中使用vector存储观察者对象。
jdk中,notifyObsevers方法调用之前,使用setChanged,在代码注释中已有体现。
其余的基本上思路是一致的。
一些总结:
观察者模式的应用
1.观察者模式的优点:
①观察者和被观察者之间是抽象耦合。
观察者中通知行为,依赖于IObsever接口。而不依赖于某一具体的观察者对象。
耦合有三种:
a.零耦合 实际业务中无关
b.具体耦合 类与类之间有依赖关系,一个类中,持有另一个具体类的应用
c.抽象耦合 这种关系发生在一个具体类和一个抽象类或者接口之间,这样就使
必须发生关系的类之间保持最大的灵活性。
所以提倡面向接口编程。
②建立一套触发机制
可以设计既是观察者有是被观察者的实际类,来实现一套流程的触发机制。
-------------------------------------------------------------------------------
2.观察者设计模式的缺点
java的消息通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率。
-------------------------------------------------------------------------------
3.观察者使用场景
①关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合关系”。
②事件多级触发
③扩系统的消息交换场景,如消息队列的处理机制
-------------------------------------------------------------------------------
在android 开发中也有很多基于观察者设计模式的应用。
比如一些事件总线的框架:
我只使用过EventBus,后期准备尝试RxAndroid
另外,listview的适配器数据更新,或者自己业务中需要处理的一些场景,使用观察者设计模式都能进行更好的解决
附上我的设计模式的os git上的地址:
啰嗦一句:想要更好的使用观察者设计模式来解决实际开发中遇到的问题,或者优化现有代码的扩展性和维护性,需要
在开发的过程之中,多思考。在必要的时候,使用它,能够事半功倍。