孟远

每天进步一点点


  • 首页

  • 关于

  • 标签

  • 归档

程序畅通执行-命令模式

发表于 2020-09-09

模式介绍

命令模式指的是将每一种请求封装成一个对象,把客户端参数化,当用户使用不同的请求时,对请求进行排队或者记录请求日志,以及支持可撤销的操作。

我们平时接触最多的命令模式就是菜单命令了,比如操作系统中的关机操作,我们点击“关机”后,系统会执行一系列的操作,这一系列的操作,对于用户而言,用户根本不了解点击“关机”后,程序执行了什么操作。用户要知道的,只是想要关闭电脑时,去点击一下“关机”就可以。

命令模式相对于其他模式而言,没有那么多的条条框框,不过正是因为这一点,命令模式相对于其他的设计模式更为灵活多变。

使用场景

  • 需要抽象出待执行的操作,然后以参数的形式提供出来。
  • 在不同的时刻指定、排列和执行请求。一个命令对象可以有与初始请求无关的生存期。
  • 需要支持取消操作。
  • 支持修改日志功能。
  • 支持事务操作。

模式构成

  • Receiver:真正执行具体命令的核心类。
  • Command:抽象的命令接口。
  • ConcreateCommand:命令接口的具体实现类,持有Receiver。
  • Invoker:请求者类,持有Command。
  • Client:客户端角色,发出命令的地方,比如“关机”一例中,客户端就是我们自己。

模式示例

命令模式总体来说并不难,只是比较繁琐,一个简单的调用关系,被解耦成多个部分,必定会增加类的复杂度,但是即便如此,命令模式的结构也是合理并清晰的。

大家应该都玩过《俄罗斯方块》,这款游戏中有四个核心指令,那就是上下左右,左右控制左右平移,上控制变形,下控制快速下落,这次我们就模拟这款小游戏。

首先是Receiver,也就是执行具体命令的核心类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TetrisReceiver {

public String left(){
return "左移";
}

public String right(){
return "右移";
}

public String top(){
return "变形!";
}

public String bottom(){
return "加速下落!";
}
}

接下来我们定义一个接口,作为《俄罗斯方块》命令角色的抽象:

1
2
3
4
public interface TetrisCommand {

String execute();
}

