实践中的设计模式--让代码更简洁的状态模式
1、定义
状态模式(Allow an object to alter its behavior when its internal state changes.The object will appear to
change its class.)翻译过来就是:允许一个对象在其内部状态改变时改变其行为,这个对象看起来好像是改变了其类。状态模式是一种对象行为型模式。
2、组成角色
状态模式的通用类图如下:
状态模式包含角色如下:
- 上下文角色(Context):上下文角色一般是一个类,上下文角色会聚合很多和 state,这些 state 使用静态常量修饰,并且负责 state 的状态切换;另外上下文角色还会包含抽象状态角色中定义的所有行为如 request,然后内部将请求委托给 state 的 handle 处理;
- 抽象状态角色(State):抽象状态角色一般是一个抽象类,用来定义具体状态的公共行为比如 handle,任何具体状态都必须实现该抽象类中的抽象方法;
- 具体状态角色(ConcreteState):继承抽象状态角色,实现抽象方法,实际处理来自 Context 的委托请求,当 Context 改变状态时行为也跟着改变。
3、状态模式代码实现
状态模式的代码实现如下:
// 抽象状态角色
abstract class State {
// 上下文角色,负责状态切换
protected Ctx context;
public void setContext(Ctx context) {
this.context = context;
}
// 状态的公共行为,需要子类自行实现其状态对应的行为
abstract void handle1();
abstract void handle2();
}
// 具体状态1
class ConcreteState1 extends State {
@Override
void handle1() {
// 本状态ConcreteState1时的业务逻辑
}
@Override
void handle2() {
// 设置当前状态为state2
super.context.setCurrentState(Ctx.state2);
super.context.request2();
}
}
// 具体状态2
class ConcreteState2 extends State {
@Override
void handle1() {
super.context.setCurrentState(Ctx.state1);
super.context.request1();
}
@Override
void handle2() {
// 本状态ConcreteState2时的业务逻辑
}
}
class Ctx {
// 上下文角色一般会包含全部状态,使用静态常量修饰
public final static State state1 = new ConcreteState1();
public final static State state2 = new ConcreteState2();
// 定义上下文保存的当前状态
private State currentState;
public State getCurrentState() {
return currentState;
}
// 设置当前状态
public void setCurrentState(State currentState) {
this.currentState = currentState;
// 初始化state中的上下文
this.currentState.setContext(this);
}
// 上下文一般包含抽象状态中的所有行为,然后委托给state
public void request1() {
this.currentState.handle1();
}
public void request2() {
this.currentState.handle2();
}
}
测试方法一般如下:
Ctx context = new Ctx(); // 新建一个上下文
context.setCurrentState(new ConcreteState1()); // 设置当前状态
context.request1(); // 调用request动作
context.request2();
4、优缺点
状态模式的优缺点总结如下:
- 减少代码体积,利于拓展:状态模式可以消除繁杂的条件判断语句块,使得业务逻辑清晰,很好地应对对象状态的增加、删除的业务场景,因为添加新的状态只需要增加新的状态类就好了;
- 状态模式状态很多时会导致状态类比较多,子类太多的时候就不方便维护管理了。
5、应用场景
状态模式的应用场景如下:
- 行为随状态改变而改变的场景;
- 化繁为简,如果代码中包含大量的条件语句块比如 switch…case、if 等,这些语句块的出现会导致业务逻辑变更时代码块也会变更,对状态的增加、删除时的调整修改起来比较吃力时就可以考虑状态模式;
6、使用实例
6.1 不引入状态模式时
状态模式的重点在于状态切换,往往一个对象的内部状态发生变化时,该对象的具体行为也会发生改变,开起来就像对象的状态在控制着行为的变化一样。比如我们家里熟悉的电视机,其状态可以分为待机、关机以及正常播放三种状态,各个状态下对应的行为用下表描述如下:
开机 |
关机 |
播放 |
待机 |
|
---|---|---|---|---|
待机状态 |
√ |
√ |
||
关机状态 |
√ |
√ |
||
播放状态 |
√ |
√ |
如上表格,我们将电视机的状态以及各个状态可以进行的行为罗列了下(其中√代表可以进行的行为,空白代表不可以进行或者进行了无效果的行为),假设我们有一个电视机对象,当然首先是定义一个电视机的接口:
// 定义一个电视机接口
interface ITelevision {
// 开机
void powerOn();
// 关机
void powerOff();
// 播放
void play();
// 待机
void standby();
}
接下来就是我们的电视机的实现类:
// 电视机的实现类
class Telev implements ITelevision {
@Override
public void powerOn() {
System.out.println("开机...");
}
@Override
public void powerOff() {
System.out.println("关机...");
}
@Override
public void play() {
System.out.println("播放...");
}
@Override
public void standby() {
System.out.println("待机...");
}
}
没错,这里我们只是简单的实现了下接口中的各个行为,我们在 main 方法中模拟电视机进行下测试:
public static void main(String[] args) {
ITelevision tv = new Telev();
tv.powerOn();
tv.play();
tv.standby();
tv.powerOff();
}
运行效果相比大家都知道了吧,上述测试输出如下:
开机...
播放...
待机...
关机...
但是我们一开始也说了,电视机的各个行为是在其状态约束下才有的,比如待机行为,你总不能在电视机正处于关机状态直接按遥控器待机吧。这里我们将上面的代码进行下改造,在电视机中加入其状态,以便我们的行为可以根据状态进行一些操作,首先定义一个表示电视机对象状态的枚举 TVStateEnum:
// 定义一个电视机状态的枚举
enum TVStateEnum {
// 分别定义待机、关机、播放三种状态
STANDBY_STATE(1), POWER_OFF_STATE(2), PLAY_STATE(3);
private final int state;
private TVStateEnum(int state) {
this.state = state;
}
}
接下来我们在电视机对象中引入电视机状态的概念,同时修改我们的行为逻辑,在内部加入状态判断,修改下电视机类如下:
// 电视机的实现类
class Telev implements ITelevision {
// 这里加入电视机的状态字段,构造中传入
private TVStateEnum state;
public Telev(TVStateEnum state) {
this.state = state;
}
public TVStateEnum getState() {
return state;
}
public void setState(TVStateEnum state) {
this.state = state;
}
// 开机
@Override
public void powerOn() {
switch (this.state) {
// 待机状态
case STANDBY_STATE:
// 待机状态进行开机,没有任何效果,所以这里什么也不做,以下同理
break;
// 关机状态
case POWER_OFF_STATE:
// 关机状态进行开机,是允许的,开机之后默认属于standby待机状态
System.out.println("开机...");
this.setState(TVStateEnum.STANDBY_STATE);
break;
// 播放状态
case PLAY_STATE:
// 播放状态进行开机,没有任何效果,所以这里什么也不做,以下同理
break;
default:
break;
}
}
// 关机
@Override
public void powerOff() {
switch (this.state) {
// 待机状态 & 播放状态 都可以进行关机操作
case STANDBY_STATE:
case PLAY_STATE:
System.out.println("关机...");
this.setState(TVStateEnum.POWER_OFF_STATE);
break;
// 关机状态
case POWER_OFF_STATE:
break;
default:
break;
}
}
// 播放
@Override
public void play() {
switch (this.state) {
// 待机状态
case STANDBY_STATE:
System.out.println("播放...");
this.setState(TVStateEnum.PLAY_STATE);
break;
// 关机状态
case POWER_OFF_STATE:
break;
// 播放状态
case PLAY_STATE:
break;
default:
break;
}
}
// 待机
@Override
public void standby() {
switch (this.state) {
// 待机状态
case STANDBY_STATE:
break;
// 关机状态
case POWER_OFF_STATE:
System.out.println("关机...");
this.setState(TVStateEnum.POWER_OFF_STATE);
break;
// 播放状态
case PLAY_STATE:
System.out.println("待机...");
this.setState(TVStateEnum.STANDBY_STATE);
break;
default:
break;
}
}
}
上面的电视机的行为中,我们加入了电视机状态的判断,使用了冗长的 switch…case 语句,目的就是控制电视机对象的行为不至于脱离其状态而随意执行,当然测试代码也要同步修改下:
public static void main(String[] args) {
ITelevision tv = new Telev(TVStateEnum.POWER_OFF_STATE);
tv.play(); // 如果直接进行播放的话,因为电视机处于待机状态,所以没有任何输出
// 必须先开机,才能播放
tv.powerOn();
tv.play();
tv.standby();
tv.powerOff();
}
这里需要注意的是,加入了状态判断之后,如果直接调用 play 的话是不被允许的,因为电视机的默认状态是关机,上述测试输出如下:
开机...
播放...
待机...
关机...
6.2 引入状态模式
在上面的电视机例子中,我们发现,行为操作前需要进行各种状态判断,而这些判断使用了比较冗余的 switch…case 语句来实现的,假设我们后面电视机的状态不止如上三种了,比如加入了死机状态,那么我们的行为都要对该状态做出处理(尽管死机状态下我们的操作行为是无效的,case 语句中不必做任何响应,但这不代表我们可以省略 case 判断逻辑)。
状态模式的出现就刚好可以解决冗余的 switch…case 逻辑,就好比之前我们讲工厂方法模式的出现解决了简单工厂模式中的冗余的 if 判断一样,避免了巨大的条件语句块的出现,了解过规则引擎的朋友应该都听说过 Drools,规则引擎的出现也是为了解决冗余多变的业务逻辑判断问题,从这一角度来讲,状态模式也是如此。下面一起看下引入了状态模式之后我们的类图设计:
6.2.1 电视机的抽象状态
// 抽象的电视机状态角色
abstract class TVState {
// 使用遥控器作为上下文,控制电视机状态的切换
protected RemoteControlMachine remoteControlMachine;
public void setRemoteControlMachine(RemoteControlMachine remoteControlMachine) {
this.remoteControlMachine = remoteControlMachine;
}
// 开机
abstract void powerOn();
// 关机
abstract void powerOff();
// 播放
abstract void play();
// 待机
abstract void standby();
}
6.2.2 待机状态
// 待机状态
class StandByState extends TVState {
@Override
void powerOn() {
// do nothing
}
@Override
void powerOff() {
System.out.println("关机...");
// 使用遥控器设置电视机状态为 关机
super.remoteControlMachine.setCurrentState(RemoteControlMachine.POWER_OFF_STATE);
// 执行关机的行为
super.remoteControlMachine.powerOff();
}
@Override
void play() {
System.out.println("播放...");
super.remoteControlMachine.setCurrentState(RemoteControlMachine.PLAY_STATE);
// 执行播放的行为
super.remoteControlMachine.play();
}
@Override
void standby() {
// do nothing
}
}
6.2.3 关机状态
// 关机状态
class PowerOffState extends TVState {
@Override
void powerOn() {
System.out.println("开机...");
// 开机后状态默认为 待机
super.remoteControlMachine.setCurrentState(RemoteControlMachine.STANDBY_STATE);
// 执行待机的行为
super.remoteControlMachine.standby();
}
@Override
void powerOff() {
// do nothing
}
@Override
void play() {
// do nothing
}
@Override
void standby() {
// do nothing
}
}
6.2.4 播放状态
// 播放状态
class PlayState extends TVState {
@Override
void powerOn() {
// do nothing
}
@Override
void powerOff() {
System.out.println("关机...");
// 使用遥控器设置电视机状态为 关机
super.remoteControlMachine.setCurrentState(RemoteControlMachine.POWER_OFF_STATE);
// 执行关机的行为
super.remoteControlMachine.powerOff();
}
@Override
void play() {
// do nothing
}
@Override
void standby() {
System.out.println("待机...");
// 使用遥控器设置电视机状态为 待机
super.remoteControlMachine.setCurrentState(RemoteControlMachine.STANDBY_STATE);
// 执行待机的行为
super.remoteControlMachine.standby();
}
}
6.2.5 遥控器角色(上下文角色)
// 遥控器,扮演上下文角色,负责电视机状态切换
class RemoteControlMachine {
// 包含电视机的三种状态:待机、关机、播放
public final static TVState STANDBY_STATE = new StandByState();
public final static TVState POWER_OFF_STATE = new PowerOffState();
public final static TVState PLAY_STATE = new PlayState();
// 标识当前状态
private TVState currentState;
// 获取当前状态
public TVState getCurrentState() {
return currentState;
}
// 设置当前状态,遥控器负责电视机的具体状态切换
public void setCurrentState(TVState currentState) {
this.currentState = currentState;
this.currentState.setRemoteControlMachine(this);
}
// 委托给state统一去处理
public void powerOn() {
// 当前状态下如何powerOn,由state去确定
this.currentState.powerOn();
}
public void powerOff() {
this.currentState.powerOff();
}
public void play() {
this.currentState.play();
}
public void standby() {
this.currentState.standby();
}
}
6.2.6 测试
RemoteControlMachine context = new RemoteControlMachine();
context.setCurrentState(new PowerOffState());
context.play(); // 如果直接进行播放的话,因为电视机处于待机状态,所以没有任何输出
context.powerOn();
context.play();
context.standby();
context.powerOff();
上面测试输出如下:
开机...
播放...
待机...
关机...
可以看到,测试结果没有任何不同,但是我们没有写一行 switch…case 语句块,反而是将对象的各个状态抽出来做成状态类,然后各个状态类在对各个行为做出实现,代码更加精简。
状态模式具体的状态类在对状态做出变更时其行为也跟着做出变更,其实代码量减少并不十分明显,但是对于状态拓展十分友好,只需要增加状态类再实现各个行为即可拓展新的状态出来,也体现了开闭原则及单一职责原则;状态模式将对象状态的变更放到类的内部进行,外部调用者无需关心对象的状态及行为的变化,也体现了更好的封装性;另外对代码的 cpd(代码重复率检测)也是很有提升明显。
7、总结
本小节我们介绍了状态模式的定义,优缺点已经使用场景,然后用电视机的例子帮大家更好地理解模式,状态模式的出现,一定程度解决了繁杂的语句块的硬编码的形式,成为条件分支、判断的终结者,另外状态模式下代码结构更加清晰,面向拓展更加友好。
推荐阅读
-
实践中的设计模式--让代码更简洁的状态模式
-
像首席技术官一样思考:如何高效管理 30 人的研发团队?-管理越多越轻松。好的研发团队,应该是上拨下用,即下级对上级的向上管理;而不是反过来,总是向下管理,甚至是 CTO 做经理的事,经理做工程师的事,工程师最终会被当成实习生。如果是这样,就会越管越累,不仅团队无法成长,而且团队整天很忙还效率低下,问题一大堆。 有这样一个小故事:一位高级经理下班后帮忙倒垃圾,结果被老板训斥了一顿。这就好比首席技术官做了实习生自己该做的事。事情本身没有对错之分,只是从不同的角度有不同的理解。 古人云:"用人不疑,疑人不用"。在面对自己的研发团队时,应该相信他们能做好,授权一线开发人员充分发挥专业特长,不要限制他们的工作。但在相信他们的同时,也要进行二次确认,始终秉持 "我相信,但我要确认 "的原则和严谨的精神。因为每个人都会犯错和疏忽,通过发挥团队的智慧,团队犯错的机会就会大大减少。比如回归测试、代码审查、开发演示、变更审批等等。 如前所述,每个人都难免会犯错。但作为管理者,你所设计和商定的流程不能出错。管理者的每一个决定和沟通都应该经过深思熟虑。就像红绿灯的交通设计,某辆车不小心闯红灯可能会扣分,但红绿灯的设计一定要正确、人性化、统一。再比如,开发人员可能会因为疏忽大意写出 bug,但研发流程的设计和上线流程的发布不能有任何差错。因此,流程体系的设计,一方面要结合当前团队规模、业务特点和需要重点解决的问题来设计,另一方面也要在人员防错、效率提升、发挥团队集体智慧等维度进行综合考量。应该站在更高更抽象的角度去思考,不断思考一个倍受欢迎的园区应该如何设计,思考一个灵动、经典、永恒的建筑应该遵循怎样的模式,思考一个成功、优秀、卓越的研发团队应该需要怎样的流程和制度。 最后,反馈很重要。向上汇报很重要,向下反馈也很重要。能够保持顺畅的双向反馈和闭环管理,对研发团队的协作和沟通有着非常明显的积极作用。在向上汇报方面,要培养团队在正式汇报、会议汇报、私下沟通、书面总结、非正式场合等方面的沟通能力,提醒下属报喜也要报忧。凡事先记录,再跟进,最后反馈。反馈很重要,主动汇报更难得。 另一方面,同时也不要忽视向下反馈。好的爱,是双向的。团队也是如此,没有严格的上下级之分,只是分工和角色不同而已。作为管理者,不必总保持一种 "神秘感",让人 "捉摸不透 "才是牛。当团队做得好或有人做得好时,要记得在公开或私下场合给予肯定和赞许。业务有增长、业绩有提升时,别忘了给团队一些鼓励,或者安排一次下午茶或聚餐。在例会或正式会议上,也可以同步向大家传达一些重要信息和高层指示。"欲速则不达,欲远则同行"。 当向上汇报、向下反馈的沟通闭环形成后,同时结合前面研发过程的管理闭环,双管齐下,就能形成良性循环。如此反复,持之以恒,优秀卓越的研发团队,必将呈现。 能力、产出和效率 接下来,继续重复关于能力、产出和效率的话题。 站在不同的角色,以及一个企业经营、生存和发展所需要的基础上,我把研发生产力分为三个层次,分别是:一线员工关心的研发能力、管理层关心的软件产出和操作人员关心的企业生产效率。简单概括就是:既要把工作做好,又要能出成果,还要能帮企业赚钱。
-
小红书大产品部架构 小红书产品概览--经过性能、稳定性、成本等多个维度的详细评估,小红书最终决定选择基于腾讯云星海自研硬件的SA2云服务器作为主力机型使用。结合其秒级的快速扩缩、超强兼容和平滑迁移能力,小红书在抵御上亿次用户访问、保证系统稳定运行的同时,也实现了成本的大幅降低。 星海SA2云服务器是基于腾讯云星海的首款自研服务器。腾讯云星海作为自研硬件品牌,通过创新的高兼容性架构、简洁可靠的自主设计,结合腾讯自身业务以及百万客户上云需求的特点,致力于为云计算时代提供安全、稳定、性能领先的基础架构产品和服务。如今,星海SA2云服务器也正在为越来越多的企业提供低成本、高效率、更安全的弹性计算服务。 以下是与小红书SRE总监陈敖翔的对话实录。 问:请您介绍一下小红书及其主要商业模式? 小红书是一个面向年轻人的生活方式平台,在这里,他们发现了向上、多元的真实世界。小红书日活超过 3500 万,月活跃用户超过 1 亿,日均笔记曝光量达 80 亿。小红书由社交平台和在线购物两大部分组成。与其他线上平台相比,小红书的内容基于真实的口碑分享,播种不止于线上,还为线下实体店赋能。 问:围绕业务发展,小红书的系统架构经历了怎样的变革和演进? 系统架构变化不大,影响最深的是资源开销。过去三年,资源开销大幅增加,同比增长约 10 倍。在此背景下,我们努力进行优化,包括很早就开始使用 K8S 进行资源调度。到 18 年年中,绝大多数服务已经完全实现了容器化。 问:目前小红书系统架构中的计算基础设施建设和布局是怎样的? 我们目前的建设方式可以简单描述为星型结构。腾讯云在上海的一个区是我们的计算中心,承载着我们的核心数据和在线业务。在外围,我们还有两个数据中心进行计算分流,同时承担灾备和线上业务双活的角色。 与其他新兴电子商务互联网公司类似,小红书的大部分计算能力主要用于线下数据分析、模型训练和在线推荐等平台。随着业务的发展,对算力的需求也在加速增长。
-
趣谈留言队列,搞清楚留言队列到底是什么!-说到消息队列,洪觉大概能猜到人们听到消息队列的反应,大致可以分为以下几类人。 第一类人,懵懵懂懂,刚上大学接触编程,还没用过消息队列,甚至还以为消息队列就是代码里面要新建一个List之类的;第二类人,听过消息队列,了解消息队列,但具体是什么还不是太明白,只知道一说到消息队列,脑海里马上出现了三组词,削峰、异步、解耦;第三类人,用过消息队列,对它有一定了解,但不知道为什么要这样设计,消息队列有什么样的前世今生,是如何演化到现在的模式的?**第四类人,已经对消息队列有了足够的了解,可以阅读本帖作为复习和温习。**你属于哪一类?无论你对消息队列了解多少,读完这篇文章后,我相信你都会有所收获。 什么是消息队列?我们为什么要使用消息队列?真的只是因为它看起来很勉强、很常用吗?当然不是,一项技术的出现往往是为了解决某种痛点,我们就从这个痛点出发,看看消息队列到底是为了解决什么问题而诞生的。 相信大家在工作之前,或者工作中接触单片机的次数会多一点,不管什么业务都一股脑塞进一个系统里,这种情况下接触消息队列的场景会比较少。但随着业务的增长,量上去了,单机系统就很难维护了,也扛不住并发量的增长,就需要把原来的单体应用拆分成多个服务。例如,牛奇网采用分布式架构,将原来的单体系统拆分成用户服务、题库服务、求职服务、论坛服务等,每个分布式节点都有一个集群,保证高可用性。 那虽然在这样的微服务架构下,如果某个核心业务并发量过大,系统就扛不住了。比如淘宝、淘票票、拼多多、京东等电商场景中的支付场景,你在某宝下单并支付后,调用支付服务,完成支付后,还需要更新订单的状态,这个时候就需要调用订单服务,那我们平时也下单,除了简单完成这些操作外,还会给你相应的积分;商家也会收到订单消息,并给您发送旺旺消息,确认订单无误;同时,也会给您发送消息,确认订单无误。确认订单无误;同时您还可以查看您的物流状态;还有系统为了给您推荐更适合您的商品,会根据您的订单做类似的推荐等等,我说的这些都是当我们下单后,肉眼可以感知到系统所做的动作。 **一个支付动作如果还需要调用那么多服务,等他们响应成功,最后再告诉用户你支付成功了,用户在系统中的整个体验会非常糟糕。**设想一下,假设请求服务+处理请求+响应总共需要 50ms,我们上面列出的场景:支付服务、订单服务、积分服务、商家服务、物流服务、推荐服务,总共需要 300ms。
-
Adobe国际认证中文官方网站】Adobe中国摄影计划,免费安装正版激活--Adobe Creative Cloud中国摄影计划。与此同时,Adobe宣布天猫为Adobe Creative Cloud中国摄影计划的电商战略合作伙伴,并将与其合作上线Adobe天猫官方旗舰店。 此举无疑一方面扩大了Adobe在中国的影响力,另一方面也有助于国内用户更好地培养正版软件意识,推动Adobe软件在中国的正版化进程。 网络异常,图片无法显示 ||网络异常 Adobe Creative Cloud中国摄影计划包括Photoshop和Lightroom Classic两大桌面创意工具,以及iOS版Photoshop Express。 其中,Adobe Lightroom Classic和Adobe Photoshop作为两款常用的图像处理软件,对于那些玩摄影、后期修图的创意设计人群无疑有着巨大的帮助,而LR+PS套装对于摄影领域用户的重要性自不必说,正版产品的性能实时更新也可以放心!体验最新功能,对于新镜头(补偿)和机身(RAW 读取)都能第一时间适应。不信你看: Photoshop 图像合成 裁剪、移除对象、润饰合成照片、玩转色彩和特效,创建精美图片和艺术品! Lightroom Classic 照片编辑 轻松批量管理和编辑照片,内置专业创意控件和摄影师预设,让你的照片大放异彩。 手机 PS 便捷编辑 Photoshop Express 支持多种滤镜、贴纸,手机即可完成抠图、除雾等任务 人工智能编辑工具 神经滤镜、快速点击选区、自动选择主题等人工智能功能让图像编辑更轻松 创意画笔内容识别 定制艺术画笔工具,实现个性化效果;内容识别填充,智能去除无用物体。 Adobe Creative Cloud 中国摄影计划的推出,为中国的专业摄影师、摄影爱好者、后期修图和其他创意设计人员带来了全方位的内容和体验。 网络异常,图片无法显示 ||网络异常 当然,不可否认的是,"由于盗版软件缺乏开发、维护和升级成本,销售价格远低于正版软件。再加上很多普通人并不需要使用正版软件的复杂功能,版权观念较淡,还是有大量的创意设计人员会选择盗版软件"。 但事实上,当所有的软件都不再是单一的软件,而是变成一种服务时,单机版盗版的存在就逐渐成为鸡肋。因为有太多的服务让你即使是所谓的 "完美破解",也无法享受,Adobe Cloud 就是一个很好的例子,所谓的完美破解,你只能使用 "Adobe "的一半,对于更精彩的 "云",只能望云兴叹。更何况,越来越多的设计工具从免费走向付费,越来越多的设计师和企业已经接受了付费使用的模式。 其次,对于互联网时代的企业数字化转型而言,数字化合规至关重要。21年来,使用盗版PS和未经授权的方正字体被指侵权的事情闹得沸沸扬扬,虽然新闻真假难辨,但也给使用盗版工具的用户敲响了警钟。 付费使用正版工具,可以更放心地进行设计,不用担心版权风险!
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面