有了命令抽象,就开始创建具体的命令类,它将持有Receiver:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandLeft implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandLeft(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.left();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandRight implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandRight(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.right();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandBottom implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandBottom(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.bottom();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandTop implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandTop(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.top();
}
}

对于请求者Invoker,我们这里用一个Button类来表示,命令由按钮执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TetrisButtons {
private TetrisCommand commandLeft;
private TetrisCommand commandRight;
private TetrisCommand commandTop;
private TetrisCommand commandBottom;


public void setCommandLeft(TetrisCommand commandLeft) {
this.commandLeft = commandLeft;
}

public void setCommandRight(TetrisCommand commandRight) {
this.commandRight = commandRight;
}

public void setCommandTop(TetrisCommand commandTop) {
this.commandTop = commandTop;
}

public void setCommandBottom(TetrisCommand commandBottom) {
this.commandBottom = commandBottom;
}


public String toLeft() {
return commandLeft.execute();
}


public String toRight() {
return commandRight.execute();
}


public String toBottom() {
return commandBottom.execute();
}


public String toTop() {
return commandTop.execute();
}
}

最后,由客户端来决定,调用哪些命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//首先要有《俄罗斯方块游戏》
TetrisReceiver receiver = new TetrisReceiver();
//我们创建好定义好的命令
TetrisCommand commandLeft = new TetrisCommandLeft(receiver);
TetrisCommand commandRight = new TetrisCommandRight(receiver);
TetrisCommand commandBottom = new TetrisCommandBottom(receiver);
TetrisCommand commandTop = new TetrisCommandTop(receiver);
//将命令统一封装到按钮中
TetrisButtons buttons = new TetrisButtons();
buttons.setCommandLeft(commandLeft);
buttons.setCommandRight(commandRight);
buttons.setCommandBottom(commandBottom);
buttons.setCommandTop(commandTop);
//具体按下哪个按钮,由用户决定
buttons.toTop();
buttons.toTop();
buttons.toTop();
buttons.toTop();
buttons.toLeft();
buttons.toRight();
buttons.toRight();
buttons.toBottom();

总结

看了上述的案例,大家肯定觉得是一篇长篇代码文,明明是一个很简单的调用逻辑,为何要做的如此复杂?对于大部分开发者来说,可能更愿意接受这样的代码:

1
2
3
4
5
6
7
8
9
//创建游戏
TetrisReceiver receiver = new TetrisReceiver();
//实现什么操作,直接调用相关函数
receiver.top();
receiver.top();
receiver.top();
receiver.top();
receiver.left();
receiver.right();

上面这样写,确实会很方便,但是这样的逻辑留给后来者,没有人会觉得方便,日后维护修改,也会有许多不可控的隐患。

命令模式调用逻辑做的如此复杂,主要是遵循了设计模式当中重要的原则:对修改关闭,对扩展开放。

除此之外,使用命令模式的另一个好处是可以实现命令的记录功能。比如在上面的例子中,我们在请求者Invoker中使用一个数据结构来存储执行过程中的命令对象,以此可以方便地知道刚刚执行过那些命令,并可在必要时恢复、撤销,具体代码大家可以自行尝试。

命令模式充分体现了几乎是所有设计模式的通病:类数量的膨胀,大量衍生类的创建。

其实这是一个不可避免的问题,因为这样做带给我们的好处非常多:更弱的耦合性、更灵活的控制性、更好的扩展性。

不过,在实际开发中,是不是采用命令模式还是需要斟酌。


感谢

《Android源码设计模式解析与实战》 何红辉、关爱民 著

提升代码灵活性-责任链模式

发表于 2017-11-16

模式介绍

责任链模式是行为设计模式之一。

首先我们从字面去理解责任链模式:“责任”指的是一个人应尽的义务、分内应做的事;“链”指的是一个个小环首尾相连,组成的长链条。

为什么是“链”?因为“链”的每一环都是可以拆卸的,它们虽然是环环相扣,但是哪天我不想要中间的某一环,我只需要将其去下来即可,这大大提升了代码的灵活性。

所以责任链模式通俗来讲,指的是一件事情,有顺序地传递给一群人,当第一个人无法处理时,再传递给第二个人去做,直到有人可以解决掉这件事情为止。

模式示例

小明是一家技术公司的测试,他除了完成本职工作外,还负责给技术部的小伙伴购买零食,不过零食钱是先由小明自己垫付的,接着到月底拿着发票去找领导报销的流程。

这个月的月底,小明算了一下,一共花费了2450元。

接着小明拿着零食的发票去找组长报销,结果组长说这金额太大了,只有500以下我才有权限签字,你得去找技术总监。

接着小明找到技术总监,总监说2450太多啦,只有2000以下我才有权限签字,你得去找咱们的老板。

接着小明去到老板的办公室,老板看了一下说非常好,技术部的同学们都非常辛苦,以后都多买一些零食,接着麻溜地就签了字,小明顺利完成了此月的零食报销请求。

使用场景

通过上述示例,我相信大家已经有些理解责任链模式了。

组长、总监、老板针对小明的报销请求,都有自己要做的事情,但是根据小明的报销金额不同,无法第一时间就知道到底是谁来处理这个报销请求。

这就需要小明按照一定的顺序,一个个地去问,直到找到能处理此次报销请求的人为止。

这种情况下,就非常适合使用我们今天要说的责任链模式。

通过上面的描述,我们可以得出责任链模式的使用场景:

  • 一个请求需要多个对象处理,但具体由哪个对象处理需要在运行时动态判断时;
  • 需要动态指定一组对象处理一个请求。

所含角色

责任链模式包含两个主要角色:

  • 处理者抽象(Handler):抽象的处理者,声明一个处理请求的方法,并在其中保持对下一个处理者的引用。
  • 处理者实现(HandlerImpl):处理者抽象的具体实现,对请求进行处理,如果无法处理,则通过下一个处理者的引用将其转发下去。

具体代码

针对上述示例和角色描述,我们来将其转化成具体的代码。

首先是处理者的抽象,针对上述示例,我们的抽象的请求处理方法应该为报销:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public abstract class Leader {
//下一个处理者
private Leader nextLeader;

/**
* 处理报销请求
*
* @param money 申请报销的金额
*/
public final void handlerRequest(int money) {
if (money <= getSelfLimit()) {
//当申请的金额<=自己的处理额度时,将此次请求消化
handler(money);
} else {
//否则交给下一个处理者来处理
if (nextLeader != null) {
nextLeader.handlerRequest(money);
}
}

}

/**
* 获取自身的额度,由具体实现来设置
*
* @return 自身能处理的额度
*/
public abstract int getSelfLimit();

/**
* 具体处理逻辑在这里实现
*
* @param money 申请报销的金额
*/
public abstract void handler(int money);


public void setNextLeader(Leader nextLeader) {
this.nextLeader = nextLeader;
}

public Leader getNextLeader() {
return nextLeader;
}
}

代码还是有些多的,让我们来解释一下这个处理者抽象类:

  • nextLeader:指定了当请求无法处理时,下一级的处理者。这是责任链模式中,“链”的核心。对外暴露了set、get方法。
  • handlerRequest(int money):注意此方法不是抽象并且是final修饰的,也就是无法重写,无法修改。它其中的逻辑是用来判断此次请求,应该是由自己消化,还是分发给下一个处理者。当你要开始请求时,只需要调用此方法即可。为了适应我们举的示例,这里有一个money参数。这里说一下,每一种设计模式仅仅代表着一种编程思想,具体代码还是要看需求的,如果需求复杂,那么这里分发的逻辑也会复杂、参数也会更多,反之亦然。
  • getSelfLimit():抽象方法,指定处理者的最高处理额度。
  • handler(int money):当此次请求该由自己消化时,具体的消化逻辑在这里实现。

如果还有疑问,不要急,我们先来具体看一个处理者的实现,可能你就会恍然大悟:
组长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LeaderGroup extends Leader {

/**
* @return 组长报销的处理额度
*/
@Override
public int getSelfLimit() {
return 500;
}

/**
* 当报销金额 <= getSelfLimit() 时,请求会到这里来消化
*
* @param money 申请报销的金额
*/
@Override
public void handler(int money) {
Toast.makeText(App.context, "小钱,组长报销:" + money, Toast.LENGTH_SHORT).show();
}
}

组长继承了抽象的处理者,并指定了组长的报销额度以及具体的处理逻辑。
同样的,总监和老板也是一样的道理:
总监:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LeaderGeneral extends Leader {

/**
* @return 总监报销的处理额度
*/
@Override
public int getSelfLimit() {
return 2000;
}


/**
* 当报销金额 <= getSelfLimit() 时,请求会到这里来消化
*
* @param money 申请报销的金额
*/
@Override
public void handler(int money) {
Toast.makeText(App.context, "金额挺大,总监报销:" + money, Toast.LENGTH_SHORT).show();
}
}

老板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LeaderCEO extends Leader {

/**
* @return 老板报销的处理额度
*/
@Override
public int getSelfLimit() {
return Integer.MAX_VALUE;
}


/**
* 当报销金额 <= getSelfLimit() 时,请求会到这里来消化
*
* @param money 申请报销的金额
*/
@Override
public void handler(int money) {
Toast.makeText(App.context, "这么多钱,老板报销:" + money, Toast.LENGTH_SHORT).show();
}
}

至此,我们的处理者抽象,以及处理者实现都已经完成了,下面我们来具体模拟一下报销流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//获取输入框中输入的金额
String money = et_money.getText().toString();
if (TextUtils.isEmpty(money)) {
Toast.makeText(this, "请输入金额", Toast.LENGTH_SHORT).show();
return;
}
//组长实例
Leader group = new LeaderGroup();
//总监实例
Leader general = new LeaderGeneral();
//CEO实例
Leader CEO = new LeaderCEO();
//组长的下一级是总监
group.setNextLeader(general);
//总监的下一级是CEO
general.setNextLeader(CEO);
//由组长优先处理报销
group.handlerRequest(Integer.valueOf(money));

上面的测试代码逻辑也很清晰:

  1. 获取到具体的报销金额
  2. 首先创了组长、总监、CEO的实例
  3. 按照组长 -> 总监 -> 老板 的报销顺序将其进行排序
  4. 由组长优先处理报销请求,当组长无法处理时,会由组长实例中的下一个处理者来处理。

当我们需求发生变化,比如组长的报销金额变成了1000、比如现在组长没有报销权限了等等,你会发现非常地好维护。

这里我们假设总监无法报销了,当组长无法报销时,直接找到CEO来报销:

1
2
3
4
5
6
Leader group = new LeaderGroup();
Leader CEO = new LeaderCEO();

group.setNextLeader(CEO);

group.handlerRequest(Integer.valueOf(money));

我们仅仅是将组长的下一个处理者改为老板即可,就可以完美删除掉总监的存在。

这种“链”式的写法,完美解耦了请求者和处理者,提高代码了灵活性。

总结

到这里,一个最基本的责任链模式就完成了。

代码已经上传至GitHub。

责任链模式的优点已经说过了。

缺点就是处理者太多的话,必定会影响性能,尤其是在一些递归调用中,使用时一定要注意。

感谢

《Android源码设计模式解析与实战》 何红辉、关爱民 著

随时灵活变化-状态模式

发表于 2017-11-14

模式介绍

状态模式的结构和策略模式几乎一模一样。

但是它们两者的目的、本质却完全不同。

策略模式的行为是彼此独立,相互替换的。回想之前举的价格计算器,我们出行时想使用地铁,则使用地铁的价格计算器,如果使用出租车,则使用出租车的价格计算器……

而状态模式的行为则是平行的,不可替换的。状态模式更像是被封装在对象内部的一个东西,当对象的状态发生变化时,其行为也要发生变化。

使用场景

  • 一个对象的行为需要根据状态发生变化,尤其是在运行时状态发生变化,行为需要跟着一起变化时;
  • 代码中有大量判断语句(if、switch),且这些语句依赖该对象的状态。

模式角色

  • State:状态接口,定义所有行为的抽象。
  • ConcreteStateA、ConcreteStateB:某个状态的具体行为实现,一个状态对应一个具体行为实现。

模式示例

相信大家在开发中,只要是动态App(和服务器有交互),都会有登录的需求。

针对这个需求,我们可以得出两个状态:登录状态、未登录状态。

接着我们可以想象一些行为:

  • 登录
  • 退出登录
  • 查看账户金币
  • 赚取金币
  • 金币兑换道具

针对上述行为,在登录状态和未登录状态下,所做的事情是完全不同的:

  1. 登录状态:
    • 登录:提示用户已经登录过了,请勿重复登录
    • 退出登录:清除用户缓存并提示用户退出登录成功
    • 查看账户金币:从服务器查询用户余额
    • 赚取金币:跳转到赚金币页面
    • 金币兑换道具:跳转到兑换道具页
  2. 未登录状态:
    • 登录:判断用户输入的账号密码,正确提示用户登录成功。
    • 退出登录:提示用户已经退出过了。
    • 查看账户金币:提示用户请先登录
    • 赚取金币:跳转到赚金币页(这里也可以提示用户登录,具体看产品定的登录时机)
    • 金币兑换道具:提示用户请先登录

想必看完这个示例,大家已经对状态模式有了一些自己的见解。

这种情况下,非常适合使用状态模式。

如果项目中,还是通过判断语句来进行状态的区分,就说明这个项目没有很好地应用状态模式。

下面我们就来将上述案例转化成代码。

首先是状态行为的抽象,该抽象包含了所有的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface UserState {

//登录
void login();

//登出
void logout();

//查询余额
void seeMoney();

//赚金币
void earnMoney();

//兑换道具
void exchange();
}

针对我们上面描述的示例,有以上5种行为,我们将其进行了抽象。

接着来思考行为的具体实现:登录状态的具体实现、未登录状态的具体实现。

具体做起来也很简单,就是创建两个类去实现我们的行为抽象,在各自的实现下,去实现该状态下,应该做的事情。

首先来看登录状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LoginStateImpl implements UserState {
@Override
public void login() {
Toast.makeText(App.context, "您已经登录了,无需登录!", Toast.LENGTH_SHORT).show();
}

@Override
public void logout() {

Toast.makeText(App.context, "退出登录成功!", Toast.LENGTH_SHORT).show();
}

@Override
public void seeMoney() {
Toast.makeText(App.context, "您目前有100金币", Toast.LENGTH_SHORT).show();
}

@Override
public void earnMoney() {
Toast.makeText(App.context, "跳转到-赚金币", Toast.LENGTH_SHORT).show();
}

@Override
public void exchange() {
Toast.makeText(App.context, "跳转到-兑换道具", Toast.LENGTH_SHORT).show();
}
}

根据示例需求,我实现了登录状态下,这5种行为的具体实现。

未登录状态也是同理,这里就不列举代码了,可以直接查看GitHub。

接下来我们来看一下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//默认是未登录状态
private UserState userState = new LogutStateImpl();

@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.bt_login://登录行为
userState.login();
//核心:更换用户状态
userState = new LoginStateImpl();
break;
case R.id.bt_logout://退出登录行为
userState.logout();
userState = new LogutStateImpl();
break;
case R.id.bt_see_money://查看余额行为
userState.seeMoney();
break;
case R.id.bt_earn_money://赚取金币行为
userState.earnMoney();
break;
case R.id.bt_exchange://兑换行为
userState.exchange();
break;
}
}

这里也是利用多态的特性,根据状态的变化,注入不同的实现。

至此,我们已经使用状态模式成功实现了上述需求,并且去除了重复、杂乱的判断语句,体现出了状态模式的精髓。

总结

代码已经上传至GitHub,可以下载查阅。

其实看到这里,各位看官想必已经了解了状态模式。

状态模式的关键点在于:不同状态下、同一行为的不同响应。

状态模式是为了优化代码结构而产生的。我们通过if-else其实可以完美判断用户的登录状态,但是这种实现使得逻辑与具体行为耦合在一起,后期难以维护。

状态模式就是为了消除这种丑态,实现逻辑与行为的解耦。

当然并不是所有的if-else都适合使用状态模式,具体是否使用,还是由你来决定。

京东云主机使用(1)-使用Java+Tomcat开始开发

发表于 2017-08-24

前言

上一篇介绍了京东云主机的登录和简单的静态网页配置。

这一篇主要讲述服务器开发环境的配置和部署。

首先,实现服务器开发的语言特别多:Java、PHP、Node.JS、Python等。

这里我选择的是Java+Tomcat+IDEA。

所以本篇文章是围绕着这三者展开的。

并且我们的服务器系统是:Ubuntu 16.04 64。 没看上一篇的看官需要了解。

环境配置

Java环境配置

首先我们先输入用户名密码登录我们的服务器:

登录成功

接下来,我们先删除之前下载的apache2,这里我们已经用不到了:

1
sudo apt-get remove apache2

现在我们开始下载JDK,先切换目录:

1
cd /usr/local/

下载JDK:

1
sudo wget http://download.oracle.com/otn-pub/java/jdk/8u144-b01/090f390dda5b47b9b721c7dfaa008135/jdk-8u144-linux-x64.tar.gz

这个http链接对应的是写本篇文章时,最新版本的JDK地址。

下载完成后,将其解压:

1
tar -xvzf jdk-8u144-linux-x64.tar.gz

解压完成后,删除压缩包:

1
rm jdk-8u144-linux-x64.tar.gz

到这里,下载步骤就算完成了,接着开始环境变量的配置:

1
vim /etc/profile

Vim是Linux文件编辑模式,该命令会打开文件并可进行编辑。

该命令的功能非常强大,如果之前你没有见过该命令,这次可以先稍微了解一下。

如果非要现在学习vim,可以参考此文。

这里我简单介绍一下该命令。

Vim命令有三种模式:普通模式、编辑模式、命令模式。

顾名思义,想要编辑必须进入编辑模式,想要进行保存或是退出就要进入命令模式,平时浏览时处于普通模式即可。

这里我们以刚才执行的命令开始进行讲解。

在执行完vim /etc/profile后,会打开/etc/profile文件,此时默认处于普通模式。

我们只需一直往下滑动,或是使用Ctrl+F/D,滑动到文件底部。

接着将光标移动至内容的最下方,点击i/c/o进入编辑模式,复制以下内容:

1
2
3
4
5
6
JAVA_HOME=/usr/local/jdk1.8.0_144
JAVA_BIN=/usr/local/jdk1.8.0_144/bin
JRE_HOME=/usr/local/jdk1.8.0_144/jre
PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
CLASSPATH=:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
export JAVA_HOME JAVA_BIN JRE_HOME PATH CLASSPATH

如果各位看官修改了安装路径,这里注意要匹配。

复制完成后,点击Esc,即可退出编辑模式。

接着输入冒号(Shift+;),进入命令模式。

输入wq,然后回车,进行文件的保存并退出Vim模式。

至此,Java的环境变量应该已经配置成功,使用命令来刷新环境:

1
source /etc/profile

接着输入:

1
java -version

如果效果如下:

Java环境测试

代表环境变量已经配置成功。

如果失败,请检查环境变量的路径是否正确,或是JDK是否安装成功。

Tomcat环境配置

1
cd /usr/local/

下载Tomcat压缩包:

1
sudo wget http://archive.apache.org/dist/tomcat/tomcat-8/v8.5.20/bin/apache-tomcat-8.5.20.tar.gz

该包的版本是Tomcat8.5.20,如果不适合你,可以去Tomcat下载页自行选择。

接着进行解压:

1
tar zxvf apache-tomcat-8.5.20.tar.gz

解压完成后,这个文件的名字也太长了,我们重命名一下:

1
mv apache-tomcat-8.5.20 /usr/local/tomcat

接着拷贝catalina.sh

1
cp -p /usr/local/tomcat/bin/catalina.sh /etc/init.d/tomcat

开始配置环境:

1
vim /etc/init.d/tomcat

操作方式和之前描述的一样,在开头直接添加以下内容:

1
2
3
4
5
6
JAVA_HOME=/usr/local/jdk1.8.0_144
CATALINA_HOME=/usr/local/tomcat

chmod 755 /etc/init.d/tomcat
chkconfig --add tomcat
chkconfig tomcat on

至此,Tomcat的环境变量配置工作,已经完成。

下面我们来启动Tomcat:

1
service tomcat start

验证启动是否成功:

1
ps -ef|grep tomcat

如果输出内容如下,则代表启动成功:
Tomcat启动成功

如果输出内容如下,则代表失败:
Tomcat启动失败

启动失败的原因基本上都是因为环境配置的问题,请仔细检查路径配置。

启动成功后,在浏览器输入IP://8080,比如:

1
116.196.93.148:8080

即可看到如下效果:
Tomcat运行效果

有些人说,我不想输入端口号,怎么办?

1
vim /usr/local/tomcat/conf/server.xml

执行上述命令打开Tomcat的配置文件,找到如下内容:

server修改端口号

进入编辑模式将port 改为 80 即可。

如果你还买了域名,想要监听域名,找到如下内容:

server修改默认地址

进入编辑模式将name改为自己的域名即可。

修改完成后进入命令模式输入wq保存并退出。

接着重启Tomcat:

1
2
service tomcat stop
service tomcat start

之后再访问的话,就不用带端口号了。

如果你修改了Host-name,记得使用你修改的域名。

至此,我们服务器的环境搭建工作就告一段落了。

HelloWeb

接下来我们就要开始开发我们的Web项目了!

这里我下载了IntelliJ IDEA作为我的开发软件。

这里注意要下载商业版,也就是Ultimate版。

Community版是不支持Web开发的。

下载完成后,我们需要为本机也装上Java和Tomcat。

接着我们就可以开始开发HelloWorld了!

具体的下载和开发流程,我这里就不介绍了,参考此文。

我就是按照一步一步来进行开发的。

按照上述步骤开发完成后,我们的HelloWorld就已经在本机完成了。

效果如下:

localhost:8080

接着我们要将该项目部署到我们自己的服务器上,让所有人都可以访问!

最基本的方法就是将项目达成War包,将War包上传至Linux服务器的Tomcat/webapps文件夹中。

之后在访问时,Tomcat会自动将War包进行解压。

传输的方法,可以参考上次说到的Mac向服务器上传文件。

Windows的话可以使用Xshell。

这里我们就不做过多介绍了,因为这种方式明显过于繁琐,每次修改项目都要重新生成War包并上传服务器。

我希望我们可以像是在本机调试一样,修改完成后点击运行就可以看到效果。

这就是我们接下来要做的事情。

Hello 热部署

我们首先登录自己的Linux服务器,切换到自己的Tomcat的bin目录下:

1
cd /usr/local/tomcat/bin/

接下来打开VIM模式,开始编辑catalina.sh文件:

1
vim catalina.sh

在文件的100行,也就是# OS specific support. $var _must_ be set to either true or false.之上,添加如下代码:

1
2
3
4
5
6
7
8
9
10
export CATALINA_OPTS="-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=116.196.93.148"

export JAVA_OPTS="-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false"

其中要注意的是,hostname为自己的IP地址,两个port设置为和本机不冲突的端口号即可,注意此端口号不是Tomcat的访问端口,要进行区分,比如我设置的就是9999,你也可以选择使用它。

添加修改完成后保存退出,接下来要对自己的IP地址进行映射。先查看自己的用户名:

1
uname -n

将显示出来的用户名进行复制。

接下来打开hosts文件:

1
vim /etc/hosts

添加如下代码:

1
2
127.0.1.1      复制的用户名
::1 复制的用户名

注意不用删除和注释其他映射,只需添加即可。

映射完成后,我们需要重启Tomcat:

1
service tomcat stop

执行完后请确认是否关闭成功:

1
ps -ef|grep tomcat

如果输出

Tomcat关闭失败

则代表关闭失败,执行下句命令强制关闭:

1
2
//port为端口号,像上述图片中,端口为2392
kill -9 port

接下来启动Tomcat,因为我们修改了catalina.sh,所以我们不能使用startup.sh来启动Tomcat,需使用:

1
2
3
//注意此时的目录还是在Tomcat/bin目录下
// > /dev/null 2>&1 & 的含义是不显示启动日志
sh catalina.sh run > /dev/null 2>&1 &

确认启动成功:

1
ps -ef|grep tomcat

启动成功后,我们Linux服务器的配置就结束了!

我们回到IDEA中,进行IDEA的配置。

首先打开Configurations:

Configurations

接着创建远端Tomcat:

创建远端Tomcat

接着开始进行远端Tomcat的配置:

远端Tomcat配置

SFTP协议配置:

SFTP协议配置

接着添加部署项目:

添加项目

点击OK进行保存,接着我们选择使用远端Tomcat来部署并运行项目:

运行项目

如果出现以下效果:

运行失败

代表连接远端失败,请仔细检查你的配置,此过程比较耗时。

运行成功Log如下:

成功Log

效果如下:

成功效果

注意上图访问的IP,是我们Linux服务器的地址,也就代表我们成功将项目部署到了我们的服务器上。

接着我们修改index.jsp,再次运行项目:

热部署效果

可以看到我们修改的效果已经成功部署到服务器中。

上文中的IP就是我的服务器IP,大家也可以访问的。

总结

至此,我们已经成功使用IDEA+Tomcat+Java实现了项目的开发以及热部署。

接下来就比较自由了,自己的项目就由自己去写了。

可以搭建个人博客,可以搭建资源共享网页等等。

各有各的风格,大家可以自行学习后台开发,搭建过程就不准备再写博客了。

碰巧今天是七夕节,今天看文章的人都是敬业的程序员!

噗哈哈哈,祝大家七夕节开心快乐!

感谢

搭建JDK+Tomcat

Mac向服务器上传文件

idea部署项目到远程tomcat

Linux VIM命令使用

京东云主机使用(0)-搭建简单网页(macOS)

发表于 2017-08-23

前言

在郭霖大神的带领下,我花了一元钱入手了2个月的京东云主机,也就是个人服务器。

这是我人生第一台服务器,多么值得纪念。。。。。。

入手地址在这里

一直不买的原因也是因为自己的Android水平没有达标,不想去学其他方面的知识而分心。

其实很容易发现这他喵的就是一个不想学习的借口罢了!

更容易发现这明显是没钱买吧!

所以趁此机会,入手了2个月服务器来尝鲜。名额有限,说不定已经没有了。。。

购买流程就不说了,服务器系统选择的是Ubuntu 16.04 64位。

接下来的使用状况都是围绕着Ubuntu 16.04 64位展开的。

登录云主机

郭霖大神推荐了两款软件用于控制服务器 和 上传下载服务器文件:Xshell和Xftp。

但是两款软件都是Windows系统的,没有macOS系统。

如果你是Windows系统的,可移步郭霖大神的搭建教程,相对比较简单。

那么如何在macOS系统下操作服务器呢?

在京东云的帮助中心中,macOS系统的登录方式有两种:一种是VNC登录,一种是SSH密钥登录。

VNC登录

VNC登录是京东云为用户提供的一种通过Web浏览器连接服务器的方式。

很简单,就是在京东云的控制台点击远程连接即可。

接着打开了Ubuntu 16.04 64的控制台,需要先进行登录,用户名为root,密码发送到了你的邮箱和手机当中。
登录面板

如果想要修改密码,可在控制台-操作 进行修改。修改完成后记得重启生效。

输入完成并正确就登录上了服务器,非常简单。
登录成功

不过使用VNC登录的场景很少:

  • 查看云服务的启动进度

  • 无法通过其他登录方式登录时,才使用VNC来登录服务器

所以这种登录方式,体验体验即可,并不实用。

并且它不支持复制粘贴、不支持文件上传,而且是单点登录,使用起来简直是折磨。

SSH密钥登录

京东云帮助中心提供了SSH创建和登录教程。

成功设置SSH密钥后,我们就可以不使用VNC登录,直接在Mac的命令行就可以进行服务器的登录。

下面我们来一步一步设置SSH密钥:

什么是SSH密钥?

就我的理解而言,它是一种网络通讯协议,主要用于计算机之间的加密登录。

使用SSH登录的具体流程如下:

SSH密钥登录

可以看出一个SSH串要提供给服务器和本机,当SSH串匹配成功后,就可以实现免密登录。

这样的优点就是当登录请求被恶意拦截时,密码也不会泄露。

接下来,我们就要生成SSH密钥,并保存到本机和服务器。

要说一句的是,SSH密钥登录很多地方都有用到,比如GitHub。

如果你的电脑已经有SSH密钥,那么直接使用这个即可。在我的理解下,一台电脑只能有一个SSH密钥。

具体的SSH成功流程可参考GitHub官方教程。

在这里我也简单罗列一下SSH密钥的生成步骤:

1.校验本机是否已经生成SSH密钥:

1
ls -al ~/.ssh

如果输出了

1
2
3
4
id_dsa.pub
id_ecdsa.pub
id_ed25519.pub
id_rsa.pub

则代表已经生成过,直接跳过第二步,执行第三步。

2.生成SSH密钥。如果已经生成跳过。

1
2
//注意修改最后的E-mail地址
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

执行完成后,会让输入保存路径,直接按下回车,使用默认路径进行SSH密钥的保存就可以。

接着会提示你输入该SSH的密钥密码,可以为空,直接回车,想设置的同学也可以进行设置。

该SSH密钥密码用于第一次使用SSH时的校验,并可以在SSH密钥的配置文件中关闭SSH密钥密码校验。

我是设置的,更多细节大家可以自己去查阅一些资料。

3.复制SSH密钥。

1
pbcopy < ~/.ssh/id_rsa.pub

使用该命令后,你的粘贴板内容就会变成SSH密钥。

这次我们要将SSH密钥上传到我们自己的服务器里。

打开京东云的控制板,添加SSH密钥:

京东云添加SSH密钥

接着点击完成,Over。

4.测试SSH密钥。
使用SSH密钥登录也非常简单。
打开我们Mac的命令行输入:

1
ssh user@xxx.xxx.xxx.xxx

user为用户名,我们的用户名为root。@之后为IP地址,比如:

1
ssh root@116.196.93.148

接着会提示输入用户输入服务器的登录密码,正确后就可以登录成功。

如果失败,建议按照京东云帮助中心教程,走一遍。

简单网页搭建

我们先为我们的服务器下载一个服务器,这里使用郭神用的apache2。

apache2是专门用来显示静态网页的服务器程序。

在登录服务器成功后输入下面命令:

1
sudo apt-get install apache2

接着输入Y完成安装。

之后打开浏览器,输入我们服务器的IP,可以看到下面效果:

接着我们来替换这个html文件样式。

它在我们服务器的地址是:/var/www/html/index.html

我们只要自己写一个简单的静态Html文件,然后上传服务器覆盖掉它即可。

这里我们直接拿着郭神的简单html来做示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!Doctype html>
<html>
<head>
<title>京东云测试</title>
<style>
body{text-align:center}
</style>
</head>
<body>
<h1>欢迎来到郭霖的京东云主页</h1>
<p>
点击
<a href="http://guolin.tech">这里</a>
跳转到我的博客
</p>
</body>
</html>

将该文件保存为index.html。

接着我们将该文件上传至服务器,这里有一篇mac向服务器上传文件的教程。非常好用。
上传命令:

1
2
3
//注意将yourUsername修改为你的mac用户名
//并且我的文件保存在桌面Desktop。
put /Users/yourUsername/Desktop/index.html /var/www/html

按照上述步骤后,我们成功将index.html上传至服务器并覆盖。
刷新我们的网页,可以看到下面效果:
效果

。。。。。。。

为什么显示源码!?

因为Mac的记事本以.html结尾时,会将内容格式化成文本,不做代码显示。

解决也很简单,这篇文章。

解决后重新执行上传代码,重新刷新页面,效果如下:
效果

简单查阅后,在head中添加如下代码即可:

1
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

接着再次执行文件上传,再次刷新页面,效果如下:
效果

总结

至此,一个非常简单的静态网页的个人博客便搭建完成了!

写出这么个静态网页,带上这篇博客的出炉,一共耗时2天,走的弯路没有描述。

其中包括Linux命令行控制、SSH密钥理解等,都是新知识,于我而言还是有很大提升的。

以后如果用这个服务器,搭建一个动态的个人博客。

想想还有些小激动呢!

实现动态替换-策略模式

发表于 2017-08-02

模式介绍

通常,我们实现一个功能,可以有多种策略。

我们可以根据实际需求来选择最合适的策略。

针对这种情况,一种常规的方法就是将多种策略封装在一个类中,每个策略对应一个方法,在使用时通过if-else进行情况判断,不同情况调用不同方法来实现不同的策略。

这种实现方法我们可以称之为硬编码。

然而,当策略越来越多的时候,这个类就会变得臃肿,并且由于if-else等复杂逻辑的存在,在维护时会更容易产生错误,维护成本就会变高。

并且这种写法明显违反了面向对象中的开闭原则。

如果我们将这些策略的共同点抽象出来,提供一个统一的接口,不同的策略有着不同的实现,这样在客户端就可以通过注入不同的实现对象来实现动态替换。

这种模式的可扩展性、可维护性都是极高的。这,就是我们要说的策略模式。

模式定义

针对一个功能,将每一种解决方案封装起来,而且使它们可以相互替换。

使用场景

  • 针对同一类型问题的多种处理方式,仅仅是具体行为存在差异;
  • 需要安全地封装多种同一类型的操作时;
  • 同一抽象类有多个子类,同时又需要使用if-else或swithc-case来选择具体子类时。

模式角色

  • Strategy:策略抽象。
  • ConcreteStrategy:具体策略,针对一个问题,有多少种策略,就应有多少个具体策略。

模式示例

2014年12月28日北京提高了公交、地铁的价格,不再是单一票价,而是分距离计价。

我们就根据上述需求,来做一个出行价格计算器。

话不多说,我们来实现这个功能:
测试UI
简单做了计算器的UI,它应该提供具体出行方式的选择,很明显,我们提供了三种。

并且有一个出行距离的输入框。

在获取到出行方式和出行距离后,我们就可以来计算具体价格。

需求很明确,接下来我们要做的就是先实现各种出行方式下的价格计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class PriceCaculateController {

/**
* 根据出行方式,选择距离的计算方法
*/
public static float cacluatePrice(int km, int typeMode) {
switch (typeMode) {
case Constant.TYPE_BUS:
return caculateBusPrice(km);
case Constant.TYPE_SUBWAY:
return caculateSubwayPrice(km);
case Constant.TYPE_TAX:
return caculateTaxPrice(km);
}
return 0;
}

/**
* 计算出租车价格
* 小于3Km,定价9元,大于3km,每1km + 1元
*
*/
private static float caculateTaxPrice(int km) {
if (km <= 0) {
return 0;
}
if (km <= 3) {
return 9;
}
return 9 + (km - 3);
}

/**
* 计算公交车价格
* 小于5Km,定价2元,小于10km,定价3元,其余4元
*/
private static float caculateBusPrice(int km) {
if (km <= 0) {
return 0;
}
if (km <= 5) {
return 2;
}
if (km <= 10) {
return 3;
}
return 4;
}
/**
* 计算地铁价格
* 小于5Km,定价3元,小于10km,定价4元,小于15Km,定价5元,其余6元
*/
private static float caculateSubwayPrice(int km) {
if (km <= 0) {
return 0;
}
if (km <= 5) {
return 3;
}
if (km <= 10) {
return 4;
}
if (km <= 15) {
return 5;
}
return 6;
}
}

PriceCaculateController中包含了所有出行方式的计算细节。

细心的同学可以发现,我将PriceCaculateController中的所有计算方法私有化,向外暴露了一个cacluatePrice(int km, int typeMode)方法。

用户在使用时,仅需告诉我们出行距离与出行方式,我们就可以完全自动计算出价格,下面是使用的代码:

1
float price = PriceCaculateController.cacluatePrice(20,1);

这样我们就实现了页面与计算功能的完美解耦,页面完全不关心计算的细节。

接下来我们在页面上添加基础判断之后,调用PriceCaculateController.cacluatePrice即可。

我们可以来看看具体效果:
具体效果

至此,我们的价格计算器已经开发完成了。

很明显,我们的核心代码全在PriceCaculateController中。

随着出行方式的增加、价格计算的优化,PriceCaculateController必定会变得臃肿不堪。

为了将PriceCaculateController功能进行拆分,我们就要使用今天讲到的策略模式。

让我们来回顾策略模式中的角色:

一个策略抽象以及多个具体策略。

接下来我们就将价格计算功能进行抽象:

1
2
3
4
5
6
7
public interface StrategyCaculate {

/**
* 根据距离计算价格
*/
float caculatePrice(int km);
}

策略抽象非常简单。

接下里我们来实现具体策略,有多少种出行方式,就应该有多少种具体策略:
公共汽车(Bus):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BusStrategyCaculate implements StrategyCaculate {
@Override
public float caculatePrice(int km) {
if (km <= 0) {
return 0;
}
if (km <= 5) {
return 2;
}
if (km <= 10) {
return 3;
}
return 4;
}
}

出租车(Tax):

1
2
3
4
5
6
7
8
9
10
11
12
public class TaxStrategyCaculate implements StrategyCaculate {
@Override
public float caculatePrice(int km) {
if (km <= 0) {
return 0;
}
if (km <= 3) {
return 9;
}
return 9 + (km - 3);
}
}

地铁(Subway):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SubwayStrategyCaculate implements StrategyCaculate {
@Override
public float caculatePrice(int km) {
if (km <= 0) {
return 0;
}
if (km <= 5) {
return 3;
}
if (km <= 10) {
return 4;
}
if (km <= 15) {
return 5;
}
return 6;
}
}

三种出行方式对应三个具体策略。

当有新的出行方式时,我们只需创建新的类去实现策略抽象即可。

简单写一些测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
StrategyCaculate caculate = null;
switch (typeMode) {
case Constant.TYPE_BUS:
caculate = new BusStrategyCaculate();
break;
case Constant.TYPE_SUBWAY:
caculate = new SubwayStrategyCaculate();
break;
case Constant.TYPE_TAX:
caculate = new TaxStrategyCaculate();
break;
}
assert caculate != null;
return caculate.caculatePrice(km);

可以发现,这里我们又运用多态的特性,根据不同的出行方式,创建不同的子类,接着调用子类中的价格计算。

最后的效果和之前的效果是一致的。

相关代码已经提交至GitHub。

总结

有些人会有疑惑了,我们最开始的PriceCaculateController并不复杂,并且一个方法对应一种出行方式,清晰明了,为什么非要改成这个样子?

对于这个问题,我们现在的出行方式仅仅有三种、并且价格计算的逻辑非常简单。

如果现在继续添加出行方式:飞机、自驾、高铁、大巴等。

如果优化出租车的价格计算逻辑:出租车3Km以内9元,之后每1Km加1元,燃气费2元,堵车服务费每分钟0.1元,夜晚11点之后价格提升,每个城市的价格不同等等。

如果将上述逻辑全部写出,那么就出租车价格计算的逻辑,就要有上百行的代码。

如果你们公司是一家专业的出行价格计算公司,这些计算细节、出行方式都必定要涵盖到。

那么将来,PriceCaculateController的代码量,会有多大?

如果我们运用了策略模式,项目的目录结构就变成了:
策略模式目录结构
当某种交通工具的价格计算发生问题,我们仅仅去找对应的具体策略即可。并且避免产生了PriceCaculateController这种代码量庞大的类。

策略模式很好地遵循了开闭原则,注入不同的具体策略,会有不同的效果,从而达到很好的扩展性。

使用策略模式的优点有很多:

  • 结构清晰明了,使用简单;
  • 降低耦合度,扩展方便;
  • 封装彻底,数据更为安全。

所以,在你能预见到某个功能会有扩展的需求时,并且使用场景符合策略模式的使用场景,还是强烈建议你使用策略模式的。

感谢

《Android源码设计模式解析与实战》 何红辉、关爱民 著

创建型设计模式-抽象工厂模式

发表于 2017-08-02

模式介绍

前段时间介绍了工厂方法模式。
本次介绍的模式非常容易和工厂方法模式混淆,并且复杂度也有一定的提升,叫做抽象工厂模式。
其实在现实生活中,工厂都会生产某一样具体的产品,不存在说抽象的工厂。
那么该模式的含义到底是什么呢?
抽象工厂模式的出现最早是为了解决不同操作系统下的图形化处理,这就好比iOS中的Button、Android中的Button、WindowPhone中的Button一样,虽然都是Button,但系统的不同,会导致Button也存在差异。
这就引出了抽象工厂模式的定义:
为创建出一组相互依赖的对象提供一个封装接口。
定义中有几个关键字,需要注意:

  • 一组:这是和工厂方法模式的重要区别所在,抽象工厂模式用于生产多个产品,而不是一个。如果生产一个产品,请使用工厂方法模式或建造者模式。
  • 相互依赖:生产出一组产品之后,这组产品一定是相互依赖的,注意是一定。就好比上面的例子中,Android系统能使用iOS系统的Button吗?答案是否定的,因为iOS的Button依赖于iOS系统。

解释完名词,想必大家对抽象工厂模式的使用场景就有了一些认知:
当产品很多,并且有特定的关联可以进行抽象,就可以使用抽象工厂模式。

模式构成

抽象工厂模式的主要角色还是4个:

  • AbstractFactory:抽象工厂,包含一组产品的生产抽象,每一个方法都应对应一个产品。
  • ConcreteFactory:抽象工厂实现,应包含生产一组产品的具体实现。
  • AbstractProduct:一组产品中的一个产品抽象。
  • ConcreteProduct:具体的产品实现。

模式示例

现在我们开始实现在上述介绍中提到的例子。
我们先来将思路捋一捋:
对应关系

  1. 我们产品包含:操作系统、Button。
  2. 工厂应该生产一组产品,即生产操作系统和Button。
  3. 生产出来的一组产品应相互依赖,并且和另一组产品相互独立。

接下来,我们就按照思路来实现抽象工厂模式。
首先我们来初始化抽象产品AbstractProduct:
操作系统System:

1
2
3
4
5
6
public interface SystemAbsProduct {

//获取系统型号
String getSystem();

}

Button:

1
2
3
4
5
6
public interface ButtonAbsProduct {

//按钮响应
String getButtonName(SystemAbsProduct system);

}

我们可以发现,Button与操作系统存在依赖关系,这种依赖关系的体现由开发人员自己来控制,我这里只是个简单实例。
两个产品的抽象已经有了,我们先不急着去实现它们。
接下来我们来定义抽象工厂AbstractFactory,它应该有两个方法,分别返回系统和Button两个产品:

1
2
3
4
5
6
7
8
public interface AbsFactory {

//创建操作系统
SystemAbsProduct createSystem();

//创建Button
ButtonAbsProduct createButton();
}

到这里应该都没有什么问题,抽象工厂、抽象产品都已经创建完成了。
接下来我们想要分别构建出WindowPhone系统、Android系统、iOS系统下的Button。
我们应该能想到,必须要先要有具体的产品,才能用具体工厂来生产。
所以接下来,我们要先创建具体的产品:
操作系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface SystemAbsProduct {

//获取系统型号
String getSystem();

public class WindowPhone implements SystemAbsProduct{

@Override
public String getSystem() {
return App.context.getString(R.string.window_phone);
}
}
public class iOS implements SystemAbsProduct{

@Override
public String getSystem() {
return App.context.getString(R.string.ios);
}
}

public class Android implements SystemAbsProduct{

@Override
public String getSystem() {
return App.context.getString(R.string.android);
}
}
}

Button:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface ButtonAbsProduct {

//按钮响应
String getButtonName(SystemAbsProduct system);

public class WindowPhoneButton implements ButtonAbsProduct {

@Override
public String getButtonName(SystemAbsProduct system) {
if (system.getSystem().equals(App.context.getString(R.string.window_phone))) {
return "点击了WindowPhone系统的Button";
}
return App.context.getString(R.string.system_error);
}
}

public class IOSButton implements ButtonAbsProduct {

@Override
public String getButtonName(SystemAbsProduct system) {
if (system.getSystem().equals(App.context.getString(R.string.ios))) {
return "点击了iOS系统的Button";
}
return App.context.getString(R.string.system_error);
}
}

public class AndroidButton implements ButtonAbsProduct {

@Override
public String getButtonName(SystemAbsProduct system) {
if (system.getSystem().equals(App.context.getString(R.string.android))) {
return "点击了Android系统的Button";
}
return App.context.getString(R.string.system_error);
}
}
}

因为类的数量庞大,所以我选择使用内部类的形式来实现ConcreteProduct角色。
我们可以发现,在Button角色中,我们进行了简单的判断,当操作系统不匹配时,会提示用户,这就类似于运行崩溃的效果。
接下来,我们来创建最后一个角色:ConcreteFactory。
它应该有多个,每一个负责创建一组具体的产品,也就是说有多少组产品,就应该有多少个具体工厂:
WindowPhoneFactory:

1
2
3
4
5
6
7
8
9
10
11
public class WindowPhoneFactory implements AbsFactory {
@Override
public SystemAbsProduct createSystem() {
return new SystemAbsProduct.WindowPhone();
}

@Override
public ButtonAbsProduct createButton() {
return new ButtonAbsProduct.WindowPhoneButton();
}
}

IOSFactory:

1
2
3
4
5
6
7
8
9
10
11
public class IOSFactory implements AbsFactory {
@Override
public SystemAbsProduct createSystem() {
return new SystemAbsProduct.IOS();
}

@Override
public ButtonAbsProduct createButton() {
return new ButtonAbsProduct.IOSButton();
}
}

AndroidFactory:

1
2
3
4
5
6
7
8
9
10
11
public class AndroidFactory implements AbsFactory {
@Override
public SystemAbsProduct createSystem() {
return new SystemAbsProduct.Android();
}

@Override
public ButtonAbsProduct createButton() {
return new ButtonAbsProduct.AndroidButton();
}
}

三个具体工厂分别具体产生三组相互依赖的产品。
至此,简单的抽象工厂模式已经构建完成。
但是最关键的还是使用,接下来我们来写一些简单的测试代码:

1
2
3
4
5
6
7
8
//构建WindowPhone具体工厂
AbsFactory iOSFactory = new IOSFactory();
//构建操作系统
SystemAbsProduct iOSSystem = iOSFactory.createSystem();
//构建按钮
ButtonAbsProduct iOSButton = iOSFactory.createButton();
//调用依赖
Toast.makeText(this, iOSButton.getButtonName(iOSSystem), Toast.LENGTH_SHORT).show();

创建iOS系统工厂,接着创建iOS系统和Button,最后调用Button的点击,会发现成功提示出调用成功。
如果我们使用错误的依赖:

1
2
3
4
5
6
7
8
AbsFactory androidFactory1 = new AndroidFactory();
SystemAbsProduct androidSystem1 = androidFactory1.createSystem();
ButtonAbsProduct androidButton1 = androidFactory1.createButton();
AbsFactory iOSFactory1 = new IOSFactory();
SystemAbsProduct iOSSystem1 = iOSFactory1.createSystem();
ButtonAbsProduct iOSButton1 = iOSFactory1.createButton();
//使用Android的Button时,传入iOS操作系统
Toast.makeText(this, androidButton1.getButtonName(iOSSystem1), Toast.LENGTH_SHORT).show();

上述演示代码,会提示系统不匹配。

总结

抽象工厂模式的最大优势就是分离了接口与实现,客户端使用工厂来创建所需对象,实现面向产品的接口编程,使其在切换产品类时更加灵活、容易。
当然缺点也非常明显,一是类文件的增加,二是不太容易扩展产品类,因为每添加一个新产品,都要修改工厂,随之所有工厂实现都要修改。
抽象工厂模式与工厂方法模式的区别也很明显:
抽象工厂模式用来生产一组相互依赖的产品。
工厂方法模式用来批量生产一种产品。
抽象工厂模式的相关代码已经上传至GitHub,需要的同学可以下载参考。

感谢

抽象工厂模式和工厂模式的区别-caoglish的回答

《Android源码设计模式解析与实战》 何红辉、关爱民 著

Android获取图片的正确姿势

发表于 2017-07-27

前言

很多项目中都会有用户修改头像或者类似的功能。
该功能会访问用户的相册、相机来获取图片,然后显示到页面上。
实现该功能还是比较简单的,网上的资料也非常多,简单查阅之后复制粘贴便能实现,但是很多细节其实并不理解。
并且由于Android安全性的提升,包括Android6.0(API 23)的权限系统升级、Android7.0(API 24)的私有文件访问限制,很多地方稍不注意就会发生崩溃。
最近再次用到了这个功能,这次打算用一篇文章来详细记录这个功能点所对应的知识点,并解决掉之前的很多疑问。

打开相册

打开手机相册的方式有多种:
第一种:

1
2
3
4
5
Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
// 设置文件类型
intent.setType("image/*");
activity.startActivityForResult(intent, requestCode);

第二种:

1
2
3
4
5
Intent intent = new Intent();
intent.setAction(Intent.ACTION_GET_CONTENT);
// 设置文件类型
intent.setType("image/*");
activity.startActivityForResult(intent, requestCode);

第三种:

1
2
3
4
5
Intent intent = new Intent();
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
// 设置文件类型
intent.setType("image/*");
activity.startActivityForResult(intent, requestCode);

这几种方式都可以在获取到读取文件权限的前提下,完美实现图片选择。
第三种ACTION_OPEN_DOCUMENT是在Android5.0(API 19)之后新添加的意图,如果使用的话需要进行

1
2
3
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT){
//TODO
}

我们这里先不介绍ACTION_OPEN_DOCUMENT。
第二种ACTION_GET_CONTENT与第一种ACTION_PICK这两个意图类型的作用也非常类似,都是用来获取手机内容,包括联系人、相册等。
通过intent.setType("image/*")来指定MIME Type,让系统知道要打开的应用。
这里需要注意,必须指定MIME Type,否则项目会崩溃:

1
2
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.GET_CONTENT }
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.ACTION_PICK }

根据不同的MIME Type,可以跳转到不同的应用。
那么这两者有什么区别呢?
ACTION_GET_CONTENT与ACTION_PICK的官方解释在这里。
英语比较差,跟着百度翻译看了半天还是不懂。

英语好的同学可自行食用上面的链接,应该不需要翻墙。
两者的区别介绍都写在了ACTION_GET_CONTENT,大概是在说:
如果你有一些特定的集合(由URI标识)想让用户选择,使用ACTION_PICK。
如果让用户基于MIME Type选择数据,使用ACTION_GET_CONTENT。
在平局的情况下,建议使用ACTION_GET_CONTENT。
这个还是需要各位看官自己好好理解,我也没能完全了解两者的使用区别。
并且我发现两者返回的Uri格式是不同的:
关于Android中Uri的介绍,可以参考这篇文章。

两种意图分别唤起相册后,选择同一张图片的回调,也就是在onActivityResult中接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
switch (requestCode) {
case REQUEST_CODE_ALBUM://相册
Uri dataUri = data.getData();
Log.i("mengyuanuri","uri:"+dataUri.getScheme()+":"+dataUri.getSchemeSpecificPart());
break;
}
}

接下来我们来看看两个意图类型下选择同一张照片返回的数据:
ACTION_GET_CONTENT:

1
content://com.android.providers.media.documents/document/image:2116

ACTION_PICK:

1
content://media/external/images/media/2116

没有其他的东西,两者都是返回一个Uri。
为什么不直接返回给我们图片,而是一个Uri呢?
因为Intent传输有大小的限制。
所以我们需要根据Uri来获取到文件的具体路径。
但是我们发现,就算是同一张照片,两种意图下,返回的Uri也是不一致的。
这主要是因为Uri在Android中的类型也分为很多种,比如这两个意图的Uri种类就不一致。
这里就不做赘述了,我们可以通过网上大神封装的解析Uri的方法将它们统一转化成File路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public static String getPath( final Uri uri) {
// DocumentProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(App.context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];

if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {

final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

return getDataColumn(App.context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];

Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

final String selection = "_id=?";
final String[] selectionArgs = new String[]{
split[1]
};

return getDataColumn(App.context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(App.context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}

return null;
}

调用完成后,会发现不同的Uri对应的是同一个文件路径:

1
/storage/emulated/0/temp/kouliang_avatar.jpg

断点跟进该方法,会发现两个Uri走的是不同的if判断。

简单来说,三种方法都可以使用,并且三种方法都是在onActivityResult中返回Uri,而不是图片。
一般情况使用ACTION_GET_CONTENT的会多一些。

相机

打开相机的方式:

1
2
3
4
5
//指定相机意图
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//设置相片保存的地址
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
activity.startActivityForResult(intent, requestCode);

相机图片的获取方式不同于相册,相机图片获取需要先指定图片的保存路径,在拍摄成功后,我们只需直接去指定路径获取即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (requestCode) {
//相册
case REQUEST_CODE_ALBUM:
Uri dataUri = data.getData();
Log.i("mengyuanuri", "相册uri:" + dataUri.getScheme() + ":" + dataUri.getSchemeSpecificPart());
break;
//相机,注意,相机的回调中Intent为空,不要使用
case REQUEST_CODE_CAMER:
File bgPath = Constant.bgPath;
Bitmap bitmap = BitmapFactory.decodeFile(bgPath.getPath());
iv_bg.setImageBitmap(bitmap);
break;
}

非常简单,在相机回调中去指定路径中读取图片并显示。
但是我们应该可以想到,有些手机没有相机,也就是没有MediaStore.ACTION_IMAGE_CAPTURE意图对应的应用。
如果没有对其进行判断就会抛出ActivityNotFound的异常。
如何解决这个问题:

  1. try-catch,简单粗暴;
  2. 通过PackageManager去查询MediaStore.ACTION_IMAGE_CAPTURE意图是否存在。

两种做法都很简单,这里展示如何用PackageManager:

1
2
3
4
5
6
7
8
9
/**
* 判断某个意图是否存在
*/
public static boolean isHaveCame(String intentName) {
PackageManager packageManager = App.context.getPackageManager();
Intent intent = new Intent(intentName);
List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
return list.size() > 0;
}

接着我们运行,十分成功。
但是在7.1的虚拟机中,打开相机崩溃了:

1
2
3
4
5
6
7
8
9
10
11
12
13
android.os.FileUriExposedException: file:///storage/emulated/0/photo_bg.jpg exposed beyond app through ClipData.Item.getUri(
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:845)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8941)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8926)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
at android.app.Activity.startActivityForResult(Activity.java:4225)
at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:54)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:75)
at android.app.Activity.startActivityForResult(Activity.java:4183)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:708)
at com.my.photoget.utils.AppUtils.startCamer(AppUtils.java:37)

崩溃的主要原因是因为在7.0(API 24)中对文件读取进行了安全性的提升,这篇文章详细介绍了解决方案。
这里提一下,这和当初Android6.0(API 23)权限管理改版一致,如果build.gradle中的targetSdkVersion<23,则会沿用以前的权限管理机制,无需进行权限管理改版,权限管理详见这篇小文。
同理,这里如果你的targetSdkVersion<24的话,则无需进行上述崩溃的适配。
但是更新一定是往更好的方向去的,还是建议各位看官及时更新,及时适配,保证targetSdkVersion为最新SDK。

裁剪

裁剪功能是可选功能,如果想要对获取到的图片进行裁剪,我们可以继续使用裁剪Intent来对图片进行裁剪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Intent intent = new Intent("com.android.camera.action.CROP");
//设置要裁剪的图片Uri
intent.setDataAndType(cropBean.dataUri, "image/*");
//配置一系列裁剪参数
intent.putExtra("outputX", cropBean.outputX);
intent.putExtra("outputY", cropBean.outputY);
intent.putExtra("scale", cropBean.scale);
intent.putExtra("aspectX", cropBean.aspectX);
intent.putExtra("aspectY", cropBean.aspectY);
intent.putExtra("outputFormat", cropBean.outputFormat);
intent.putExtra("return-data", cropBean.isReturnData);
intent.putExtra("output", cropBean.saveUri);
//跳转
activity.startActivityForResult(intent, requestCode);

裁剪参数的含义可以参考这篇文章:

附加选项 数据类型 描述
crop String 发送裁剪信号
aspectX int X方向上的比例
aspectY int Y方向上的比例
outputX int 裁剪区的宽
outputY int 裁剪区的高
scale boolean 是否保留比例
return-data boolean 是否将数据保留在Bitmap中返回
data Parcelable 相应的Bitmap数据
circleCrop String 圆形裁剪区域
output URI 将URI指向相应的file://
outputFormat String 图片输出格式
noFaceDetection boolean 是否取消人脸识别

每个属性的解释都很清晰,这里我将裁剪参数封装为了一个Bean对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class CropBean {

//要裁剪的图片Uri
public Uri dataUri;

//裁剪宽度
public int outputX;
//裁剪高度
public int outputY;

//X方向上的比例
public int aspectX;
//Y方向上的比例
public int aspectY;

//是否保留比例
public boolean scale;

//是否将数据保存在Bitmap中返回
public boolean isReturnData;
//相应的Bitmap数据
public Parcelable returnData;

//如果不需要将图片在Bitmap中返回,需要传递保存图片的Uri
public Uri saveUri;

//圆形裁剪区域
public String circleCrop;

//图片输出格式,默认JPEG
public String outputFormat = Bitmap.CompressFormat.JPEG.toString();

//是否取消人脸识别
public boolean noFaceDetection;

/**
* 根据宽高计算裁剪比例
*/
public void caculateAspect() {

scale = true;

if (outputX == outputY) {
aspectX = 1;
aspectY = 1;
return;
}
float proportion = (float) outputX / (float) outputY;

aspectX = (int) (proportion * 100);
aspectY = 100;
}
}

关于封装对象中caculateAspect()方法,因为aspectX与aspectY是用来设定裁剪框宽高比例的,所以我选择在指定完outputX与outputY(也就是裁剪图片的宽度和高度)之后,直接根据宽高来计算裁剪框的大小。
caculateAspect()中就是具体的计算过程。
还有几个比较重要的参数需要提一下:

  • intent.setData(Uri uri)是必须指定的,它代表着要裁剪的图片的Uri。
  • return-data参数代表是否要返回数据,如果为true,则返回Bitmap对象,如果为false,则会将图片直接保存到另一个参数output中。也就是说,当return-data为true时,output是没有用的,直接在onActivityResult中取data当中的Bitmap即可。如果为false,则直接在onActivityResult中去之前指定到output中的地址取出图片即可。
  • 综上一点,强烈建议设置return-data为false并且设置output,因为Intent传输是有大小限制的。为防止超出大小的现象发生,通过Uri传输最为安全。

总结

到此为止,获取图片显示的功能已经完成了。
整个项目已经上传至GitHub,简单总结一下:

  1. 通过相册获取图片的方式有很多,但是在onActivityResult中都是以Uri的方式传递的。
  2. 裁剪功能不是必要的,如果没有裁剪需求可忽略。强烈建议不要将return-data设置为true,可能会超出Intent传输大小限制。
  3. 当你的targetSdkVersion>=23时,需要进行权限管理的升级,当你的targetSdkVersion>=24时,需要进行FileProvider的适配。强烈建议进行适配,提升应用的安全性。

感谢

使用系统裁剪
Intent传输大小实战
相机7.0图片选择适配

Android面试技能点

发表于 2017-07-25

技能点

主要技能

  1. 面向对象思想
  2. 设计模式
  3. 算法基础
  4. Android适配
  5. Android架构
  6. Android动画
  7. Android事件机制
  8. Android消息通讯机制
  9. Android数据通传输
  10. Android线程管理
  11. Android内存管理
  12. Android网络管理
  13. Android底层机制以及Linux内核机制
  14. Android JNI编程
  15. Android安全机制
  16. Android单元测试、压力测试
  17. 通过Android源码定位问题
  18. Android流行开源组件、框架
  19. Android权限管理

加分技能

  1. 计算机专业+高学历
  2. 独立开发能力
  3. 知名App的开发经验
  4. Html+JavaScript
  5. C++
  6. 蓝牙开发
  7. 技术难点的攻关能力

应用最广-工厂方法模式

发表于 2017-07-24

模式介绍

工厂方法模式是应用最广泛的模式之一,也是创建型模式之一。
工厂方法模式指的是定义出一个用于创建对象的接口,让子类决定实例化哪个类。听起来可能不太懂,没关系,往下看慢慢就明白了。
工厂方法模式是一种结构简单的模式,在我们平时开发中应用非常广泛,也许你并不知道,但你已经使用了无数次该模式。

使用场景

在任何需要生成复杂对象的地方,都可以用使用工厂方法模式。简单对象无需使用工厂方法模式,更无需使用建造者模式,因为这样反而会损失性能。

模式构成

工厂方法模式的成员构成如下:

  • Product:商品功能的抽象。
  • ConcreteProduct:商品功能的具体实现。
  • Factory:生产商品的工厂抽象。
  • ConcreteFactory:生产商品的工厂具体实现。

需要注意的是,工厂方法模式中的Product和建造者模式中的Product是有区别的:
前者的Product指的是商品的功能,后者指的是商品的属性。
也就是两者Product中封装的内容是不一致的,是两个完全不同的模式结构。

模式示例

如果将汽车的功能抽象出来,其实都是一致的,无非是有些高级功能,有的汽车有,有的汽车没有。
但是最基本的功能,比如驾驶、大灯、雨刷等功能,是所有汽车都应该具备的。
不过就算是这些都具备的功能,也会因车的品牌、型号的不同,存在差异。
这些就是后话了,我们现在就开始用工厂方法模式的代码来实现上述示例。
先来创建汽车Product,它应该包含汽车所有的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface CarProduct {
/**
* 汽车-通用功能封装
* 开始驾驶
*/
void drive();
/**
* 汽车-通用功能封装
* 打开车的大灯
*/
void openHeadlamps();

//....省略其他功能

}

可以看出,我们的汽车商品包含驾驶以及大灯两个功能。
两个最简单的功能,在不同品牌、不同型号的车下都会存在差异:
奥迪A6L:

1
2
3
4
5
6
7
8
9
10
11
public class AudiA6L implements CarProduct{
@Override
public void drive() {
Log.i("factory","奥迪A6L,平稳起步。");
}

@Override
public void openHeadlamps() {
Log.i("factory","奥迪A6L,尊享华丽大灯打开!");
}
}

奔驰E260L:

1
2
3
4
5
6
7
8
9
10
11
public class BenzE260L implements CarProduct {
@Override
public void drive() {
Log.i("factory","奔驰E260L,弹射起步!");
}

@Override
public void openHeadlamps() {
Log.i("factory","奔驰E260L,标配大灯打开。");
}
}

我们创建了两款汽车,分别去实现了CarProduct,并且实现了各自功能的细节。
栗子而已,各位看官可不要因为奔驰E260L能不能弹射起步和我撕啊。

如果还有其他类型的汽车,我们只需要接着创建类去实现CarProduct即可。
接下来我们就要创建汽车的生产工厂了:

1
2
3
4
5
6
7
8
9
public abstract class CarFactory {

/**
* 创建CarProduct
*
* @return CarProduct
*/
public abstract CarProduct createProduct();
}

可以看出我们的抽象工厂中,返回了抽象的商品对象。
那么我们应该在工厂实现中,实现创建商品的具体细节:
奥迪A6L的工厂:

1
2
3
4
5
6
public class AudiA6LFactory extends CarFactory {
@Override
public CarProduct createProduct() {
return new AudiA6L();
}
}

奔驰E260L的工厂:

1
2
3
4
5
6
public class BenzE260LFactory extends CarFactory {
@Override
public CarProduct createProduct() {
return new BenzE260L();
}
}

因为示例比较简单,仅仅是new的操作,实际创建汽车会更复杂一些。
接下来我们写一些测试代码:

1
2
3
4
5
6
7
8
9
10
//创建奔驰工厂
CarFactory carFactory1 = new BenzE260LFactory();
BenzE260L product1 = (BenzE260L) carFactory1.createProduct();
product1.drive();
product1.openHeadlamps();
//创建奥迪工厂
CarFactory carFactory2 = new AudiA6LFactory();
AudiA6L product2 = (AudiA6L) carFactory2.createProduct();
product2.drive();
product2.openHeadlamps();

这里注意一点,创建工厂的代码是利用面向对象思想中多态思想(继承、重写、父类引用指向子类对象)来书写的。
接下来打印的Log如下:

1
2
3
4
factory: 奔驰E260L,弹射起步!
factory: 奔驰E260L,标配大灯打开。
factory: 奥迪A6L,平稳起步。
factory: 奥迪A6L,尊享华丽大灯打开!

到这里,我们最基本的工厂方法模式已经实现了。
但是到这里还有一个缺陷:商品实现与工厂实现一对一。
也就是说每多出一个汽车类型、就要为其创建一个具体工厂实现。
为了解决这一问题,我们需要对工厂抽象、工厂细节进行优化,利用泛型和反射来实现商品实现与工厂实现多对一:

1
2
3
4
5
6
7
8
9
10
public abstract class NewCarFactory {
/**
* 利用泛型来决定要生成的具体商品
*
* @param clz 具体商品的类名class
* @param <T> 具体商品的类名
* @return 具体商品类
*/
public abstract <T extends CarProduct> T createProduct(Class<T> clz);
}

上面代码就是经过优化的抽象工厂,创建方法增加了一个参数,决定了要创建的具体商品。
接下来是工厂的具体细节,应该具备创建任意商品的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CarConcreteFactory extends NewCarFactory {

@Override
public <T extends CarProduct> T createProduct(Class<T> clz) {
CarProduct product = null;
try {
product = (CarProduct) Class.forName(clz.getName()).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return (T) product;
}
}

我们用参数得到类名,然后通过反射来创建对应的类。
这样子我们只要有这么一个工厂细节,就能应对所有的商品创建。
写一下测试代码:

1
2
3
4
5
6
7
8
9
10
11
//创建通用汽车工厂
NewCarFactory carFactory = new CarConcreteFactory();
//创建奔驰E260L
BenzE260L benzE260L = carFactory.createProduct(BenzE260L.class);
//创建奥迪A6L
AudiA6L audiA6L = carFactory.createProduct(AudiA6L.class);
//测试
benzE260L.drive();
audiA6L.drive();
benzE260L.openHeadlamps();
audiA6L.openHeadlamps();

只有一个工厂,生产出了不同类型的汽车。

1
2
3
4
factory: 奔驰E260L,弹射起步!
factory: 奥迪A6L,平稳起步。
factory: 奔驰E260L,标配大灯打开。
factory: 奥迪A6L,尊享华丽大灯打开!

结果是一致的。
两种实现方式都可以,利用反射的方式更简洁而已。
而且上面一对一的写法,还有一个名字:多工厂方法模式。

总结

工厂方法模式对于大家来说是非常好理解的一个模式,即便是第一次听说,只要读者懂点Java知识,理解这个模式绝对不难。
工厂方法模式也存在缺陷:每次我们添加新的商品时,都要创建新的类。
如果你辞职了,让不懂工厂方法模式的人接手了,这简直是折磨,因为虽然解耦了,但是代码复杂度也随之提升了。
是否使用工厂方法模式,需要设计者,你,来决定。

感谢

《Android源码设计模式解析与实战》 何红辉、关爱民 著

123
孟远

孟远

一名Android开发小将

22 日志
8 标签
GitHub E-Mail 知乎 简书
© 2020 孟远
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4