├── .nojekyll ├── INTRODUCTION.md ├── 01~编程范式 ├── 函数式编程 │ ├── 函数组合.md │ ├── README.md │ ├── 术语概念.md │ └── 99~参考资料 │ │ ├── 2018~手把手介绍函数式编程:从命令式重构到函数式.md │ │ └── 2006~Functional Programming For The Rest of Us.md ├── 面向对象编程 │ ├── 继承与组合.md │ ├── OOP 的缺陷.md │ └── README.md ├── .DS_Store ├── 事件驱动编程 │ └── README.md ├── 基础范式 │ ├── 命令式编程.md │ └── 声明式编程.md ├── README.md └── 元编程 │ └── README.md ├── 02~面向对象的设计模式 ├── 04~其他模式 │ └── README.md ├── .DS_Store ├── 99~参考资料 │ ├── 李兴华~《研磨设计模式》 │ │ └── README.md │ ├── 2015~《设计模式之禅》 │ │ └── README.md │ └── 《Refactoring Guru》 │ │ ├── 01~创建型模式 │ │ └── .DS_Store │ │ └── README.md ├── 00~SOLID │ ├── 最少知识.md │ ├── 开放封闭.md │ ├── 单一职责.md │ ├── README.md │ ├── 里氏替换.md │ ├── 接口隔离.md │ └── 依赖倒置.md ├── 01~创建型模式 │ ├── README.md │ ├── 单例.md │ ├── 原型.md │ ├── 构建器.md │ ├── 工厂方法.md │ └── 抽象工厂.md ├── 03~行为型模式 │ ├── README.md │ ├── 备忘录.md │ ├── 策略.md │ ├── 模板方法.md │ ├── 访问者 │ │ └── README.md │ ├── 中介者.md │ ├── 职责链.md │ ├── 观察者.md │ ├── 迭代器.md │ ├── 状态.md │ └── 命令.md ├── README.md └── 02~结构型模式 │ ├── 外观.md │ ├── 享元.md │ ├── 桥接.md │ ├── 适配器.md │ ├── 组合.md │ ├── 装饰.md │ └── 代理.md ├── 03~软件架构设计 ├── 04~服务设计模式 │ ├── 权限认证 │ │ └── README.link │ └── N + 1 查询 │ │ └── README.md ├── 03~领域驱动设计 │ └── README.link ├── 05~系统架构设计 │ └── README.link └── 2024~软件架构的考虑维度.md ├── .DS_Store ├── .gitattributes ├── .gitignore ├── _sidebar.md ├── README.md ├── index.html └── LICENSE /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /INTRODUCTION.md: -------------------------------------------------------------------------------- 1 | # 本篇导读 2 | -------------------------------------------------------------------------------- /01~编程范式/函数式编程/函数组合.md: -------------------------------------------------------------------------------- 1 | # 函数组合 2 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/04~其他模式/README.md: -------------------------------------------------------------------------------- 1 | # 其他模式 2 | -------------------------------------------------------------------------------- /03~软件架构设计/04~服务设计模式/权限认证/README.link: -------------------------------------------------------------------------------- 1 | https://github.com/wx-chevalier/Auth-Notes.git -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DesignPattern-Notes/master/.DS_Store -------------------------------------------------------------------------------- /01~编程范式/面向对象编程/继承与组合.md: -------------------------------------------------------------------------------- 1 | # 继承与组合 2 | 3 | - https://lwn.net/SubscriberLink/787800/b7f5351b3a41421a/ 4 | -------------------------------------------------------------------------------- /03~软件架构设计/03~领域驱动设计/README.link: -------------------------------------------------------------------------------- 1 | https://github.com/wx-chevalier/DDD-and-Clean-Architecture-Notes.git -------------------------------------------------------------------------------- /03~软件架构设计/05~系统架构设计/README.link: -------------------------------------------------------------------------------- 1 | [](https://github.com/wx-chevalier/System-Architecture-Notes.git) 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.xmind filter=lfs diff=lfs merge=lfs -text 2 | *.pdf filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /01~编程范式/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DesignPattern-Notes/master/01~编程范式/.DS_Store -------------------------------------------------------------------------------- /02~面向对象的设计模式/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DesignPattern-Notes/master/02~面向对象的设计模式/.DS_Store -------------------------------------------------------------------------------- /02~面向对象的设计模式/99~参考资料/李兴华~《研磨设计模式》/README.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://zq99299.github.io/note-book/design_pattern/) 2 | 3 | # 研磨设计模式 4 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/99~参考资料/2015~《设计模式之禅》/README.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | - https://blueblue233.github.io/blog/5116627/ 4 | - https://github.com/tianzhich/design-pattern-note 5 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/99~参考资料/《Refactoring Guru》/01~创建型模式/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/DesignPattern-Notes/master/02~面向对象的设计模式/99~参考资料/《Refactoring Guru》/01~创建型模式/.DS_Store -------------------------------------------------------------------------------- /03~软件架构设计/04~服务设计模式/N + 1 查询/README.md: -------------------------------------------------------------------------------- 1 | # N + 1 查询 2 | 3 | # Links 4 | 5 | - https://ananthakumaran.in/2023/01/01/solving_n_plus_1_queries_on_rails.html Understanding N + 1 queries problem 6 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/00~SOLID/最少知识.md: -------------------------------------------------------------------------------- 1 | # 最少知识原则 2 | 3 | > Only talk to you immediate friends. 4 | 5 | 只与你最直接的朋友交流。尽量减少对象之间的交互,从而减小类之间的耦合。简言之,一定要做到:低耦合,高内聚。在做系统设计时,不要让一个类依赖于太多的其他类,需尽量减小依赖关系,否则,您死都不知道自己怎么死的。该原则也称为“迪米特法则(Law of Demeter)”,由 Ian Holland 提出。这个人不太愿意和陌生人说话,只和他走得最近的朋友们交流。 6 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/01~创建型模式/README.md: -------------------------------------------------------------------------------- 1 | # 创建型模式 2 | 3 | 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。 4 | 5 | 创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。 6 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/99~参考资料/《Refactoring Guru》/README.md: -------------------------------------------------------------------------------- 1 | # Refactoring Guru 2 | 3 | - 创建型模式:这类模式提供创建对象的机制,能够提升已有代码的灵活性和可复用性。 4 | - 结构型模式:这类模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。 5 | - 行为模式:这类模式负责对象间的高效沟通和职责委派。 6 | 7 |  8 | -------------------------------------------------------------------------------- /01~编程范式/面向对象编程/OOP 的缺陷.md: -------------------------------------------------------------------------------- 1 | # OOP 的缺陷 2 | 3 | # 鸭嘴兽效应 4 | 5 | 现实世界并不总是能被整齐地划分成具有明确属性定义的类别。例如,假设我们创建了一个代表动物王国的类层次结构。该类层次结构中既包含爬行动物(冷血、有鳞片、产卵等等),又包含哺乳动物(恒温、有毛、生育等等),还包含鸟类、两栖动物、无脊椎动物等等。 6 | 7 | 然而,对于鸭嘴兽,它似乎不属于我们上述定义的任何类别。我们要做什么呢?我们是创建一个全新的类别,还是重新考虑整个分类方案呢?就工作量和程序复杂性而言,这两种方法都会产生显著的成本。 8 | 9 | # 内部逻辑与外部逻辑 10 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/README.md: -------------------------------------------------------------------------------- 1 | # 行为型模式 2 | 3 | 行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象 之间的交互。 4 | 5 | 在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。行为型模式分为类行为型模式和对象行为型模式两种:类行为型模式:类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。对象行为型模式:对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。 6 | -------------------------------------------------------------------------------- /01~编程范式/事件驱动编程/README.md: -------------------------------------------------------------------------------- 1 | # 事件驱动编程 2 | 3 | 其实,基于事件驱动的程序设计在图形用户界面(GUI)出现很久前就已经被应用于程序设计中,可是只有当图形用户界面广泛流行时,它才逐渐形演变为一种广泛使用的程序设计模式。在过程式的程序设计中,代码本身就给出了程序执行的顺序,尽管执行顺序可能会受到程序输入数据的影响。 4 | 5 | 在事件驱动的程序设计中,程序中的许多部分可能在完全不可预料的时刻被执行。往往这些程序的执行是由用户与正在执行的程序的互动激发所致。 6 | 7 | - 事件:就是通知某个特定的事情已经发生(事件发生具有随机性)。 8 | - 事件与轮询:轮询的行为是不断地观察和判断,是一种无休止的行为方式。而事件是静静地等待事情的发生。事实上,在 Windows 出现之前,采用鼠标输入字符模式的 PC 应用程序必须进行串行轮询,并以这种方式来查询和响应不同的用户操做。 9 | - 事件处理器:是对事件做出响应时所执行的一段程序代码。事件处理器使得程序能够对于用户的行为做出反映。 10 | 11 | 事件驱动常常用于用户与程序的交互,通过图形用户接口(鼠标、键盘、触摸板)进行交互式的互动。当然,也可以用于异常的处理和响应用户自定义的事件等等。 12 | 13 | 事件驱动不仅仅局限在 GUI 编程应用。但是实现事件驱动我们还需要考虑更多的实际问题,如:事件定义、事件触发、事件转化、事件合并、事件排队、事件分派、事件处理、事件连带等等。 14 | 15 | 其实,到目前为止,我们还没有找到有关纯事件驱动编程的语言和类似的开发环境。所有关于事件驱动的资料都是基于 GUI 事件的。 16 | 属于事件驱动的编程语言有:VB、C#、Java(Java Swing 的 GUI)等。它们所涉及的事件绝大多数都是 GUI 事件。 17 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/00~SOLID/开放封闭.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # 开放封闭原则 4 | 5 | > Software entities like classes, modules and functions should be open for extension but closed for modifications. 6 | 7 | 软件实体,如:类、模块与函数,对于扩展应该是开放的,但对于修改应该是封闭的。简言之,对扩展开放,对修改封闭。当需求发生改变的时候,我们需要对代码进行修改,这个时候我们应该尽量去扩展原来的代码,而不是去修改原来的代码,因为这样可能会引起更多的问题。 8 | 9 | 但是如果能够确保对整体架构不会产生任何影响,那么也没必要搞得那么复杂了;直接改这个类吧,有时候过度拘泥模式而导致代码膨胀反而得不偿失。假设你是一名成功的开源类库作者,很多开发者使用你的类库。如果某天你要扩展功能,只能通过修改某些代码完成,结果导致类库的使用者都需要修改代码。更可怕的是,他们被迫修改了代码后,又可能造成别的依赖者也被迫修改代码。这种场景绝对是一场灾难。如果你的设计是满足开闭原则的,那就完全是另一种场景。你可以通过扩展,而不是修改来改变软件的行为,将对依赖方的影响降到最低。 10 | 11 | 实现开闭原则的关键是抽象。在 Bertrand Meyer 提出开闭原则的年代(上世纪 80 年代),在类库中增加属性或方法,都不可避免地要修改依赖此类库的代码。这显然导致软件很难维护,因此他强调的是要允许通过继承来扩展类。随着技术发展,我们有了更多的方法来实现开闭原则,包括接口、抽象类、策略模式等。 12 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/00~SOLID/单一职责.md: -------------------------------------------------------------------------------- 1 | # 单一职责原则 2 | 3 | > There should never be more than one reason for a class to change. 4 | 5 | 单一职责原则的定义是就一个类而言,应该仅有一个引起他变化的原因。也就是说一个类应该只负责一件事情。如果一个类负责了方法 M1,方法 M2 两个不同的事情,当 M1 方法发生变化的时候,我们需要修改这个类的 M1 方法,但是这个时候就有可能导致 M2 方法不能工作。这个不是我们期待的,但是由于这种设计却很有可能发生。所以这个时候,我们需要把 M1 方法,M2 方法单独分离成两个类,让每个类只专心处理自己的方法。 6 | 7 | 要真正理解并正确运用单一职责原则,并没有那么容易。单一职责就跟“盐少许”一样,不好把握。单一职责原则某种程度上说是在分离关注点。分离不同角色的关注点,分离不同时间的关注点。 8 | 9 | - 利益相关者角色是一个重要的变化原因,不同的角色会有不同的需求,从而产生不同的变化原因。作为居民,家用的电线是普通的 220V 电线,而对电网建设者,使用的是高压电线。用一个 Wire 类同时服务于两类角色,通常意味着坏味道。 10 | 11 | - 变更频率是另一个值得考虑的变化原因。即使对同一类角色,需求变更的频率也会存在差异。最典型的例子是业务处理的需求比较稳定,而业务展示的需求更容易发生变更,毕竟人总是喜新厌旧的。因此这两类需求通常要在不同的类中实现。 12 | 13 | 单一职责原则可以降低类的复杂度,一个类只负责一项职责,这样逻辑也简单很多。提高类的可读性,和系统的维护性,因为不会有其他奇怪的方法来干扰我们理解这个类的含义 当发生变化的时候,能将变化的影响降到最小,因为只会在这个类中做出修改。 14 | 15 | # 案例:书籍与打印机 16 | -------------------------------------------------------------------------------- /01~编程范式/函数式编程/README.md: -------------------------------------------------------------------------------- 1 | # 函数式编程 2 | 3 | 函数式编程(functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。函数式编程关心类型(代数结构)之间的关系,命令式编程关心解决问题的步骤。函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。 4 | 5 | 命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。 6 | 而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。 7 | 函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方,比如你可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。 8 | 9 | 其中,λ 演算(lambda calculus)为该语言最重要的基础。而且,λ 演算的函数可以接受函数当作输入(引数)和输出(传出值)。函数式编程中的 lambda 可以看成是两个类型之间的关系,一个输入类型和一个输出类型。lambda 演算就是给 lambda 表达式一个输入类型的值,则可以得到一个输出类型的值,这是一个计算,计算过程满足 -等价和 -规约。函数式编程的思维就是如何将这个关系组合起来,用数学的构造主义将其构造出你设计的程序。 10 | 11 | # 特性 12 | 13 | # Links 14 | 15 | - https://www.zhoulujun.cn/html/theory/model/8139.html?from=groupmessage&isappinstalled=0 16 | - https://mp.weixin.qq.com/s/1pAzdyBR4qE1A2aZF4GVIw 17 | -------------------------------------------------------------------------------- /01~编程范式/基础范式/命令式编程.md: -------------------------------------------------------------------------------- 1 | # 命令式编程 2 | 3 | 命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。从本质上讲,它是“冯.诺依曼机”运行机制的抽象,它的编程思想方式源于计算机指令的顺序排列。过程化语言模拟的是计算机机器的系统构造,而并不是基于语言的使用者的个人能力和倾向。 4 | 5 | 程序流程图是命令式语言进行程序编写的有效辅助手段。命令式语言特别适合解决线性(或者说按部就班)的算法问题。它强调“自上而下(自顶向下)”“精益求精”的设计方式。这种方式非常类似我们的工作和生活方式,因为我们的日常活动都是按部就班的顺序进行的。 6 | 7 | 命令式语言趋向于开发运行较快且对系统资源利用率较高的程序。命令式语言非常的灵活并强大,同时有许多经典应用范例,这使得程序员可以用它来解决多种问题。命令式语言的不足之处就是它不适合某些种类问题的解决,例如那些非结构化的具有复杂算法的问题。问题出现在,命令式语言必须对一个算法加以详尽的说明,并且其中还要包括执行这些指令或语句的顺序。实际上,给那些非结构化的具有复杂算法的问题给出详尽的算法是极其困难的。 8 | 9 | 命令式对实际事物处理一般可以拆分为以下两种模式: 10 | 11 | - 流程驱动:一般就是主动轮询 在干活中还要分心 主动去找活干 这样有空余的时间也完全浪费掉了。采用警觉式者主动去轮询(polling),行为取决于自身的观察判断,是流程驱动的,符合常规的流程驱动式编程(Flow-Driven Programming)的模式。 12 | 13 | - 事件驱动:比如公司有一个 oa 系统 你干完活的时候只需要看下 oa 系统有没分配给你活 没有可以干自己的事 不用担心还有其他事没干完采用托付式者被动等通知(notification),行为取决于外来的突发事件,是事件驱动 的,符合事件驱动式编程(Event-Driven Programming,简称 EDP)的模式。 14 | 15 | # Links 16 | 17 | - https://www.cnblogs.com/zhoulujun/p/10800344.html 18 | - [编程语言漫谈](http://tech.youzan.com/programming-language/) 19 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/00~SOLID/README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # SOLID 4 | 5 | - 单一职责原则(Single Responsibility Principle, SRP):一个类只允许有一个职责,即只有一个导致该类变更的原因。 6 | 7 | - 开放封闭原则(Open Closed Principle, OCP):一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 8 | 9 | - 里式替换原则(Liskov Substitution Principle, LSP):所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。 10 | 11 | - 最少知识原则(Least Knowledge Principle, LKP):又称迪米特法则(Law of Demeter),一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。 12 | 13 | - 接口分离原则(Interface Segregation Principle,ISP):多个特定的客户端接口要好于一个通用性的总接口。 14 | 15 | - 依赖倒置原则(Dependency Inversion Principle, DIP):依赖抽象,而不是依赖实现;抽象不应该依赖细节;细节应该依赖抽象;高层模块不能依赖低层模块,二者都应该依赖抽象。 16 | 17 | 将以上六大原则的英文首字母拼在一起就是 SOLID(稳定的),所以也称之为 SOLID 原则。 18 | 19 |  20 | 21 | 单一职责是所有设计原则的基础,开闭原则是设计的终极目标。里氏替换原则强调的是子类替换父类后程序运行时的正确性,它用来帮助实现开闭原则。而接口隔离原则用来帮助实现里氏替换原则,同时它也体现了单一职责。依赖倒置原则是过程式编程与 OO 编程的分水岭,同时它也被用来指导接口隔离原则。 22 | 23 | # Links 24 | 25 | - https://www.baeldung.com/solid-principles 26 | -------------------------------------------------------------------------------- /01~编程范式/README.md: -------------------------------------------------------------------------------- 1 | # 编程范式 2 | 3 | 托马斯.库尔提出“科学的革命”的范式论后,Robert Floyd 在 1979 年图灵奖的颁奖演说中使用了编程范式一词。编程范式一般包括三个方面,以 OOP 为例: 4 | 5 | - 学科的逻辑体系——规则范式:如 类/对象、继承、动态绑定、方法改写、对象替换等等机制。 6 | 7 | - 心理认知因素——心理范式:按照面向对象编程之父 Alan Kay 的观点,“计算就是模拟”。OO 范式极其重视隐喻(metaphor)的价值,通过拟人化,按照自然的方式模拟自然。 8 | 9 | - 自然观/世界观——观念范式:强调程序的组织技术,视程序为松散耦合的对象/类的组合,以继承机制将类组织成一个层次结构,把程序运行视为相互服务的对象之间的对话。 10 | 11 | 简单来说,编程范式是程序员看待程序应该具有的观点,代表了程序设计者认为程序应该如何被构建和执行的看法。编程范式是编程语言的一种分类方式,它并不针对某种编程语言。就编程语言而言,一种语言可以适用多种编程范式。 12 | 13 | 常见的编程范式有:命令式、过程式、说明式、面向对象、函数式、泛型编程等。事实上,凡是非命令式的编程都可归为声明式编程。因此,命令式、函数式和逻辑式是最核心的三种范式。为清楚起见,我们用一幅图来表示它们之间的关系。 14 | 15 |  16 | 17 | 与命令式编程相对的声明式编程(declarative programming)。顾名思义,声明式编程由若干规范(specification)的声明组成的,即一系列陈述句:‘已知这,求解那’,强调‘做什么’而非‘怎么做’。声明式编程是人脑思维方式的抽象,即利用数理逻辑或既定规范对已知条件进行推理或运算。 18 | 19 | 一些编程语言是专门为某种特定范式设计的,例如 C 语言是过程式编程语言;Smalltalk 和 Java 是较纯粹的面向对象编程语言;Haskell 是纯粹的函数式编程语言。另外一些编程语言和编程范式的关系并不一一对应,如 Python,Scala,Groovy 都支持面向对象和一定程度上的函数式编程。C++是多范式编程语言成功的典范。C++ 支持和 C 语言一样的过程式编程范式,同时也支持面向对象编程范式,STL(Standard Template Library)使 C++具有了泛型编程能力。支持多种范式可能是 C++直到现在仍然具有强大的生命力的原因之一。Swift 是一门典型的多范式编程语言,即支持面向对象编程范式,也支持函数式编程范式,同时还支持泛型编程。Swift 支持多种编程范式是由其创造目标决定的。 20 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/README.md: -------------------------------------------------------------------------------- 1 | # 设计模式 2 | 3 | 设计模式(Design Patterns)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 4 | 5 | > Descriptions of communicating objects and classes that are customized to solve a general design problem in a particular context. 6 | 7 | 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。 8 | 9 | > 本篇侧重于对于设计模式的理论介绍,对于不同语言的实践请参阅《[PL-Notes](https://github.com/wx-chevalier/PL-Notes?q=)》中各个具体编程语言的设计模式章节。 10 | 11 | ## Gang of Four 12 | 13 | 在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素)的书,该书首次提到了软件开发中设计模式的概念。 14 | 15 | 四位作者合称 GOF(四人帮,全拼 Gang of Four)。他们所提出的设计模式主要是基于以下的面向对象设计原则: 16 | 17 | - 对接口编程而不是对实现编程。 18 | - 优先使用对象组合而不是继承。 19 | 20 |  21 | 22 | # Links 23 | 24 | - [Refactoring Guru](https://refactoringguru.cn/design-patterns/factory-method) 25 | 26 | - https://mp.weixin.qq.com/s/yEAdDRijRav9bYgvR8a8QQ 2.5 万字详解:23 种设计模式 27 | 28 | - https://har01d.cn/notes/prototype-pattern.html 29 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/00~SOLID/里氏替换.md: -------------------------------------------------------------------------------- 1 | # 里氏替换原则 2 | 3 | > Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. 4 | 5 | 该原则由麻省理工学院的 Barbara Liskov 女士提出,即使用基类的指针或引用的函数,必须是在不知情的情况下,能够使用派生类的对象。父类能够替换子类,但子类不一定能替换父类。也就是说,在代码中可以将父类全部替换为子类,程序不会报错,也不会在运行时出现任何异常,但反过来却不一定成立。学过 OO 的同学都知道,子类本来就可以替换父类,为什么还要里氏替换原则呢?这里强调的不是编译错误,而是程序运行时的正确性。程序运行的正确性通常可以分为两类。一类是不能出现运行时异常,最典型的是 UnsupportedOperationException,也就是子类不支持父类的方法。第二类是业务的正确性,这取决于业务上下文。 6 | 7 | 下例中,由于 java.sql.Date 不支持父类的 toInstance 方法,当父类被它替换时,程序无法正常运行,破坏了父类与调用方的契约,因此违反了里氏替换原则。 8 | 9 | ```java 10 | package java.sql; 11 | 12 | public class Date extends java.util.Date { 13 | 14 | @Override 15 | public Instant toInstant() { 16 | throw new java.lang.UnsupportedOperationException(); 17 | } 18 | } 19 | ``` 20 | 21 | 该原则包含以下几层要求: 22 | 23 | - 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法,子类可以增加自己独有的方法。 24 | - 当子类的方法重载父类的方法时候,方法的形参要比父类的方法的输入参数更加宽松。 25 | - 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。 26 | 27 | 里氏替换原则之所以这样要求是因为继承有很多缺点,他虽然是复用代码的一种方法,但同时继承在一定程度上违反了封装。父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。这也导致了,如果需求变更,子类对父类的方法进行一些复写的时候,其他的子类无法正常工作。 28 | 29 | 如果你的设计满足里氏替换原则,那么子类(或接口的实现类)就可以保证正确性的前提下替换父类(或接口),改变系统的行为,从而实现扩展。BranchByAbstraction 和绞杀者模式 都是基于里氏替换原则,实现系统扩展和演进。这也就是对修改封闭,对扩展开放,因此里氏替换原则是实现开闭原则的一种解决方案。 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | .DS_Store 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/02~结构型模式/外观.md: -------------------------------------------------------------------------------- 1 | # 外观 2 | 3 | 外观是一种结构型设计模式,能为程序库、框架或其他复杂类提供一个简单的接口。 4 | 5 | # 案例:视频转换 6 | 7 | ```ts 8 | // 这里有复杂第三方视频转换框架中的一些类。我们不知晓其中的代码,因此无法 9 | // 对其进行简化。 10 | 11 | class VideoFile 12 | // ... 13 | 14 | class OggCompressionCodec 15 | // ... 16 | 17 | class MPEG4CompressionCodec 18 | // ... 19 | 20 | class CodecFactory 21 | // ... 22 | 23 | class BitrateReader 24 | // ... 25 | 26 | class AudioMixer 27 | // ... 28 | 29 | 30 | // 为了将框架的复杂性隐藏在一个简单接口背后,我们创建了一个外观类。它是在 31 | // 功能性和简洁性之间做出的权衡。 32 | class VideoConverter is 33 | method convert(filename, format):File is 34 | file = new VideoFile(filename) 35 | sourceCodec = new CodecFactory.extract(file) 36 | if (format == "mp4") 37 | destinationCodec = new MPEG4CompressionCodec() 38 | else 39 | destinationCodec = new OggCompressionCodec() 40 | buffer = BitrateReader.read(filename, sourceCodec) 41 | result = BitrateReader.convert(buffer, destinationCodec) 42 | result = (new AudioMixer()).fix(result) 43 | return new File(result) 44 | 45 | // 应用程序的类并不依赖于复杂框架中成千上万的类。同样,如果你决定更换框架, 46 | // 那只需重写外观类即可。 47 | class Application is 48 | method main() is 49 | convertor = new VideoConverter() 50 | mp4 = convertor.convert("funny-cats-video.ogg", "mp4") 51 | mp4.save() 52 | ``` 53 | -------------------------------------------------------------------------------- /01~编程范式/元编程/README.md: -------------------------------------------------------------------------------- 1 | ## 元编程 2 | 3 | 元编程是用来产生代码的程序,操纵代码的程序,在运行时创建和修改代码而非编程时,这种程序叫做元程序。而编写这种程序就叫做元编程。比如编译原理中用来生成词法分析器和语法分析器的 lex 和 yacc。 4 | 5 | 元编程技术在多种编程语言中都可以使用,但更多的还是被应用于动态语言中,因为动态语言提供了更多的在运行时将代码视为数据进行操纵的能力。虽然像 C#和 Java 这样较为静态的语言也提供了反射机制,但是仍然没有诸如 Ruby 这样的更趋动态性的语言那么透明,这是因为静态语言在运行时其代码和数据是分布在两个层次上的。 6 | 7 | 元编程是指某类[计算机程序]的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在[运行时]完成部分本应在[编译时]完成的工作。很多情况下比手工编写全部代码相比工作效率更高。编写元程序的语言称之为元语言,被操作的语言称之为目标语言。一门语言同时也是自身的元语言的能力称之为反射。 8 | 9 | 反射是促进元编程的一种很有价值的语言特性。把编程语言自身作为头等对象(如 Lisp 或 Rebol)也很有用。支持泛型编程的语言也使用元编程能力。 10 | 11 | 元编程通常有两种方式起作用。一种方式是通过应用程序接口(API)来暴露运行时引擎的内部信息。另一种方法是动态执行包含编程命令的字符串。因此,“程序能编写程序”。虽然两种方法都能用,但大多数方法主要靠其中一种。 12 | 13 | 使用示例一个简单元编程的例子是使用 bash 脚本的产生式编程示例: 14 | 15 | ```shell 16 | #!/bin/bash 17 | # metaprogram 18 | echo '#!/bin/bash' >program 19 | for ((I=1; I<=992; I++)) do 20 | echo "echo $I" >>program 21 | done 22 | chmod +x program 23 | ``` 24 | 25 | 这个脚本(或程序)生成了一个新的 993 行程序来打印 1 至 992。这只是演示用代码来写更多代码,并不是打印数字的最有效方法。然而,一个程序员可以几分钟内编写和执行元程序,却生成了近 1000 行代码。 26 | 27 | 不是所有的元编程都用产生式编程。如果程序可以在运行时改变(如 Lisp、Python、REBOL、Smalltalk、Ruby、PHP、Perl, Tcl、Lua、Groovy 和 JavaScript),这种技术可以不实际生成源代码就使用元编程。 28 | 29 | 最常用的元编程工具是编译器,把高级语言转换为汇编语言或机器语言。更灵活的方法是在程序中嵌入解释器直接处理程序数据。有一些实现例如为 Object Pascal 编写的 RemObject's Pascal Script。 30 | 31 | 另一个很常用的元编程例子是 lex 和 yacc,用来生成词法分析器和语法分析器。Yacc 通常用作编译器的编译器,生成一个把高级语言转换为机器语言的工具。 32 | 33 | quine 是一种源代码等于输出的特殊的元程序。 34 | 35 | 面向语言的程序设计是一种强烈关注元编程的编程风格,通过领域特定语言来实现。 36 | -------------------------------------------------------------------------------- /01~编程范式/面向对象编程/README.md: -------------------------------------------------------------------------------- 1 | # Object Oriented Programming(面向对象编程) 2 | 3 | Smalltalk 的设计者、面向对象编程之父 Alan Kay 曾经这样描述面向对象的本质:很久以前,我在描述“面向对象编程”时使用了“对象”这个概念。很抱歉这个概念让许多人误入歧途,他们将学习的重心放在了“对象”这个次要的方面。真正主要的方面是“消息”,日文中有一个词 ma,表示“间隔”,与其最为相近的英文或许是“ interstitial”。创建一个规模宏大且可生长的系统的关键在于其模块之间应该如何交流,而不在于其内部的属性和行为应该如何表现。 4 | 5 | 面向对象程序设计(Object-oriented programming OOP)是种通过类、方法、对象和消息传递,来支持面向对象的程序设计范式。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,程序会被设计成彼此相关的对象。 6 | 7 | 面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对计算机下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。即把事情交给最适合的对象去做。 8 | 9 | 面向对象和面向过程的区别最直观的比喻就如:摇(狗尾巴)和 狗.摇尾巴()的区别。 10 | 11 | # 三大特性 12 | 13 | 面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。多态的作用:消除类型之间的耦合关系。现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。 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 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/02~结构型模式/享元.md: -------------------------------------------------------------------------------- 1 | # 享元 2 | 3 | 享元是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象。 4 | 5 | # 案例:画布 6 | 7 | ```ts 8 | // 享元类包含一个树的部分状态。这些成员变量保存的数值对于特定树而言是唯一 9 | // 的。例如,你在这里找不到树的坐标。但这里有很多树木之间所共有的纹理和颜 10 | // 色。由于这些数据的体积通常非常大,所以如果让每棵树都其进行保存的话将耗 11 | // 费大量内存。因此,我们可将纹理、颜色和其他重复数据导出到一个单独的对象 12 | // 中,然后让众多的单个树对象去引用它。 13 | class TreeType is 14 | field name 15 | field color 16 | field texture 17 | constructor TreeType(name, color, texture) { ... } 18 | method draw(canvas, x, y) is 19 | // 1. 创建特定类型、颜色和纹理的位图。 20 | // 2. 在画布坐标 (X,Y) 处绘制位图。 21 | 22 | // 享元工厂决定是否复用已有享元或者创建一个新的对象。 23 | class TreeFactory is 24 | static field treeTypes: collection of tree types 25 | static method getTreeType(name, color, texture) is 26 | type = treeTypes.find(name, color, texture) 27 | if (type == null) 28 | type = new TreeType(name, color, texture) 29 | treeTypes.add(type) 30 | return type 31 | 32 | // 情景对象包含树状态的外在部分。程序中可以创建数十亿个此类对象,因为它们 33 | // 体积很小:仅有两个整型坐标和一个引用成员变量。 34 | class Tree is 35 | field x,y 36 | field type: TreeType 37 | constructor Tree(x, y, type) { ... } 38 | method draw(canvas) is 39 | type.draw(canvas, this.x, this.y) 40 | 41 | // 树和森林类是享元的客户端。如果不打算继续对树类进行开发,你可以将它们合 42 | // 并。 43 | class Forest is 44 | field trees: collection of Trees 45 | 46 | method plantTree(x, y, name, color, texture) is 47 | type = TreeFactory.getTreeType(name, color, texture) 48 | tree = new Tree(x, y, type) 49 | trees.add(tree) 50 | 51 | method draw(canvas) is 52 | foreach (tree in trees) do 53 | tree.draw(canvas) 54 | ``` 55 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/备忘录.md: -------------------------------------------------------------------------------- 1 | # 备忘录 2 | 3 | 备忘录是一种行为设计模式,允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。 4 | 5 | # 案例:复杂文字编辑器 6 | 7 | ```ts 8 | // 原发器中包含了一些可能会随时间变化的重要数据。它还定义了在备忘录中保存 9 | // 自身状态的方法,以及从备忘录中恢复状态的方法。 10 | class Editor is 11 | private field text, curX, curY, selectionWidth 12 | 13 | method setText(text) is 14 | this.text = text 15 | 16 | method setCursor(x, y) is 17 | this.curX = curX 18 | this.curY = curY 19 | 20 | method setSelectionWidth(width) is 21 | this.selectionWidth = width 22 | 23 | // 在备忘录中保存当前的状态。 24 | method createSnapshot():Snapshot is 25 | // 备忘录是不可变的对象;因此原发器会将自身状态作为参数传递给备忘 26 | // 录的构造方法。 27 | return new Snapshot(this, text, curX, curY, selectionWidth) 28 | 29 | // 备忘录类保存有编辑器的过往状态。 30 | class Snapshot is 31 | private field editor: Editor 32 | private field text, curX, curY, selectionWidth 33 | 34 | constructor Snapshot(editor, text, curX, curY, selectionWidth) is 35 | this.editor = editor 36 | this.text = text 37 | this.curX = curX 38 | this.curY = curY 39 | this.selectionWidth = selectionWidth 40 | 41 | // 在某一时刻,编辑器之前的状态可以使用备忘录对象来恢复。 42 | method restore() is 43 | editor.setText(text) 44 | editor.setCursor(curX, curY) 45 | editor.setSelectionWidth(selectionWidth) 46 | 47 | // 命令对象可作为负责人。在这种情况下,命令会在修改原发器状态之前获取一个 48 | // 备忘录。当需要撤销时,它会从备忘录中恢复原发器的状态。 49 | class Command is 50 | private field backup: Snapshot 51 | 52 | method makeBackup() is 53 | backup = editor.createSnapshot() 54 | 55 | method undo() is 56 | if (backup != null) 57 | backup.restore() 58 | // ... 59 | ``` 60 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/02~结构型模式/桥接.md: -------------------------------------------------------------------------------- 1 | # 桥接 2 | 3 | 桥接是一种结构型设计模式,可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能在开发时分别使用。 4 | 5 | # 案例:设备与遥控器 6 | 7 | ```ts 8 | // “抽象部分”定义了两个类层次结构中“控制”部分的接口。它管理着一个指向“实 9 | // 现部分”层次结构中对象的引用,并会将所有真实工作委派给该对象。 10 | class RemoteControl is 11 | protected field device: Device 12 | constructor RemoteControl(device: Device) is 13 | this.device = device 14 | method togglePower() is 15 | if (device.isEnabled()) then 16 | device.disable() 17 | else 18 | device.enable() 19 | method volumeDown() is 20 | device.setVolume(device.getVolume() - 10) 21 | method volumeUp() is 22 | device.setVolume(device.getVolume() + 10) 23 | method channelDown() is 24 | device.setChannel(device.getChannel() - 1) 25 | method channelUp() is 26 | device.setChannel(device.getChannel() + 1) 27 | 28 | 29 | // 你可以独立于设备类的方式从抽象层中扩展类。 30 | class AdvancedRemoteControl extends RemoteControl is 31 | method mute() is 32 | device.setVolume(0) 33 | 34 | 35 | // “实现部分”接口声明了在所有具体实现类中通用的方法。它不需要与抽象接口相 36 | // 匹配。实际上,这两个接口可以完全不一样。通常实现接口只提供原语操作,而 37 | // 抽象接口则会基于这些操作定义较高层次的操作。 38 | interface Device is 39 | method isEnabled() 40 | method enable() 41 | method disable() 42 | method getVolume() 43 | method setVolume(percent) 44 | method getChannel() 45 | method setChannel(channel) 46 | 47 | 48 | // 所有设备都遵循相同的接口。 49 | class Tv implements Device is 50 | // ... 51 | 52 | class Radio implements Device is 53 | // ... 54 | 55 | 56 | // 客户端代码中的某个位置。 57 | tv = new Tv() 58 | remote = new RemoteControl(tv) 59 | remote.togglePower() 60 | 61 | radio = new Radio() 62 | remote = new AdvancedRemoteControl(radio) 63 | ``` 64 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/02~结构型模式/适配器.md: -------------------------------------------------------------------------------- 1 | # 适配器 2 | 3 | 适配器是一种结构型设计模式,它能使接口不兼容的对象能够相互合作。 4 | 5 | # 案例:方孔和圆钉 6 | 7 |  8 | 9 | 适配器假扮成一个圆钉(RoundPeg),其半径等于方钉(SquarePeg)横截面对角线的一半(即能够容纳方钉的最小外接圆的半径)。 10 | 11 | ```ts 12 | // 假设你有两个接口相互兼容的类:圆孔(RoundHole)和圆钉(RoundPeg)。 13 | class RoundHole is 14 | constructor RoundHole(radius) { ... } 15 | 16 | method getRadius() is 17 | // 返回孔的半径。 18 | 19 | method fits(peg: RoundPeg) is 20 | return this.getRadius() >= peg.radius() 21 | 22 | class RoundPeg is 23 | constructor RoundPeg(radius) { ... } 24 | 25 | method getRadius() is 26 | // 返回钉子的半径。 27 | 28 | 29 | // 但还有一个不兼容的类:方钉(SquarePeg)。 30 | class SquarePeg is 31 | constructor SquarePeg(width) { ... } 32 | 33 | method getWidth() is 34 | // 返回方钉的宽度。 35 | 36 | 37 | // 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适 38 | // 配器对象作为圆钉。 39 | class SquarePegAdapter extends RoundPeg is 40 | // 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。 41 | private field peg: SquarePeg 42 | 43 | constructor SquarePegAdapter(peg: SquarePeg) is 44 | this.peg = peg 45 | 46 | method getRadius() is 47 | // 适配器会假扮为一个圆钉, 48 | // 其半径刚好能与适配器实际封装的方钉搭配起来。 49 | return peg.getWidth() * Math.sqrt(2) / 2 50 | 51 | 52 | // 客户端代码中的某个位置。 53 | hole = new RoundHole(5) 54 | rpeg = new RoundPeg(5) 55 | hole.fits(rpeg) // true 56 | 57 | small_sqpeg = new SquarePeg(5) 58 | large_sqpeg = new SquarePeg(10) 59 | hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。 60 | 61 | small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg) 62 | large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg) 63 | hole.fits(small_sqpeg_adapter) // true 64 | hole.fits(large_sqpeg_adapter) // false 65 | ``` 66 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/策略.md: -------------------------------------------------------------------------------- 1 | # 策略 2 | 3 | 策略是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。 4 | 5 | # 案例:计算操作 6 | 7 | ```ts 8 | // 策略接口声明了某个算法各个不同版本间所共有的操作。上下文会使用该接口来 9 | // 调用有具体策略定义的算法。 10 | interface Strategy is 11 | method execute(a, b) 12 | 13 | // 具体策略会在遵循策略基础接口的情况下实现算法。该接口实现了它们在上下文 14 | // 中的互换性。 15 | class ConcreteStrategyAdd implements Strategy is 16 | method execute(a, b) is 17 | return a + b 18 | 19 | class ConcreteStrategySubtract implements Strategy is 20 | method execute(a, b) is 21 | return a - b 22 | 23 | class ConcreteStrategyMultiply implements Strategy is 24 | method execute(a, b) is 25 | return a * b 26 | 27 | // 上下文定义了客户端关注的接口。 28 | class Context is 29 | // 上下文会维护指向某个策略对象的引用。上下文不知晓策略的具体类。上下 30 | // 文必须通过策略接口来与所有策略进行交互。 31 | private strategy: Strategy 32 | 33 | // 上下文通常会通过构造函数来接收策略对象,同时还提供设置器以便在运行 34 | // 时切换策略。 35 | method setStrategy(Strategy strategy) is 36 | this.strategy = strategy 37 | 38 | // 上下文会将一些工作委派给策略对象,而不是自行实现不同版本的算法。 39 | method executeStrategy(int a, int b) is 40 | return strategy.execute(a, b) 41 | 42 | 43 | // 客户端代码会选择具体策略并将其传递给上下文。客户端必须知晓策略之间的差 44 | // 异,才能做出正确的选择。 45 | class ExampleApplication is 46 | method main() is 47 | 48 | // 创建上下文对象。 49 | 50 | // 读取第一个数。 51 | // 读取最后一个数。 52 | // 从用户输入中读取期望进行的行为。 53 | 54 | if (action == addition) then 55 | context.setStrategy(new ConcreteStrategyAdd()) 56 | 57 | if (action == subtraction) then 58 | context.setStrategy(new ConcreteStrategySubtract()) 59 | 60 | if (action == multiplication) then 61 | context.setStrategy(new ConcreteStrategyMultiply()) 62 | 63 | result = context.executeStrategy(First number, Second number) 64 | 65 | // 打印结果。 66 | ``` 67 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/模板方法.md: -------------------------------------------------------------------------------- 1 | # 模板方法 2 | 3 | 模板方法是一种行为设计模式,它在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。 4 | 5 | # 案例:策略游戏 6 | 7 | ```ts 8 | // 抽象类定义了一个模版方法,其中通常会包含某个由抽象原语操作调用组成的算 9 | // 法框架。具体子类会实现这些操作,但是不会对模版方法做出修改。 10 | class GameAI is 11 | // 模版方法定义了某个算法的框架。 12 | method turn() is 13 | collectResources() 14 | buildStructures() 15 | buildUnits() 16 | attack() 17 | 18 | // 某些步骤可在基类中直接实现。 19 | method collectResources() is 20 | foreach (s in this.builtStructures) do 21 | s.collect() 22 | 23 | // 某些可定义为抽象类型。 24 | abstract method buildStructures() 25 | abstract method buildUnits() 26 | 27 | // 一个类可包含多个模版方法。 28 | method attack() is 29 | enemy = closestEnemy() 30 | if (enemy == null) 31 | sendScouts(map.center) 32 | else 33 | sendWarriors(enemy.position) 34 | 35 | abstract method sendScouts(position) 36 | abstract method sendWarriors(position) 37 | 38 | // 具体类必须实现基类中的所有抽象操作,但是它们不能重写模版方法自身。 39 | class OrcsAI extends GameAI is 40 | method buildStructures() is 41 | if (there are some resources) then 42 | // 建造农场,接着是谷仓,然后是要塞。 43 | 44 | method buildUnits() is 45 | if (there are plenty of resources) then 46 | if (there are no scouts) 47 | // 建造苦工,将其加入侦查编组。 48 | else 49 | // 建造兽族步兵,将其加入战士编组。 50 | 51 | // ... 52 | 53 | method sendScouts(position) is 54 | if (scouts.length > 0) then 55 | // 将侦查编组送到指定位置。 56 | 57 | method sendWarriors(position) is 58 | if (warriors.length > 5) then 59 | // 将战斗编组送到指定位置。 60 | 61 | // 子类可以重写部分默认的操作。 62 | class MonstersAI extends GameAI is 63 | method collectResources() is 64 | // 怪物不会采集资源。 65 | 66 | method buildStructures() is 67 | // 怪物不会建造建筑。 68 | 69 | method buildUnits() is 70 | // 怪物不会建造单位。 71 | ``` 72 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/00~SOLID/接口隔离.md: -------------------------------------------------------------------------------- 1 | # 接口隔离原则 2 | 3 | > The dependency of one class to another one should depend on the smallest possible interface. 4 | 5 | 接口隔离原则说的是客户端不应该被迫依赖于它不使用的方法。简单来说就是更小和更具体的瘦接口比庞大臃肿的胖接口好。不要对外暴露没有实际意义的接口。换一种说法就是类间的依赖关系应该建立在最小的接口上。这样说好像更难懂。胖接口的职责过多,很容易违反单一职责原则,也会导致实现类不得不抛出 UnsupportedOperationException 这样的异常,违反里氏替换原则。因此,应该将接口设计得更瘦。 6 | 7 | 我们通过一个例子来说明。我们知道在 Java 中一个具体类实现了一个接口,那必然就要实现接口中的所有方法。如果我们有一个类 A 和类 B 通过接口 I 来依赖,类 B 是对类 A 依赖的实现,这个接口 I 有 5 个方法。但是类 A 与类 B 只通过方法 1,2,3 依赖,然后类 C 与类 D 通过接口 I 来依赖,类 D 是对类 C 依赖的实现但是他们却是通过方法 1,4,5 依赖。那么是必在实现接口的时候,类 B 就要有实现他不需要的方法 4 和方法 5 而类 D 就要实现他不需要的方法 2 和方法 3,这简直就是一个灾难的设计。所以我们需要对接口进行拆分,就是把接口分成满足依赖关系的最小接口,类 B 与类 D 不需要去实现与他们无关接口方法。比如在这个例子中,我们可以把接口拆成 3 个,第一个是仅仅由方法 1 的接口,第二个接口是包含 2,3 方法的,第三个接口是包含 4,5 方法的。这样,我们的设计就满足了接口隔离原则。 8 | 9 | 接口之所以存在,是为了解耦。开发者常常有一个错误的认知,以为是实现类需要接口。其实是消费者需要接口,实现类只是提供服务,因此应该由消费者(客户端)来定义接口。理解了这一点,才能正确地站在消费者的角度定义 Role interface,而不是从实现类中提取 Header Interface。 10 | 11 | # 案例:砖头 12 | 13 | 砖头(Brick)可以被建筑工人用来盖房子,也可以被用来正当防卫: 14 | 15 | ```java 16 | public class Brick { 17 | private int length; 18 | private int width; 19 | private int height; 20 | private int weight; 21 | 22 | public void build() { 23 | //...包工队盖房 24 | } 25 | 26 | public void defense() { 27 | //...正当防卫 28 | } 29 | } 30 | ``` 31 | 32 | 如果直接提取以下接口,这就是 Header Interface: 33 | 34 | ```java 35 | public interface BrickInterface { 36 | void buildHouse(); 37 | void defense(); 38 | } 39 | ``` 40 | 41 | 普通大众需要的是可以防卫的武器,并不需要用砖盖房子。当普通大众(Person)被迫依赖了自己不需要的接口方法时,就违反接口隔离原则。正确的做法是站在消费者的角度,抽象出 Role interface: 42 | 43 | ```java 44 | public interface BuildHouse { 45 | void build(); 46 | } 47 | 48 | public interface StrickCompetence { 49 | void defense(); 50 | } 51 | 52 | public class Brick implement BuildHouse, StrickCompetence { 53 | } 54 | ``` 55 | 56 | 有了 Role interface,作为消费者的普通大众和建筑工人就可以分别消费自己的接口: 57 | 58 | ```java 59 | // Worker.java 60 | brick.build(); 61 | 62 | // Person.java 63 | brick.strike(); 64 | ``` 65 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/02~结构型模式/组合.md: -------------------------------------------------------------------------------- 1 | # 组合 2 | 3 | 组合是一种结构型设计模式,你可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。 4 | 5 | # 案例:图形编辑器 6 | 7 | ```ts 8 | // 组件接口会声明组合中简单和复杂对象的通用操作。 9 | interface Graphic is 10 | method move(x, y) 11 | method draw() 12 | 13 | // 叶节点类代表组合的终端对象。叶节点对象中不能包含任何子对象。叶节点对象 14 | // 通常会完成实际的工作,组合对象则仅会将工作委派给自己的子部件。 15 | class Dot implements Graphic is 16 | field x, y 17 | 18 | constructor Dot(x, y) { ... } 19 | 20 | method move(x, y) is 21 | this.x += x, this.y += y 22 | 23 | method draw() is 24 | // 在坐标位置(X,Y)处绘制一个点。 25 | 26 | // 所有组件类都可以扩展其他组件。 27 | class Circle extends Dot is 28 | field radius 29 | 30 | constructor Circle(x, y, radius) { ... } 31 | 32 | method draw() is 33 | // 在坐标位置(X,Y)处绘制一个半径为 R 的圆。 34 | 35 | // 组合类表示可能包含子项目的复杂组件。组合对象通常会将实际工作委派给子项 36 | // 目,然后“汇总”结果。 37 | class CompoundGraphic implements Graphic is 38 | field children: array of Graphic 39 | 40 | // 组合对象可在其项目列表中添加或移除其他组件(简单的或复杂的皆可)。 41 | method add(child: Graphic) is 42 | // 在子项目数组中添加一个子项目。 43 | 44 | method remove(child: Graphic) is 45 | // 从子项目数组中移除一个子项目。 46 | 47 | method move(x, y) is 48 | foreach (child in children) do 49 | child.move(x, y) 50 | 51 | // 组合会以特定的方式执行其主要逻辑。它会递归遍历所有子项目,并收集和 52 | // 汇总其结果。由于组合的子项目也会将调用传递给自己的子项目,以此类推, 53 | // 最后组合将会完成整个对象树的遍历工作。 54 | method draw() is 55 | // 1. 对于每个子部件: 56 | // - 绘制该部件。 57 | // - 更新边框坐标。 58 | // 2. 根据边框坐标绘制一个虚线长方形。 59 | 60 | 61 | // 客户端代码会通过基础接口与所有组件进行交互。这样一来,客户端代码便可同 62 | // 时支持简单叶节点组件和复杂组件。 63 | class ImageEditor is 64 | method load() is 65 | all = new CompoundGraphic() 66 | all.add(new Dot(1, 2)) 67 | all.add(new Circle(5, 3, 10)) 68 | // ... 69 | 70 | // 将所需组件组合为复杂的组合组件。 71 | method groupSelected(components: array of Graphic) is 72 | group = new CompoundGraphic() 73 | group.add(components) 74 | all.remove(components) 75 | all.add(group) 76 | // 所有组件都将被绘制。 77 | all.draw() 78 | ``` 79 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/访问者/README.md: -------------------------------------------------------------------------------- 1 | # 访问者 2 | 3 | 访问者是一种行为设计模式,它能将算法与其所作用的对象隔离开来。 4 | 5 | # 案例:XML 文件导出 6 | 7 | ```ts 8 | // 元素接口声明了一个`accept`(接收)方法,它会将访问者基础接口作为一个参数。 9 | interface Shape is 10 | method move(x, y) 11 | method draw() 12 | method accept(v: Visitor) 13 | 14 | // 每个具体元素类都必须以特定方式实现`accept`方法,使其能调用相应元素类的访问者方法。 15 | class Dot extends Shape is 16 | // ... 17 | 18 | // 注意我们正在调用的`visitDot`(访问点)方法与当前类的名称相匹配。 19 | // 这样我们能让访问者知晓与其交互的元素类。 20 | method accept(v: Visitor) is 21 | v.visitDot(this) 22 | 23 | class Circle extends Dot is 24 | // ... 25 | method accept(v: Visitor) is 26 | v.visitCircle(this) 27 | 28 | class Rectangle extends Shape is 29 | // ... 30 | method accept(v: Visitor) is 31 | v.visitRectangle(this) 32 | 33 | class CompoundShape implements Shape is 34 | // ... 35 | method accept(v: Visitor) is 36 | v.visitCompoundShape(this) 37 | 38 | 39 | // 访问者接口声明了一组与元素类对应的访问方法。访问方法的签名能让访问者准 40 | // 确辨别出与其交互的元素所属的类。 41 | interface Visitor is 42 | method visitDot(d: Dot) 43 | method visitCircle(c: Circle) 44 | method visitRectangle(r: Rectangle) 45 | method visitCompoundShape(cs: CompoundShape) 46 | 47 | // 具体访问者实现了同一算法的多个版本,而且该算法能与所有具体类进行交互。 48 | // 49 | // 访问者模式在复杂对象结构(例如组合树)上使用时能发挥最大作用。在这种情 50 | // 况下,它可以存储算法的一些中间状态,并同时在结构中的不同对象上执行访问 51 | // 者方法。这可能会非常有帮助。 52 | class XMLExportVisitor implements Visitor is 53 | method visitDot(d: Dot) is 54 | // 导出点(dot)的 ID 和中心坐标。 55 | 56 | method visitCircle(c: Circle) is 57 | // 导出圆(circle)的 ID、中心坐标和半径。 58 | 59 | method visitRectangle(r: Rectangle) is 60 | // 导出长方形(rectangle)的 ID、左上角坐标、宽和长。 61 | 62 | method visitCompoundShape(cs: CompoundShape) is 63 | // 导出图形(shape)的 ID 和其子项目的 ID 列表。 64 | 65 | 66 | // 客户端代码可在不知晓具体类的情况下在一组元素上运行访问者操作。“接收”操 67 | // 作会将调用定位到访问者对象的相应操作上。 68 | class Application is 69 | field allShapes: array of Shapes 70 | 71 | method export() is 72 | exportVisitor = new XMLExportVisitor() 73 | 74 | foreach (shape in allShapes) do 75 | shape.accept(exportVisitor) 76 | ``` 77 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/中介者.md: -------------------------------------------------------------------------------- 1 | # 中介者 2 | 3 | 中介者是一种行为设计模式,能让你减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。 4 | 5 | # 案例:UI 组件通信 6 | 7 | ```ts 8 | // 中介者接口声明了一个能让组件将各种事件通知给中介者的方法。中介者可对这 9 | // 些事件做出响应并将执行工作传递给其他组件。 10 | interface Mediator is 11 | method notify(sender: Component, event: string) 12 | 13 | 14 | // 具体中介者类,可解开各个组件之间相互交叉的连接关系,并将其转移到中介者 15 | // 中。 16 | class AuthenticationDialog implements Mediator is 17 | private field title: string 18 | private field loginOrRegisterChkBx: Checkbox 19 | private field loginUsername, loginPassword: Textbox 20 | private field registrationUsername, registrationPassword 21 | private field registrationEmail: Textbox 22 | private field okBtn, cancelBtn: Button 23 | 24 | constructor AuthenticationDialog() is 25 | // 创建所有组件对象并将当前中介者传递给其构造方法以建立连接。 26 | 27 | // 当组件中有事件发生时,它会通知中介者。中介者接收到通知后可自行处理, 28 | // 也可将请求传递给另一个组件。 29 | method notify(sender, event) is 30 | if (sender == loginOrRegisterChkBx and event == "check") 31 | if (loginOrRegisterChkBx.checked) 32 | title = "登录" 33 | // 1. 显示登录表单组件。 34 | // 2. 隐藏注册表单组件。 35 | else 36 | title = "注册" 37 | // 1. 显示注册表单组件。 38 | // 2. 隐藏登录表单组件。 39 | 40 | if (sender == okBtn && event == "click") 41 | if (loginOrRegister.checked) 42 | // 尝试找到使用登录信息的用户。 43 | if (!found) 44 | // 在登录字段上方显示错误信息。 45 | else 46 | // 1. 使用注册字段中的数据创建用户账号。 47 | // 2. 完成用户登录工作。… 48 | 49 | 50 | // 组件会使用中介者接口与中介者进行交互。因此只需将它们与不同的中介者连接 51 | // 起来,你就能在其他情境中使用这些组件了。 52 | class Component is 53 | field dialog: Mediator 54 | 55 | constructor Component(dialog) is 56 | this.dialog = dialog 57 | 58 | method click() is 59 | dialog.notify(this, "click") 60 | 61 | method keypress() is 62 | dialog.notify(this, "keypress") 63 | 64 | // 具体组件之间无法进行交流。它们只有一个交流渠道,那就是向中介者发送通知。 65 | class Button extends Component is 66 | // ... 67 | 68 | class Textbox extends Component is 69 | // ... 70 | 71 | class Checkbox extends Component is 72 | method check() is 73 | dialog.notify(this, "check") 74 | // ... 75 | ``` 76 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/职责链.md: -------------------------------------------------------------------------------- 1 | # 职责链 2 | 3 | 职责链是一种行为设计模式,允许你将请求沿着处理者链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。 4 | 5 | # 案例:GUI 渲染 6 | 7 | ```ts 8 | // 处理者接口声明了一个创建处理者链的方法。还声明了一个执行请求的方法。 9 | interface ComponentWithContextualHelp is 10 | method showHelp() 11 | 12 | 13 | // 简单组件的基础类。 14 | abstract class Component implements ComponentWithContextualHelp is 15 | field tooltipText: string 16 | 17 | // 组件容器在处理者链中作为“下一个”链接。 18 | protected field container: Container 19 | 20 | // 如果组件设定了帮助文字,那它将会显示提示信息。如果组件没有帮助文字 21 | // 且其容器存在,那它会将调用传递给容器。 22 | method showHelp() is 23 | if (tooltipText != null) 24 | // 显示提示信息。 25 | else 26 | container.showHelp() 27 | 28 | 29 | // 容器可以将简单组件和其他容器作为其子项目。链关系将在这里建立。该类将从 30 | // 其父类处继承 showHelp(显示帮助)的行为。 31 | abstract class Container extends Component is 32 | protected field children: array of Component 33 | 34 | method add(child) is 35 | children.add(child) 36 | child.container = this 37 | 38 | 39 | // 原始组件应该能够使用帮助操作的默认实现... 40 | class Button extends Component is 41 | // ... 42 | 43 | // 但复杂组件可能会对默认实现进行重写。如果无法以新的方式来提供帮助文字, 44 | // 那组件总是还能调用基础实现的(参见 Component 类)。 45 | class Panel extends Container is 46 | field modalHelpText: string 47 | 48 | method showHelp() is 49 | if (modalHelpText != null) 50 | // 显示包含帮助文字的模态窗口。 51 | else 52 | super.showHelp() 53 | 54 | // ...同上... 55 | class Dialog extends Container is 56 | field wikiPageURL: string 57 | 58 | method showHelp() is 59 | if (wikiPageURL != null) 60 | // 打开百科帮助页面。 61 | else 62 | super.showHelp() 63 | 64 | 65 | // 客户端代码。 66 | class Application is 67 | // 每个程序都能以不同方式对链进行配置。 68 | method createUI() is 69 | dialog = new Dialog("预算报告") 70 | dialog.wikiPageURL = "http://..." 71 | panel = new Panel(0, 0, 400, 800) 72 | panel.modalHelpText = "本面板用于..." 73 | ok = new Button(250, 760, 50, 20, "确认") 74 | ok.tooltipText = "这是一个确认按钮..." 75 | cancel = new Button(320, 760, 50, 20, "取消") 76 | // ... 77 | panel.add(ok) 78 | panel.add(cancel) 79 | dialog.add(panel) 80 | 81 | // 想象这里会发生什么。 82 | method onF1KeyPress() is 83 | component = this.getComponentAtMouseCoords() 84 | component.showHelp() 85 | ``` 86 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/00~SOLID/依赖倒置.md: -------------------------------------------------------------------------------- 1 | # 依赖倒置 2 | 3 | > High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions. 4 | 5 | 高层模块不应该依赖于低层模块,它们应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。应该面向接口编程,不应该面向实现类编程。面向实现类编程,相当于就是论事,那是正向依赖(正常人思维);面向接口编程,相当于通过事物表象来看本质,那是反向依赖,即依赖倒置(程序员思维)。并不是说,所有的类都要有一个对应的接口,而是说,如果有接口,那就尽量使用接口来编程吧。 6 | 7 | # 依赖注入与控制反转 8 | 9 |  10 | 11 | Martin Fowler 在 2004 年发表的 [Inversion of Control Containers and the Dependency Injection pattern](https://martinfowler.com/articles/injection.html) 一文中阐述了 IoC 的概念与实践模式。 12 | 13 | 在传统的层次化模式(Layers Pattern)中,高层次的组件(Higher Level)调用低层次(Lower Level)的组件来逐步构建复杂的系统;不过这种方式会导致组件之间存在较强的耦合,对于低层次组件的强依赖往往也会限制了高层次组件的可扩展性与重用性。 14 | 15 | 通常我们在没有依赖注入的时候如果 A 依赖于 B,那么在 A 初始化或者执行中的某个过程需要先创建 B,这时我们就认为 A 对 B 的依赖是正向的。但是这样解决依赖的办法会得得 A 与 B 的逻辑耦合在一起,依赖越来越多代码就会变的越来越糟糕。如下图所示,齿轮之间是相互依赖的,一损俱损。控制反转(IOC)模式就是要解决这个问题,它会多引入一个容器(Container)的概念,让一个 IOC 容器去管理 A、B 的依赖并初始化。当我们去掉容器时,剩下的齿轮成了一个个独立的功能模块。 16 | 17 |  18 | 19 | ## IoC 20 | 21 | IoC(Inversion of Control),即控制反转;在开发中,IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。 22 | 23 | - 谁控制谁,控制什么:在传统的程序设计中,我们直接在对象内部通过 new 的方式创建对象,是程序主动创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?主要是控制外部资源获取。 24 | 25 | - 为何是反转了,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转了;哪些方面反转了?依赖对象的获取被反转了。 26 | 27 | IoC 不是一种技术,只是一种思想,一个重要的面向对象编程法则,它能指导我们如何设计松耦合、更优良的系统。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是松散耦合,这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。 28 | 29 | ## DI 30 | 31 | DI - Dependency Injection,即"依赖注入":组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 32 | 33 | 理解 DI 的关键是:"谁依赖了谁,为什么需要依赖,谁注入了谁,注入了什么",那我们来深入分析一下: 34 | 35 | - 谁依赖了谁:当然是应用程序依赖 IoC 容器 36 | - 为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源 37 | - 谁注入谁:很明显是 IoC 容器注入应用程序依赖的对象 38 | - 注入了什么:注入某个对象所需的外部资源(包括对象、资源、常量数据) 39 | 40 | IoC 和 DI 其实它们是同一个概念的不同角度描述,由于控制反转的概念比较含糊,所以 2004 年 Martin Fowler 又给出了一个新的名字:"依赖注入",相对 IoC 而言,"依赖注入" 明确描述了被注入对象依赖 IoC 容器配置依赖对象。 41 | 42 | 总的来说,控制反转(Inversion of Control)是说创建对象的控制权发生转移,以前创建对象的主动权和创建时机由应用程序把控,而现在这种权利转交给 IoC 容器,它就是一个专门用来创建对象的工厂,你需要什么对象,它就给你什么对象。有了 IoC 容器,依赖关系就改变了,原先的依赖关系就没了,它们都依赖 IoC 容器了,通过 IoC 容器来建立它们之间的关系。 43 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/01~创建型模式/单例.md: -------------------------------------------------------------------------------- 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 | - 将默认构造函数设为私有,防止其他对象使用单例类的 new 运算符。 29 | 30 | - 新建一个静态创建方法作为构造函数。该函数会“偷偷”调用私有构造函数创建一个对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。 31 | 32 | 即在类中添加一个私有静态成员变量用于保存单例实例,然后声明一个公有静态创建方法用于获取单例实例。在静态方法中实现"延迟初始化"。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。然后将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其他对象不能调用;检查客户端代码,将对单例的构造函数调用替换为对其静态创建方法调用。 33 | 34 | # 案例:数据库连接 35 | 36 | ```ts 37 | // 数据库类会对`getInstance`(获取实例)方法进行定义以让客户端在程序各处 38 | // 都能访问相同的数据库连接实例。 39 | class Database is 40 | // 保存单例实例的成员变量必须被声明为静态类型。 41 | private static field instance: Database 42 | 43 | // 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构 44 | // 造方法。 45 | private constructor Database() is 46 | // 部分初始化代码(例如到数据库服务器的实际连接)。 47 | // ... 48 | 49 | // 用于控制对单例实例的访问权限的静态方法。 50 | public static method getInstance() is 51 | if (Database.instance == null) then 52 | acquireThreadLock() and then 53 | // 确保在该线程等待解锁时,其他线程没有初始化该实例。 54 | if (Database.instance == null) then 55 | Database.instance = new Database() 56 | return Database.instance 57 | 58 | // 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。 59 | public method query(sql) is 60 | // 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以 61 | // 在这里添加限流或缓冲逻辑。 62 | // ... 63 | 64 | class Application is 65 | method main() is 66 | Database foo = Database.getInstance() 67 | foo.query("SELECT ...") 68 | // ... 69 | Database bar = Database.getInstance() 70 | bar.query("SELECT ...") 71 | // 变量 `bar` 和 `foo` 中将包含同一个的对象。 72 | ``` 73 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/观察者.md: -------------------------------------------------------------------------------- 1 | # 观察者 2 | 3 | 观察者是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其他对象。 4 | 5 | # 案例:编辑器通信 6 | 7 | ```ts 8 | // 发布者基类包含订阅管理代码和通知方法。 9 | class EventManager is 10 | private field listeners: hash map of event types and listeners 11 | 12 | method subscribe(eventType, listener) is 13 | listeners.add(eventType, listener) 14 | 15 | method unsubscribe(eventType, listener) is 16 | listeners.remove(eventType, listener) 17 | 18 | method notify(eventType, data) is 19 | foreach (listener in listeners.of(eventType)) do 20 | listener.update(data) 21 | 22 | // 具体发布者包含一些订阅者感兴趣的实际业务逻辑。我们可以从发布者基类中扩 23 | // 展出该类,但在实际情况下并不总能做到,因为具体发布者可能已经是子类了。 24 | // 在这种情况下,你可用组合来修补订阅逻辑,就像我们在这里做的一样。 25 | class Editor is 26 | public field events: EventManager 27 | private field file: File 28 | 29 | constructor Editor() is 30 | events = new EventManager() 31 | 32 | // 业务逻辑的方法可将变化通知给订阅者。 33 | method openFile(path) is 34 | this.file = new File(path) 35 | events.notify("open", file.name) 36 | 37 | method saveFile() is 38 | file.write() 39 | events.notify("save", file.name) 40 | 41 | // ... 42 | 43 | 44 | // 这里是订阅者接口。如果你的编程语言支持函数类型,则可用一组函数来代替整 45 | // 个订阅者的层次结构。 46 | interface EventListener is 47 | method update(filename) 48 | 49 | // 具体订阅者会对其注册的发布者所发出的更新消息做出响应。 50 | class LoggingListener implements EventListener is 51 | private field log: File 52 | private field message 53 | 54 | constructor LoggingListener(log_filename, message) is 55 | this.log = new File(log_filename) 56 | this.message = message 57 | 58 | method update(filename) is 59 | log.write(replace('%s',filename,message)) 60 | 61 | class EmailAlertsListener implements EventListener is 62 | private field email: string 63 | 64 | constructor EmailAlertsListener(email, message) is 65 | this.email = email 66 | this.message = message 67 | 68 | method update(filename) is 69 | system.email(email, replace('%s',filename,message)) 70 | 71 | 72 | // 应用程序可在运行时配置发布者和订阅者。 73 | class Application is 74 | method config() is 75 | editor = new TextEditor() 76 | 77 | logger = new LoggingListener( 78 | "/path/to/log.txt", 79 | "有人打开了文件:%s"); 80 | editor.events.subscribe("open", logger) 81 | 82 | emailAlerts = new EmailAlertsListener( 83 | "admin@example.com", 84 | "有人更改了文件:%s") 85 | editor.events.subscribe("save", emailAlerts) 86 | ``` 87 | -------------------------------------------------------------------------------- /01~编程范式/基础范式/声明式编程.md: -------------------------------------------------------------------------------- 1 | # 声明式编程 2 | 3 | 声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子,例如: 4 | 5 | ```sql 6 | SELECT * FROM collection WHERE num > 5 7 | ``` 8 | 9 | 通过观察声明式编程的代码我们可以发现它有一个特点是它不需要创建变量用来存储数据。另一个特点是它不包含循环控制的代码如 for,while。函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。 10 | 11 | # Web 上的声明式查询 12 | 13 | 除了 SQL,网页编程中用到的 HTML 和 CSS 也都属于声明式编程。假设你有一个关于海洋动物的网站。用户当前正在查看鲨鱼页面,因此你将当前所选的导航项目“鲨鱼”标记为当前选中项目。 14 | 15 | ```html 16 |
Sharks
19 |Whales
27 |
12 |
13 |
14 |
15 |
16 |
19 | SOLID、经典设计模式、函数式编程
20 |
21 | 在线阅读 >>
22 |
23 |
24 | 速览手册
25 | ·
26 | Report Bug
27 | ·
28 | 参考资料
29 |
40 |
45 |
46 |
47 |
64 |
97 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
143 |
144 |
145 |
146 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/01~编程范式/函数式编程/术语概念.md:
--------------------------------------------------------------------------------
1 | # Functional Programming
2 |
3 | 越靠下表示背离传统命令式语言更远
4 | 
5 |
6 | # Functional Programming(函数式编程)
7 |
8 | > [函数式编程初探——阮一峰](http://www.ruanyifeng.com/blog/2012/04/functional_programming.html) > > [函数式编程](http://www.nowamagic.net/academy/detail/1220525)
9 |
10 | 诞生 50 多年之后,[函数式编程](http://en.wikipedia.org/wiki/Functional_programming)(functional programming)开始获得越来越多的关注。不仅最古老的函数式语言 Lisp 重获青春,而且新的函数式语言层出不穷,比如 Erlang、clojure、Scala、F#等等。目前最当红的 Python、Ruby、Javascript、Java,对函数式编程的支持都很强,就连老牌的面向对象的 Java、面向过程的 PHP,都忙不迭地加入对匿名函数的支持。越来越多的迹象表明,函数式编程已经不再是学术界的最爱,开始大踏步地在业界投入实用。也许继"面向对象编程"之后,"函数式编程"会成为下一个编程的主流范式(paradigm)。未来的程序员恐怕或多或少都必须懂一点。
11 |
12 | ## Terms
13 |
14 | ### Side Effects
15 |
16 | > - [Wiki-Side_effect](
7 |
8 | ## 🍎 译序
9 |
10 | 本文是一篇手把手的函数式编程入门介绍,借助代码示例讲解细腻。但又不乏洞见,第一节中列举和点评了函数式种种让眼花缭乱的特质,给出了『理解函数式特质的指南针:函数式代码的核心特质就一条,**无副作用**』,相信这个指南针对于有积极学过挖过函数式的同学看来更是有相知恨晚的感觉。
11 |
12 | 希望看了这篇文章之后,能在学习和使用函数式编程的旅途中不迷路哦,兄 die ~
13 |
14 | PS:本人是在《[Functional Programming, Simplified(Scala edition)](https://alvinalexander.com/scala/functional-programming-simplified-book)》了解到本文。这本书由浅入深循序渐进地对`FP`做了体系讲解,力荐。
15 | 翻译中肯定会有不足和不对之处,欢迎建议([提交 Issue](https://github.com/oldratlee/translations/issues))和指正([Fork 后提交代码](https://github.com/oldratlee/translations/fork))!
16 |
17 | # 手把手介绍函数式编程:从命令式重构到函数式
18 |
19 | 有很多函数式编程文章讲解了抽象的函数式技术,也就是组合(`composition`)、管道(`pipelining`)、高阶函数(`higher order function`)。本文希望以另辟蹊径的方式来讲解函数式:首先展示我们平常编写的命令式而非函数式的代码示例,然后将这些示例重构成函数式风格。
20 |
21 | 本文的第一部分选用了简短的数据转换循环,将它们重构成函数式的`map`和`reduce`。第二部分则对更长的循环代码,将它们分解成多个单元,然后重构各个单元成函数式的。第三部分选用的是有一系列连续的数据转换循环代码,将其拆解成为一个函数式管道(`functional pipeline`)。
22 |
23 | 示例代码用的是`Python`语言,因为多数人都觉得`Python`易于阅读。示例代码避免使用`Python`范的(`pythonic`)代码,以便展示出对各种语言通用的函数式技术:`map`、`reduce`和管道。所有示例都用的是`Python 2`。
24 |
25 | ---
26 |
27 |
28 |
29 |
30 | - [理解函数式特质的指南针](#%E7%90%86%E8%A7%A3%E5%87%BD%E6%95%B0%E5%BC%8F%E7%89%B9%E8%B4%A8%E7%9A%84%E6%8C%87%E5%8D%97%E9%92%88)
31 | - [不要迭代列表,使用`map`和`reduce`](#%E4%B8%8D%E8%A6%81%E8%BF%AD%E4%BB%A3%E5%88%97%E8%A1%A8%E4%BD%BF%E7%94%A8map%E5%92%8Creduce)
32 | - [`map`](#map)
33 | - [`reduce`](#reduce)
34 | - [编写声明式代码,而非命令式](#%E7%BC%96%E5%86%99%E5%A3%B0%E6%98%8E%E5%BC%8F%E4%BB%A3%E7%A0%81%E8%80%8C%E9%9D%9E%E5%91%BD%E4%BB%A4%E5%BC%8F)
35 | - [使用函数](#%E4%BD%BF%E7%94%A8%E5%87%BD%E6%95%B0)
36 | - [消除状态](#%E6%B6%88%E9%99%A4%E7%8A%B6%E6%80%81)
37 | - [使用管道](#%E4%BD%BF%E7%94%A8%E7%AE%A1%E9%81%93)
38 | - [现在开始我们可以做什么?](#%E7%8E%B0%E5%9C%A8%E5%BC%80%E5%A7%8B%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E5%81%9A%E4%BB%80%E4%B9%88)
39 |
40 |
41 |
42 | ---
43 |
44 |
45 |
46 | # 理解函数式特质的指南针
47 |
48 | 当人们谈论函数式编程时,提到了多到令人迷路的『函数式』特质(`characteristics`):
49 |
50 | - 人们会提到不可变数据[1](#note1)(`immutable data`)、一等公民的函数[2](#note2)(`first class function`)和尾调用优化[3](#note3)(`tail call optimisation`)。这些是**有助于函数式编程的语言特性**。
51 | - 人们也会提到`map`、`reduce`、管道、递归(`recursing`)、柯里化[4](#note4)(`currying`)以及高阶函数的使用。这些是**用于编写函数式代码的编程技术**。
52 | - 人们还会提到并行化[5](#note5)(`parallelization`)、惰性求值[6](#note6)(`lazy evaluation`)和确定性[7](#note7)(`determinism`)。这些是**函数式程序的优点**。
53 |
54 | 无视这一切。函数式代码的核心特质就一条:**无副作用**(`the absence of side effects`)。即代码逻辑不依赖于当前函数之外的数据,并且也不会更改当前函数之外的数据。所有其他的『函数式』特质都可以从这一条派生出来。在你学习过程中,请以此作为指南针。不要再迷路哦,兄 die ~
55 |
56 | 这是一个非函数式的函数:
57 |
58 | ```python
59 | a = 0
60 | def increment():
61 | global a
62 | a += 1
63 | ```
64 |
65 | 而这是一个函数式的函数:
66 |
67 | ```python
68 | def increment(a):
69 | return a + 1
70 | ```
71 |
72 | # 不要迭代列表,使用`map`和`reduce`
73 |
74 | ## `map`
75 |
76 | `map`输入一个函数和一个集合,创建一个新的空集合,在原来集合的每个元素上运行该函数,并将各个返回值插入到新集合中,然后返回新的集合。
77 |
78 | 这是一个简单的`map`,它接受一个名字列表并返回这些名字的长度列表:
79 |
80 | ```python
81 | name_lengths = map(len, ["Mary", "Isla", "Sam"])
82 | print name_lengths
83 | # => [4, 4, 3]
84 | ```
85 |
86 | 这是一个`map`,对传递的集合中的每个数字进行平方:
87 |
88 | ```python
89 | squares = map(lambda x: x * x, [0, 1, 2, 3, 4])
90 |
91 | print squares
92 | # => [0, 1, 4, 9, 16]
93 | ```
94 |
95 | 这个`map`没有输入命名函数,而是一个匿名的内联函数,用`lambda`关键字来定义。`lambda`的参数定义在冒号的左侧。函数体定义在冒号的右侧。(隐式)返回的是函数体的运行结果。
96 |
97 | 下面的非函数式代码输入一个真实名字的列表,替换成随机分配的代号。
98 |
99 | ```python
100 | import random
101 |
102 | names = ['Mary', 'Isla', 'Sam']
103 | code_names = ['Mr. Pink', 'Mr. Orange', 'Mr. Blonde']
104 |
105 | for i in range(len(names)):
106 | names[i] = random.choice(code_names)
107 |
108 | print names
109 | # => ['Mr. Blonde', 'Mr. Blonde', 'Mr. Blonde']
110 | ```
111 |
112 | (如你所见,这个算法可能会为多个秘密特工分配相同的秘密代号,希望这不会因此导致混淆了秘密任务。)
113 |
114 | 可以用`map`重写成:
115 |
116 | ```python
117 | import random
118 |
119 | names = ['Mary', 'Isla', 'Sam']
120 |
121 | secret_names = map(lambda x: random.choice(['Mr. Pink',
122 | 'Mr. Orange',
123 | 'Mr. Blonde']),
124 | names)
125 | ```
126 |
127 | **练习 1**:尝试将下面的代码重写为`map`,输入一个真实名字列表,替换成用更可靠策略生成的代号。
128 |
129 | ```python
130 | names = ['Mary', 'Isla', 'Sam']
131 |
132 | for i in range(len(names)):
133 | names[i] = hash(names[i])
134 |
135 | print names
136 | # => [6306819796133686941, 8135353348168144921, -1228887169324443034]
137 | ```
138 |
139 | (希望特工会留下美好的回忆,在秘密任务期间能记得住搭档的秘密代号。)
140 |
141 | 我的实现方案:
142 |
143 | ```python
144 | names = ['Mary', 'Isla', 'Sam']
145 |
146 | secret_names = map(hash, names)
147 | ```
148 |
149 | ## `reduce`
150 |
151 | `reduce`输入一个函数和一个集合,返回通过合并集合元素所创建的值。
152 |
153 | 这是一个简单的`reduce`,返回集合所有元素的总和。
154 |
155 | ```python
156 | sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4])
157 |
158 | print sum
159 | # => 10
160 | ```
161 |
162 | `x`是迭代的当前元素。`a`是累加器(`accumulator`),它是在前一个元素上执行`lambda`的返回值。`reduce()`遍历所有集合元素。对于每一个元素,运行以当前的`a`和`x`为参数运行`lambda`,返回结果作为下一次迭代的`a`。
163 |
164 | 在第一次迭代时,`a`是什么值?并没有前一个的迭代结果可以传递。`reduce()`使用集合中的第一个元素作为第一次迭代中的`a`值,从集合的第二个元素开始迭代。也就是说,第一个`x`是集合的第二个元素。
165 |
166 | 下面的代码计算单词`'Sam'`在字符串列表中出现的次数:
167 |
168 | ```python
169 | sentences = ['Mary read a story to Sam and Isla.',
170 | 'Isla cuddled Sam.',
171 | 'Sam chortled.']
172 |
173 | sam_count = 0
174 | for sentence in sentences:
175 | sam_count += sentence.count('Sam')
176 |
177 | print sam_count
178 | # => 3
179 | ```
180 |
181 | 这与下面使用`reduce`的代码相同:
182 |
183 | ```python
184 | sentences = ['Mary read a story to Sam and Isla.',
185 | 'Isla cuddled Sam.',
186 | 'Sam chortled.']
187 |
188 | sam_count = reduce(lambda a, x: a + x.count('Sam'),
189 | sentences,
190 | 0)
191 | ```
192 |
193 | 这段代码是如何产生初始的`a`值?`'Sam'`出现次数的初始值不能是`'Mary read a story to Sam and Isla.'`。初始累加器用`reduce()`的第三个参数指定。这样就允许使用与集合元素不同类型的值。
194 |
195 | 为什么`map`和`reduce`更好?
196 |
197 | 1. 这样的做法通常会是一行简洁的代码。
198 | 1. 迭代的重要部分 —— 集合、操作和返回值 —— 以`map`和`reduce`方式总是在相同的位置。
199 | 1. 循环中的代码可能会影响在它之前定义的变量或在它之后运行的代码。按照约定,`map`和`reduce`都是函数式的。
200 | 1. `map`和`reduce`是基本原子操作。
201 | - 阅读`for`循环时,必须一行一行地才能理解整体逻辑。往往没有什么规则能保证以一个固定结构来明确代码的表义。
202 | - 相比之下,`map`和`reduce`则是一目了然表现出了可以组合出复杂算法的构建块(`building block`)及其相关的元素,代码阅读者可以迅速理解并抓住整体脉络。『哦~这段代码正在转换每个集合元素;丢弃了一些转换结果;然后将剩下的元素合并成单个输出结果。』
203 | 1. `map`和`reduce`有很多朋友,提供有用的、对基本行为微整的版本。比如:`filter`、`all`、`any`和`find`。
204 |
205 | **练习 2**:尝试使用`map`、`reduce`和`filter`重写下面的代码。`filter`需要一个函数和一个集合,返回结果是函数返回`True`的所有集合元素。
206 |
207 | ```python
208 | people = [{'name': 'Mary', 'height': 160},
209 | {'name': 'Isla', 'height': 80},
210 | {'name': 'Sam'}]
211 |
212 | height_total = 0
213 | height_count = 0
214 | for person in people:
215 | if 'height' in person:
216 | height_total += person['height']
217 | height_count += 1
218 |
219 | if height_count > 0:
220 | average_height = height_total / height_count
221 |
222 | print average_height
223 | # => 120
224 | ```
225 |
226 | 如果上面这段代码看起来有些烧脑,我们试试不以在数据上操作为中心的思考方式。而是想一想数据所经历的状态:从人字典的列表转换成平均身高。不要将多个转换混在一起。每个转换放在一个单独的行上,并将结果分配一个有描述性命名的变量。代码工作之后,再合并缩减代码。
227 |
228 | 我的实现方案:
229 |
230 | ```python
231 | people = [{'name': 'Mary', 'height': 160},
232 | {'name': 'Isla', 'height': 80},
233 | {'name': 'Sam'}]
234 |
235 | heights = map(lambda x: x['height'],
236 | filter(lambda x: 'height' in x, people))
237 |
238 | if len(heights) > 0:
239 | from operator import add
240 | average_height = reduce(add, heights) / len(heights)
241 | ```
242 |
243 | # 编写声明式代码,而非命令式
244 |
245 | 下面的程序演示三辆赛车的比赛。每过一段时间,赛车可能向前跑了,也可能抛锚而原地不动。在每个时间段,程序打印出目前为止的赛车路径。五个时间段后比赛结束。
246 |
247 | 这是个示例输出:
248 |
249 | ```
250 | -
251 | --
252 | --
253 |
254 | --
255 | --
256 | ---
257 |
258 | ---
259 | --
260 | ---
261 |
262 | ----
263 | ---
264 | ----
265 |
266 | ----
267 | ----
268 | -----
269 | ```
270 |
271 | 这是程序实现:
272 |
273 | ```python
274 | from random import random
275 |
276 | time = 5
277 | car_positions = [1, 1, 1]
278 |
279 | while time:
280 | # decrease time
281 | time -= 1
282 |
283 | print ''
284 | for i in range(len(car_positions)):
285 | # move car
286 | if random() > 0.3:
287 | car_positions[i] += 1
288 |
289 | # draw car
290 | print '-' * car_positions[i]
291 | ```
292 |
293 | 这份代码是命令式的。函数式版本则是声明式的,描述要做什么,而不是如何做。
294 |
295 | ## 使用函数
296 |
297 | 通过将代码片段打包到函数中,程序可以更加声明式。
298 |
299 | ```python
300 | from random import random
301 |
302 | def move_cars():
303 | for i, _ in enumerate(car_positions):
304 | if random() > 0.3:
305 | car_positions[i] += 1
306 |
307 | def draw_car(car_position):
308 | print '-' * car_position
309 |
310 | def run_step_of_race():
311 | global time
312 | time -= 1
313 | move_cars()
314 |
315 | def draw():
316 | print ''
317 | for car_position in car_positions:
318 | draw_car(car_position)
319 |
320 | time = 5
321 | car_positions = [1, 1, 1]
322 |
323 | while time:
324 | run_step_of_race()
325 | draw()
326 | ```
327 |
328 | 要理解这个程序,读者只需读一下主循环。『如果还剩下时间,请跑一步,然后画出线图。再次检查时间。』如果读者想要了解更多关于比赛步骤或画图的含义,可以阅读对应函数的代码。
329 |
330 | 没什么要再说明的了。**代码是自描述的。**
331 |
332 | 拆分代码成函数是一种很好的、简单易行的方法,能使代码更具可读性。
333 |
334 | 这个技术使用函数,但将函数用作子例程(`sub-routine`),用于打包代码。对照上文说的指南针,这样的代码并不是函数式的。实现中的函数使用了没有作为参数传递的状态,即通过更改外部变量而不是返回值来影响函数周围的代码。要确认函数真正做了什么,读者必须仔细阅读每一行。如果找到一个外部变量,必须反查它的源头,并检查其他哪些函数更改了这个变量。
335 |
336 | ## 消除状态
337 |
338 | 下面是赛车代码的函数式版本:
339 |
340 | ```python
341 | from random import random
342 |
343 | def move_cars(car_positions):
344 | return map(lambda x: x + 1 if random() > 0.3 else x,
345 | car_positions)
346 |
347 | def output_car(car_position):
348 | return '-' * car_position
349 |
350 | def run_step_of_race(state):
351 | return {'time': state['time'] - 1,
352 | 'car_positions': move_cars(state['car_positions'])}
353 |
354 | def draw(state):
355 | print ''
356 | print '\n'.join(map(output_car, state['car_positions']))
357 |
358 | def race(state):
359 | draw(state)
360 | if state['time']:
361 | race(run_step_of_race(state))
362 |
363 | race({'time': 5,
364 | 'car_positions': [1, 1, 1]})
365 | ```
366 |
367 | 代码仍然是分解成函数。但这些函数是函数式的,有三个迹象表明这点:
368 |
369 | 1. 不再有任何共享变量。`time`与`car_position`作为参数传入`race()`。
370 | 1. 函数是有参数的。
371 | 1. 在函数内部没有变量实例化。所有数据更改都使用返回值完成。基于`run_step_of_race()`的结果,`race()`做递归调用[3](#note3)。每当一个步骤生成一个新状态时,立即传递到下一步。
372 |
373 | 让我们另外再来看看这么两个函数,`zero()`和`one()`:
374 |
375 | ```python
376 | def zero(s):
377 | if s[0] == "0":
378 | return s[1:]
379 |
380 | def one(s):
381 | if s[0] == "1":
382 | return s[1:]
383 | ```
384 |
385 | `zero()`输入一个字符串`s`。如果第一个字符是`'0'`,则返回字符串的其余部分。如果不是,则返回`None`,`Python`函数的默认返回值。`one()`做同样的事情,但关注的是第一个字符`'1'`。
386 |
387 | 假设有一个叫做`rule_sequence()`的函数,输入一个字符串和规则函数的列表,比如`zero()`和`one()`:
388 |
389 | 1. 调用字符串上的第一个规则。
390 | 1. 除非`None`返回,否则它将获取返回值并在其上调用第二个规则。
391 | 1. 除非`None`返回,否则它将获取返回值并在其上调用第三个规则。
392 | 1. 等等。
393 | 1. 如果任何规则返回`None`,则`rule_sequence()`停止并返回`None`。
394 | 1. 否则,它返回最终规则的返回值。
395 |
396 | 下面是一些示例输入和输出:
397 |
398 | ```python
399 | print rule_sequence('0101', [zero, one, zero])
400 | # => 1
401 |
402 | print rule_sequence('0101', [zero, zero])
403 | # => None
404 | ```
405 |
406 | 这是命令式版本的`rule_sequence()`实现:
407 |
408 | ```python
409 | def rule_sequence(s, rules):
410 | for rule in rules:
411 | s = rule(s)
412 | if s == None:
413 | break
414 |
415 | return s
416 | ```
417 |
418 | **练习 3**:上面的代码使用循环来实现。通过重写为递归来使代码更加声明式。
419 |
420 | 我的实现方案:
421 |
422 | ```python
423 | def rule_sequence(s, rules):
424 | if s == None or not rules:
425 | return s
426 | else:
427 | return rule_sequence(rules[0](s), rules[1:])
428 | ```
429 |
430 | # 使用管道
431 |
432 | 在上一节中,我们重写一些命令性循环成为调用辅助函数的递归。在本节中,将使用称为管道的技术重写另一类型的命令循环。
433 |
434 | 下面的循环对乐队字典执行转换,字典包含了乐队名、错误的所属国家和活跃状态。
435 |
436 | ```python
437 | bands = [{'name': 'sunset rubdown', 'country': 'UK', 'active': False},
438 | {'name': 'women', 'country': 'Germany', 'active': False},
439 | {'name': 'a silver mt. zion', 'country': 'Spain', 'active': True}]
440 |
441 | def format_bands(bands):
442 | for band in bands:
443 | band['country'] = 'Canada'
444 | band['name'] = band['name'].replace('.', '')
445 | band['name'] = band['name'].title()
446 |
447 | format_bands(bands)
448 |
449 | print bands
450 | # => [{'name': 'Sunset Rubdown', 'active': False, 'country': 'Canada'},
451 | # {'name': 'Women', 'active': False, 'country': 'Canada' },
452 | # {'name': 'A Silver Mt Zion', 'active': True, 'country': 'Canada'}]
453 | ```
454 |
455 | 看到这样的函数命名让人感受到一丝的忧虑,命名中的`format`表义非常模糊。仔细检查代码后,忧虑逆流成河。在循环的实现中做了三件事:
456 |
457 | 1. `'country'`键的值设置成了`'Canada'`。
458 | 1. 删除了乐队名中的标点符号。
459 | 1. 乐队名改成首字母大写。
460 |
461 | 我们很难看出这段代码意图是什么,也很难看出这段代码是否完成了它看起来要做的事情。代码难以重用、难以测试且难以并行化。
462 |
463 | 与下面实现对比一下:
464 |
465 | ```python
466 | print pipeline_each(bands, [set_canada_as_country,
467 | strip_punctuation_from_name,
468 | capitalize_names])
469 | ```
470 |
471 | 这段代码很容易理解。给人的印象是辅助函数是函数式的,因为它们看过来是串联在一起的。前一个函数的输出成为下一个的输入。如果是函数式的,就很容易验证。也易于重用、易于测试且易于并行化。
472 |
473 | `pipeline_each()`的功能就是将乐队一次一个地传递给一个转换函数,比如`set_canada_as_country()`。将转换函数应用于所有乐队后,`pipeline_each()`将转换后的乐队打包起来。然后,打包的乐队传递给下一个转换函数。
474 |
475 | 我们来看看转换函数。
476 |
477 | ```python
478 | def assoc(_d, key, value):
479 | from copy import deepcopy
480 | d = deepcopy(_d)
481 | d[key] = value
482 | return d
483 |
484 | def set_canada_as_country(band):
485 | return assoc(band, 'country', "Canada")
486 |
487 | def strip_punctuation_from_name(band):
488 | return assoc(band, 'name', band['name'].replace('.', ''))
489 |
490 | def capitalize_names(band):
491 | return assoc(band, 'name', band['name'].title())
492 | ```
493 |
494 | 每个函数都将乐队的一个键与一个新值相关联。如果不变更原乐队,没有简单的方法可以直接实现。`assoc()`通过使用`deepcopy()`生成传入字典的副本来解决此问题。每个转换函数都对副本进行修改并返回该副本。
495 |
496 | 一切似乎都很好。当键与新值相关联时,可以保护原乐队字典免于被变更。但是上面的代码中还有另外两个潜在的变更。在`strip_punctuation_from_name()`中,原来的乐队名通过调用`replace()`生成无标点的乐队名。在`capitalize_names()`中,原来的乐队名通过调用`title()`生成大写乐队名。如果`replace()`和`title()`不是函数式的,则`strip_punctuation_from_name()`和`capitalize_names()`也将不是函数式的。
497 |
498 | 幸运的是,`replace()`和`title()`不会变更他们操作的字符串。这是因为字符串在`Python`中是不可变的(`immutable`)。例如,当`replace()`对乐队名字符串进行操作时,将复制原来的乐队名并在副本上执行`replace()`调用。Phew ~有惊无险!
499 |
500 | `Python`中字符串和字典之间在可变性上不同的这种对比彰显了像`Clojure`这样语言的吸引力。`Clojure`程序员完全不需要考虑是否会改变数据。`Clojure`的数据结构是不可变的。
501 |
502 | **练习 4**:尝试编写`pipeline_each`函数的实现。想想操作的顺序。数组中的乐队一次一个传递到第一个变换函数。然后返回的结果乐队数组中一次一个乐队传递给第二个变换函数。以此类推。
503 |
504 | 我的实现方案:
505 |
506 | ```python
507 | def pipeline_each(data, fns):
508 | return reduce(lambda a, x: map(x, a),
509 | fns,
510 | data)
511 | ```
512 |
513 | 所有三个转换函数都可以归结为对传入的乐队的特定字段进行更改。可以用`call()`来抽象,`call()`传入一个函数和键名,用键对应的值来调用这个函数。
514 |
515 | ```python
516 | set_canada_as_country = call(lambda x: 'Canada', 'country')
517 | strip_punctuation_from_name = call(lambda x: x.replace('.', ''), 'name')
518 | capitalize_names = call(str.title, 'name')
519 |
520 | print pipeline_each(bands, [set_canada_as_country,
521 | strip_punctuation_from_name,
522 | capitalize_names])
523 | ```
524 |
525 | 或者,如果我们愿意为了简洁而牺牲一些可读性,那么可以写成:
526 |
527 | ```python
528 | print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
529 | call(lambda x: x.replace('.', ''), 'name'),
530 | call(str.title, 'name')])
531 | ```
532 |
533 | `call()`的实现代码:
534 |
535 | ```python
536 | def assoc(_d, key, value):
537 | from copy import deepcopy
538 | d = deepcopy(_d)
539 | d[key] = value
540 | return d
541 |
542 | def call(fn, key):
543 | def apply_fn(record):
544 | return assoc(record, key, fn(record.get(key)))
545 | return apply_fn
546 | ```
547 |
548 | 上面的实现中有不少内容要讲,让我们一点一点地来说明:
549 |
550 | 1. `call()`是一个高阶函数。高阶函数是指将函数作为参数,或返回函数。或者,就像`call()`,输入和返回 2 者都是函数。
551 | 1. `apply_fn()`看起来与三个转换函数非常相似。输入一个记录(一个乐队),`record[key]`是查找出值;再以值为参数调用`fn`,将调用结果赋值回记录的副本;最后返回副本。
552 | 1. `call()`不做任何实际的事。而是调用`apply_fn()`时完成需要做的事。在上面的示例的`pipeline_each()`中,一个`apply_fn()`实例会设置传入乐队的`'country'`成`'Canada'`;另一个实例则将传入乐队的名字转成大写。
553 | 1. 当运行一个`apply_fn()`实例,`fn`和`key`2 个变量并没有在自己的作用域中,既不是`apply_fn()`的参数,也不是本地变量。但 2 者仍然可以访问。
554 | - 当定义一个函数时,会保存这个函数能闭包进来(`close over`)的变量引用:在这个函数外层作用域中定义的变量。这些变量可以在该函数内使用。
555 | - 当函数运行并且其代码引用变量时,`Python`会在本地变量和参数中查找变量。如果没有找到,则会在保存的引用中查找闭包进来的变量。就在这里,会发现`fn`和`key`。
556 | 1. 在`call()`代码中没有涉及乐队列表。这是因为`call()`,无论要处理的对象是什么,可以为任何程序生成管道函数。函数式编程的一大关注点就是构建通用的、可重用的和可组合的函数所组成的库。
557 |
558 | 完美!闭包(`closure`)、高阶函数以及变量作用域,在上面的几段代码中都涉及了。嗯,理解完了上面这些内容,是时候来个驴肉火烧打赏一下自己。🍣
559 |
560 | 最后还差实现一段处理乐队的逻辑:删除除名字和国家之外的内容。`extract_name_and_country()`可以把这些信息提取出来:
561 |
562 | ```python
563 | def extract_name_and_country(band):
564 | plucked_band = {}
565 | plucked_band['name'] = band['name']
566 | plucked_band['country'] = band['country']
567 | return plucked_band
568 |
569 | print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
570 | call(lambda x: x.replace('.', ''), 'name'),
571 | call(str.title, 'name'),
572 | extract_name_and_country])
573 |
574 | # => [{'name': 'Sunset Rubdown', 'country': 'Canada'},
575 | # {'name': 'Women', 'country': 'Canada'},
576 | # {'name': 'A Silver Mt Zion', 'country': 'Canada'}]
577 | ```
578 |
579 | `extract_name_and_country()`本可以写成名为`pluck()`的通用函数。`pluck()`使用起来是这个样子:
580 |
581 | > 【译注】作者这里用了虚拟语气『\*本\*可以』。
582 | > 言外之意是,在实践中为了更具体直白地表达出业务,可能不需要进一步抽象成`pluck()`。
583 |
584 | ```python
585 | print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
586 | call(lambda x: x.replace('.', ''), 'name'),
587 | call(str.title, 'name'),
588 | pluck(['name', 'country'])])
589 | ```
590 |
591 | **练习 5**:`pluck()`输入是要从每条记录中提取键的列表。试着实现一下。它会是一个高阶函数。
592 |
593 | 我的实现方案:
594 |
595 | ```python
596 | def pluck(keys):
597 | def pluck_fn(record):
598 | return reduce(lambda a, x: assoc(a, x, record[x]),
599 | keys,
600 | {})
601 | return pluck_fn
602 | ```
603 |
604 | # 现在开始我们可以做什么?
605 |
606 | 函数式代码与其他风格的代码可以很好地共存。本文中的转换实现可以应用于任何语言的任何代码库。试着应用到你自己的代码中。
607 |
608 | 想想特工玛丽、伊丝拉和山姆。转换列表迭代为`map`和`reduce`。
609 |
610 | 想想车赛。将代码分解为函数。将这些函数转成函数式的。将重复过程的循环转成递归。
611 |
612 | 想想乐队。将一系列操作转为管道。
613 |
614 | ---
615 |
616 | **注:**
617 |
618 | 1. [^](#note_mark1) 不可变数据是无法更改的。某些语言(如`Clojure`)默认就是所有值都不可变。任何『变更』操作都会复制该值,更改副本然后返回更改后的副本。这消除了不完整模型下程序可能进入状态所带来的`Bug`。
619 | 1. [^](#note_mark1) 支持一等公民函数的语言允许像任何其他值一样对待函数。这意味着函数可以创建,传递给函数,从函数返回,以及存储在数据结构中。
620 | 1. [^](#note_mark1) 尾调用优化是一个编程语言特性。函数递归调用时,会创建一个新的栈帧(`stack frame`)。栈帧用于存储当前函数调用的参数和本地值。如果函数递归很多次,解释器或编译器可能会耗尽内存。支持尾调用优化的语言对整个递归调用序列重用同一个的栈帧。像`Python`这样没有尾调用优化的语言通常会限制函数递归的次数(如数千次)。对于上面例子中`race()`函数,因为只有 5 个时间段,所以是安全的。
621 | 1. [^](#note_mark4) 柯里化(`currying`)是指将一个带有多个参数的函数转换成另一个函数,这个函数接受第一个参数,并返回一个接受下一个参数的函数,依此类推所有参数。
622 | 1. [^](#note_mark5) 并行化(`parallelization`)是指,在没有同步的情况下,相同的代码可以并发运行。这些并发处理通常运行在多个处理器上。
623 | 1. [^](#note_mark5) 惰性求值(`lazy evaluation`)是一种编译器技术,可以避免在需要结果之前运行代码。
624 | 1. [^](#note_mark5) 如果每次重复执行都产生相同的结果,则过程就是确定性的。
625 |
626 | ---
627 |
628 | 
629 |
--------------------------------------------------------------------------------
/01~编程范式/函数式编程/99~参考资料/2006~Functional Programming For The Rest of Us.md:
--------------------------------------------------------------------------------
1 | # Functional Programming For The Rest of Us
2 |
3 | # 傻瓜函数式编程
4 |
5 | 2006 年 6 月 19 日,星期一
6 |
7 | ## 1.开篇
8 |
9 | 我们这些码农做事都是很拖拉的。每天例行报到后,先来点咖啡,看看邮件还有 RSS 订阅的文章。然后翻翻新闻还有那些技术网站上的更新,再过一遍编程论坛口水区里那些无聊的论战。最后从头把这些再看一次以免错过什么精彩的内容。然后就可以吃午饭了。饭饱过后,回来盯着 IDE 发一会呆,再看看邮箱,再去搞杯咖啡。光阴似箭,可以回家了……
10 |
11 | (在被众人鄙视之前)我唯一想说的是,在这些拖拉的日子里总会时不时读到一些[不明觉厉](http://www.baike.com/wiki/%E4%B8%8D%E6%98%8E%E8%A7%89%E5%8E%89)的文章。如果没有打开不应该打开的网站,每隔几天你都可以看到至少一篇这样的东西。它们的共性:难懂,耗时,于是这些文章就慢慢的堆积成山了。很快你就会发现自己已经累积了一堆的收藏链接还有数不清的 PDF 文件,此时你只希望隐入一个杳无人烟的深山老林里什么也不做,用一年半载好好的消化这些私藏宝贝。当然,我是说最好每天还是能有人来给送吃的顺带帮忙打扫卫生倒垃圾,哇哈哈。
12 |
13 | 我不知道你都收藏了些什么,我的阅读清单里面相当大部分都是函数式编程相关的东东:基本上是最难啃的。这些文章充斥着无比枯燥的教科书语言,我想就连那些在华尔街浸淫 10 年以上的大牛都无法搞懂这些函数式编程(简称 FP)文章到底在说什么。你可以去花旗集团或者德意志银行找个项目经理来问问[1](#f1):你们为什么要选 JMS 而不用 Erlang?答案基本上是:我认为这个学术用的语言还无法胜任实际应用。可是,现有的一些系统不仅非常复杂还需要满足十分严苛的需求,它们就都是用函数式编程的方法来实现的。这,就说不过去了。
14 |
15 | 关于 FP 的文章确实比较难懂,但我不认为一定要搞得那么晦涩。有一些历史原因造成了这种知识断层,可是 FP 概念本身并不难理解。我希望这篇文章可以成为一个“FP 入门指南”,帮助你从[指令式编程](http://zh.wikipedia.org/zh/%E6%8C%87%E4%BB%A4%E5%BC%8F%E7%B7%A8%E7%A8%8B)走向[函数式编程](http://zh.wikipedia.org/zh/%E5%87%BD%E6%95%B8%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80)。先来点咖啡,然后继续读下去。很快你对 FP 的理解就会让同事们刮目相看了。
16 |
17 | 什么是函数式编程(Functional Programming,FP)?它从何而来?可以吃吗?倘若它真的像那些鼓吹 FP 的人说的那么好,为什么实际应用中那么少见?为什么只有那些在读博士的家伙想要用它?而最重要的是,它母亲的怎么就那么难学?那些所谓的 closure、continuation,currying,lazy evaluation 还有 no side effects 都是什么东东(译者:本着保留专用术语的原则,此处及下文类似情形均不译)?如果没有那些大学教授的帮忙怎样把它应用到实际工程里去?为什么它和我们熟悉的万能而神圣的指令式编程那么的不一样?
18 |
19 | 我们很快就会解开这些谜团。刚才我说过实际工程和学术界之间的知识断层是有其历史原因的,那么就先让我来解释一下这个问题。答案,就在接下来的一次公园漫步中:
20 |
21 | ## 2.公园漫步
22 |
23 | 时间机器启动……我们来到公元前 380 年,也就是 2000 多年前的雅典城外。这是一个阳光明媚的久违的春天,[柏拉图](http://zh.wikipedia.org/zh/%E6%9F%8F%E6%8B%89%E5%9B%BE)和一个帅气的小男仆走在一片橄榄树荫下。他们正准备前往一个学院。天气很好,吃得很饱,渐渐的,两人的谈话转向了哲学。
24 |
25 | “你看那两个学生,哪一个更高一些?”,柏拉图小心的选择用字,以便让这个问题更好的引导眼前的这个小男孩。
26 |
27 | 小男仆望向水池旁边的两个男生,“他们差不多一样高。”。
28 |
29 | “ ‘差不多一样高’ 是什么意思?” , 柏拉图问。
30 |
31 | “嗯……从这里看来他们是一样高的,但是如果走近一点我肯定能看出差别来。”
32 |
33 | 柏拉图笑了。他知道这个小孩已经朝他引导的方向走了。“这么说来你的意思是世界上没有什么东西是完全相同的咯?”
34 |
35 | 思考了一会,小男孩回答:“是的。万物之间都至少有一丁点差别,哪怕我们无法分辨出来。”
36 |
37 | 说到点子上了!“那你说,如果世界上没有什么东西是完全相等的,你怎么理解‘完全相等’这个概念?”
38 |
39 | 小男仆看起来很困惑。“这我就不知道了。”
40 |
41 | 这是人类第一次试图了解数学的本质。柏拉图认为我们所在的世界中,万事万物都是完美模型的一个近似。他同时意识到虽然我们不能感受到完美的模型,但这丝毫不会阻止我们了解完美模型的概念。柏拉图进而得出结论:完美的数学模型只存在于另外一个世界,而因为某种原因我们却可以通过联系着这两个世界的一个纽带来认识这些模型。一个简单的例子就是完美的圆形。没有人见过这样的一个圆,但是我们知道怎样的圆是完美的圆,而且可以用公式把它描述出来。
42 |
43 | 如此说来,什么是数学呢?为什么可以用数学法则来描述我们的这个宇宙?我们所处的这个世界中万事万物都可以用数学来描述吗?[2](#f2)
44 |
45 | 数理哲学是一门很复杂的学科。它和其他多数哲学一样,更着重于提出问题而不是给出答案。数学就像拼图一样,很多结论都是这样推导出来的:先是确立一些互不冲突的基础原理,以及一些操作这些原理的规则,然后就可以把这些原理以及规则拼凑起来形成新的更加复杂的规则或是定理了。数学家把这种方法称为“形式系统”或是“演算”。如果你想做的话,可以用形式系统描述俄罗斯方块这个游戏。而事实上,俄罗斯方块这个游戏的实现,只要它正确运行,就是一个形式系统。只不过它以一种不常见的形式表现出来罢了。
46 |
47 | 如果[半人马阿尔法](http://zh.wikipedia.org/wiki/%E5%8D%8A%E4%BA%BA%E9%A9%AC%E5%BA%A7%CE%B1)上有文明存在的话,那里的生物可能无法解读我们的俄罗斯方块形式系统甚至是简单的圆形的形式系统,因为它们感知世界的唯一器官可能只有鼻子(译者:偶的妈你咋知道?)也许它们是无法得知俄罗斯方块的形式系统了,但是它们很有可能知道圆形。它们的圆形我们可能没法解读,因为我们的鼻子没有它们那么灵敏(译者:那狗可以么?)可是只要越过形式系统的表示方式(比如通过使用“超级鼻子”之类的工具来感知这些用味道表示的形式系统,然后使用标准的解码技术把它们翻译成人类能理解的语言),那么任何有足够智力的文明都可以理解这些形式系统的本质。
48 |
49 | 有意思的是,哪怕宇宙中完全不存在任何文明,类似俄罗斯方块还有圆形这样的形式系统依旧是成立的:只不过没有智慧生物去发现它们而已。这个时候如果忽然一个文明诞生了,那么这些具有智慧的生物就很有可能发现各种各样的形式系统,并且用它们发现的系统去描述各种宇宙法则。不过它们可能不会发现俄罗斯方块这样的形式系统,因为在它们的世界里没有俄罗斯方块这种东西嘛。有很多像俄罗斯方块这样的形式系统是与客观世界无关的,比如说自然数,很难说所有的自然数都与客观世界有关,随便举一个超级大的数,这个数可能就和世界上任何事物无关,因为这个世界可能不是无穷大的。
50 |
51 | ## 3.历史回眸[3](#f3)
52 |
53 | 再次启动时间机……这次到达的是 20 世纪 30 年代,离今天近了很多。无论[新](http://zh.wikipedia.org/wiki/%E6%96%B0%E5%A4%A7%E9%99%B8)[旧](http://zh.wikipedia.org/wiki/%E8%88%8A%E5%A4%A7%E9%99%B8)大陆,经济大萧条都造成了巨大的破坏。社会各阶层几乎每一个家庭都深受其害。只有极其少数的几个地方能让人们免于遭受穷困之苦。几乎没有人能够幸运的在这些避难所里度过危机,注意,我说的是几乎没有,还真的有这么些幸运儿,比如说当时普林斯顿大学的数学家们。
54 |
55 | 新建成的哥特式办公楼给普林斯顿大学带来一种天堂般的安全感。来自世界各地的逻辑学者应邀来到普林斯顿,他们将组建一个新的学部。正当大部分美国人还在为找不到一片面包做晚餐而发愁的时候,在普林斯顿却是这样一番景象:高高的天花板和木雕包覆的墙,每天品茶论道,漫步丛林。
56 |
57 | 一个名叫[阿隆佐·邱奇](http://zh.wikipedia.org/zh/%E9%98%BF%E9%9A%86%E4%BD%90%C2%B7%E9%82%B1%E5%A5%87)(Alonzo Church)的年轻数学家就过着这样优越的生活。阿隆佐本科毕业于普林斯顿后被留在研究院。他觉得这样的生活完全没有必要,于是他鲜少出现在那些数学茶会中也不喜欢到树林里散心。阿隆佐更喜欢独处:自己一个人的时候他的工作效率更高。尽管如此他还是和普林斯顿学者保持着联系,这些人当中有[艾伦·图灵](http://zh.wikipedia.org/zh/%E8%89%BE%E4%BC%A6%C2%B7%E5%9B%BE%E7%81%B5)、[约翰·冯·诺伊曼](http://zh.wikipedia.org/zh/%E7%BA%A6%E7%BF%B0%C2%B7%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC)、[库尔特·哥德尔](http://zh.wikipedia.org/zh-hant/%E5%BA%93%E5%B0%94%E7%89%B9%C2%B7%E5%93%A5%E5%BE%B7%E5%B0%94)。
58 |
59 | 这四个人都对形式系统感兴趣。相对于现实世界,他们更关心如何解决抽象的数学问题。而他们的问题都有这么一个共同点:都在尝试解答关于计算的问题。诸如:如果有一台拥有无穷计算能力的超级机器,可以用来解决什么问题?它可以自动的解决这些问题吗?是不是还是有些问题解决不了,如果有的话,是为什么?如果这样的机器采用不同的设计,它们的计算能力相同吗?
60 |
61 | 在与这些人的合作下,阿隆佐设计了一个名为[lambda 演算](http://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97)的形式系统。这个系统实质上是为其中一个超级机器设计的编程语言。在这种语言里面,函数的参数是函数,返回值也是函数。这种函数用希腊字母 lambda([λ](http://en.wikipedia.org/wiki/Lambda)),这种系统因此得名[4](#f4)。有了这种形式系统,阿隆佐终于可以分析前面的那些问题并且能够给出答案了。
62 |
63 | 除了阿隆佐·邱奇,艾伦·图灵也在进行类似的研究。他设计了一种完全不同的系统(后来被称为[图灵机](http://zh.wikipedia.org/zh/%E5%9B%BE%E7%81%B5%E6%9C%BA)),并用这种系统得出了和阿隆佐相似的答案。到了后来人们证明了图灵机和 lambda 演算的能力是一样的。
64 |
65 | 如果二战没有发生,这个故事到这里就应该结束了,我的这篇小文没什么好说的了,你们也可以去看看有什么其他好看的文章。可是二战还是爆发了,整个世界陷于火海之中。那时的美军空前的大量使用炮兵。为了提高轰炸的精度,军方聘请了大批数学家夜以继日的求解各种差分方程用于计算各种火炮发射数据表。后来他们发现单纯手工计算这些方程太耗时了,为了解决这个问题,各种各样的计算设备应运而生。IBM 制造的 Mark 一号就是用来计算这些发射数据表的第一台机器。Mark 一号重 5 吨,由 75 万个零部件构成,每一秒可以完成 3 次运算。
66 |
67 | 战后,人们为提高计算能力而做出的努力并没有停止。1949 年第一台电子离散变量自动计算机诞生并取得了巨大的成功。它是[冯·诺伊曼设计架构](http://zh.wikipedia.org/zh/%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E7%BB%93%E6%9E%84)的第一个实例,也是一台现实世界中实现的图灵机。相比他的这些同事,那个时候阿隆佐的运气就没那么好了。
68 |
69 | 到了 50 年代末,一个叫 John McCarthy 的 MIT 教授(他也是普林斯顿的硕士)对阿隆佐的成果产生了兴趣。1958 年他发明了一种列表处理语言(Lisp),这种语言是一种阿隆佐 lambda 演算在现实世界的实现,而且它能在冯·诺伊曼计算机上运行!很多计算机科学家都认识到了 Lisp 强大的能力。1973 年在 MIT 人工智能实验室的一些程序员研发出一种机器,并把它叫做 Lisp 机。于是阿隆佐的 lambda 演算也有自己的硬件实现了!
70 |
71 | ## 4.函数式编程
72 |
73 | 函数式编程是阿隆佐思想在现实世界中的实现。不过不是全部的 lambda 演算思想都可以运用到实际中,因 lambda 演算在设计的时候就不是为了在各种现实世界中的限制下工作的。所以,就像面向对象的编程思想一样,函数式编程只是一系列想法,而不是一套严苛的规定。有很多支持函数式编程的程序语言,它们之间的具体设计都不完全一样。在这里我将用 Java 写的例子介绍那些被广泛应用的函数式编程思想(没错,如果你是受虐狂你可以用 Java 写出函数式程序)。在下面的章节中我会在 Java 语言的基础上,做一些修改让它变成实际可用的函数式编程语言。那么现在就开始吧。
74 |
75 | Lambda 演算在最初设计的时候就是为了研究计算相关的问题。所以函数式编程主要解决的也是计算问题,而出乎意料的是,是用函数来解决的!(译者:请理解原作者的苦心,我想他是希望加入一点调皮的风格以免读者在中途睡着或是转台……)。函数就是函数式编程中的基础元素,可以完成几乎所有的操作,哪怕最简单的计算,也是用函数完成的。我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式(这样我们就不用把所有的代码都写在同一行里了)。所以我们这里所说的“变量”是不能被修改的。所有的变量只能被赋一次初值。在 Java 中就意味着每一个变量都将被声明为`final`(如果你用 C++,就是`const`)。在 FP 中,没有非`final`的变量。
76 |
77 | ```java
78 | final int i = 5;
79 | final int j = i + 3;
80 | ```
81 |
82 | 既然 FP 中所有的变量都是`final`的,可以引出两个规定:一是变量前面就没有必要再加上`final`这个关键字了,二是变量就不能再叫做“变量”了……,于是现在开始对 Java 做两个改动:所有 Java 中声明的变量默认为`final`,而且我们把所谓的“变量”称为“符号”。
83 |
84 | 到现在可能会有人有疑问:这个新创造出来的语言可以用来写什么有用的复杂一些的程序吗?毕竟,如果每个符号的值都是不能修改的,那么我们就什么东西都不能改变了!别紧张,这样的说法不完全正确。阿隆佐在设计 lambda 演算的时候他并不想要保留状态的值以便稍后修改这些值。他更关心的是基于数据之上的操作(也就是更容易理解的“计算”)。而且,lambda 演算和图灵机已经被证明了是具有同样能力的系统,因此指令式编程能做到的函数式编程也同样可以做到。那么,怎样才能做到呢?
85 |
86 | 事实上函数式程序是可以保存状态的,只不过它们用的不是变量,而是函数。状态保存在函数的参数中,也就是说在栈上。如果你需要保存一个状态一段时间并且时不时的修改它,那么你可以编写一个递归函数。举个例子,试着写一个函数,用来反转一个 Java 的字符串。记住咯,这个程序里的变量都是默认为`final`的[5](#f5)。
87 |
88 | ```java
89 | String reverse(String arg) {
90 | if(arg.length == 0) {
91 | return arg;
92 | }
93 | else {
94 | return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1);
95 | }
96 | }
97 | ```
98 |
99 | 这个方程运行起来会相对慢一些,因为它重复调用自己[6](#f6)。同时它也会大量的消耗内存,因为它会不断的分配创建内存对象。无论如何,它是用函数式编程思想写出来的。这时候可能有人要问了,为什么要用这种奇怪的方式编写程序呢?嘿,我正准备告诉你。
100 |
101 | ## 5.FP 之优点
102 |
103 | 你大概已经在想:上面这种怪胎函数怎么也不合理嘛。在我刚开始学习 FP 的时候我也这样想的。不过后来我知道我是错的。使用这种方式编程有很多好处。其中一些是主观的。比如说有人认为函数式程序更容易理解。这个我就不说了,哪怕街上随便找个小孩都知道”容易理解“是多么主观的事情。幸运的是,客观方面的好处还有很多。
104 |
105 | ### 5.1.单元测试
106 |
107 | 因为 FP 中的每个符号都是 final 的,于是没有什么函数会有副作用。谁也不能在运行时修改任何东西,也没有函数可以修改在它作用域之外的值给其他函数继续使用(在指令式编程中可以用类成员或是全局变量做到)。这意味着决定函数执行结果的唯一因素就是它的返回值,而影响其返回值的唯一因素就是它的参数。
108 |
109 | 这正是单元测试工程师梦寐以求的啊。现在测试程序中的函数时只需要关注它的参数就可以了。完全不需要担心函数调用的顺序,也不用费心设置外部某些状态值。唯一需要做的就是传递一些可以代表边界条件的参数给这些函数。相对于指令式编程,如果 FP 程序中的每一个函数都能通过单元测试,那么我们对这个软件的质量必将信心百倍。反观 Java 或者 C++,仅仅检查函数的返回值是不够的:代码可能修改外部状态值,因此我们还需要验证这些外部的状态值的正确性。在 FP 语言中呢,就完全不需要。
110 |
111 | ### 5.2.调试查错
112 |
113 | 如果一段 FP 程序没有按照预期设计那样运行,调试的工作几乎不费吹灰之力。这些错误是百分之一百可以重现的,因为 FP 程序中的错误不依赖于之前运行过的不相关的代码。而在一个指令式程序中,一个 bug 可能有时能重现而有些时候又不能。因为这些函数的运行依赖于某些外部状态,而这些外部状态又需要由某些与这个 bug 完全不相关的代码通过某个特别的执行流程才能修改。在 FP 中这种情况完全不存在:如果一个函数的返回值出错了,它一直都会出错,无论你之前运行了什么代码。
114 |
115 | 一旦问题可以重现,解决它就变得非常简单,几乎就是一段愉悦的旅程。中断程序的运行,检查一下栈,就可以看到每一个函数调用时使用的每一个参数,这一点和指令式代码一样。不同的是指令式程序中这些数据还不足够,因为函数的运行还可能依赖于成员变量,全局变量,还有其他类的状态(而这些状态又依赖于类似的变量)。FP 中的函数只依赖于传给它的参数,而这些参数就在眼前!还有,对指令式程序中函数返回值的检查并不能保证这个函数是正确运行的。还要逐一检查若干作用域以外的对象以确保这个函数没有对这些牵连的对象做出什么越轨的行为(译者:好吧,翻译到这里我自己已经有点激动了)。对于一个 FP 程序,你要做的仅仅是看一下函数的返回值。
116 |
117 | 把栈上的数据过一遍就可以得知有哪些参数传给了什么函数,这些函数又返回了什么值。当一个返回值看起来不对头的那一刻,跳进这个函数看看里面发生了什么。一直重复跟进下去就可以找到 bug 的源头!
118 |
119 | ### 5.3.并发执行
120 |
121 | 不需要任何改动,所有 FP 程序都是可以并发执行的。由于根本不需要采用锁机制,因此完全不需要担心死锁或是并发竞争的发生。在 FP 程序中没有哪个线程可以修改任何数据,更不用说多线程之间了。这使得我们可以轻松的添加线程,至于那些祸害并发程序的老问题,想都不用想!
122 |
123 | 既然是这样,为什么没有人在那些高度并行的那些应用程序中采用 FP 编程呢?事实上,这样的例子并不少见。爱立信开发了一种 FP 语言,名叫 Erlang,并应用在他们的电信交换机上,而这些交换机不仅容错度高而且拓展性强。许多人看到了 Erlang 的这些优势也纷纷开始使用这一语言。在这里提到的电信交换控制系统远远要比华尔街上使用的系统具有更好的扩展性也更可靠。事实上,用 Erlang 搭建的系统并不具备可扩展性和可靠性,而 Java 可以提供这些特性。Erlang 只是像岩石一样结实不容易出错而已。
124 |
125 | FP 关于并行的优势不仅于此。就算某个 FP 程序本身只是单线程的,编译器也可以将其优化成可以在多 CPU 上运行的并发程序。以下面的程序为例:
126 |
127 | ```java
128 | String s1 = somewhatLongOperation1();
129 | String s2 = somewhatLongOperation2();
130 | String s3 = concatenate(s1, s2);
131 | ```
132 |
133 | 如果是函数式程序,编译器就可以对代码进行分析,然后可能分析出生成字符串 s1 和 s2 的两个函数可能会比较耗时,进而安排它们并行运行。这在指令式编程中是无法做到的,因为每一个函数都有可能修改其外部状态,然后接下来的函数又可能依赖于这些状态的值。在函数式编程中,自动分析代码并找到适合并行执行的函数十分简单,和分析 C 的内联函数没什么两样。从这个角度来说用 FP 风格编写的程序是“永不过时”的(虽然我一般不喜欢说大话空话,不过这次就算个例外吧)。硬件厂商已经没办法让 CPU 运行得再快了。他们只能靠增加 CPU 核的数量然后用并行来提高运算的速度。这些厂商故意忽略一个事实:只有可以并行的软件才能让你花大价钱买来的这些硬件物有所值。指令式的软件中只有很小一部分能做到跨核运行,而所有的函数式软件都能实现这一目标,因为 FP 的程序从一开始就是可以并行运行的。
134 |
135 | ### 5.4.热部署
136 |
137 | 在 Windows 早期,如果要更新系统那可是要重启电脑的,而且还要重启很多次。哪怕只是安装一个新版本的播放器。到了 XP 的时代这种情况得到比较大的改善,尽管还是不理想(我工作的时候用的就是 Windows,就在现在,我的系统托盘上就有个讨厌的图标,我不重启机子就不消失)。这一方面 Unix 好一些,曾经。只需要暂停一些相关的部件而不是整个操作系统,就可以安装更新了。虽然是要好一些了,对很多服务器应用来说这也还是不能接受的。电信系统要求的是 100%的在线率,如果一个救急电话因为系统升级而无法拨通,成千上万的人就会因此丧命。同样的,华尔街的那些公司怎么也不能说要安装软件而在整个周末停止他们系统的服务。
138 |
139 | 最理想的情况是更新相关的代码而不用暂停系统的其他部件。对指令性程序来说是不可能的。想想看,试着在系统运行时卸载掉一个 Java 的类然后再载入这个类的新的实现,这样做的话系统中所有该类的实例都会立刻不能运行,因为该类的相关状态已经丢失了。这种情况下可能需绞尽脑汁设计复杂的版本控制代码,需要将所有这种类正在运行的实例[序列化](http://zh.wikipedia.org/wiki/%E5%BA%8F%E5%88%97%E5%8C%96),逐一销毁它们,然后创建新类的实例,将现有数据也序列化后装载到这些新的实例中,最后希望负责装载的程序可以正确的把这些数据移植到新实例中并正常的工作。这种事很麻烦,每次有新的改动都需要手工编写装载程序来完成更新,而且这些装载程序还要很小心,以免破坏了现有对象之间的联系。理论上是没问题,可是实际上完全行不通。
140 |
141 | FP 的程序中所有状态就是传给函数的参数,而参数都是储存在栈上的。这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及新的代码获得一个 diff,然后用这个 diff 更新现有的代码,新代码的热部署就完成了。其它的事情有 FP 的语言工具自动完成!如果还有人认为这只存在于科幻小说中,他需要再想想:多年来 Erlang 工程师已经使用这种技术对它们的系统进行升级而完全不用暂停运行了。
142 |
143 | ### 5.5.机器辅助证明及优化
144 |
145 | FP 语言有一个特性很有意思,那就是它们是可以用数学方法来分析的。FP 语言本身就是形式系统的实现,只要是能在纸上写出来的数学运算就可以用这种语言表述出来。于是只要能够用数学方法证明两段代码是一致的,编译器就可以把某段代码解析成在数学上等同的但效率又更高的另外一段代码[7](#f7)。关系数据库已经用这种方法进行优化很多年了。没有理由在常规的软件行业就不能应用这种技术。
146 |
147 | 另外,还可以用这种方法来证明代码的正确性,甚至可以设计出能够自动分析代码并为单元测试自动生成边缘测试用例的工具出来!对于那些对缺陷零容忍的系统来说,这一功能简直就是无价之宝。例如心脏起搏器,例如飞行管控系统,这几乎就是必须满足的需求。哪怕你正在开发的程序不是为了完成什么重要核心任务,这些工具也可以帮助你写出更健壮的程序,直接甩竞争对手 n 条大街。
148 |
149 | ## 6. 高阶函数
150 |
151 | 我还记得在了解到 FP 以上的各种好处后想到:“这些优势都很吸引人,可是,如果必须非要用这种所有变量都是`final`的蹩脚语言,估计还是不怎么实用吧”。其实这样的想法是不对的。对于 Java 这样的指令式语言来说,如果所有的变量都是必须是`final`的,那么确实很束手束脚。然而对函数式语言来说,情况就不一样了。函数式语言提供了一种特别的抽象工具,这种工具将帮助使用者编写 FP 代码,让他们甚至都没想到要修改变量的值。高阶函数就是这种工具之一。
152 |
153 | FP 语言中的函数有别于 Java 或是 C。可以说这种函数是一个[全集](http://zh.wikipedia.org/wiki/%E5%85%A8%E9%9B%86):Java 函数可以做到的它都能做,同时它还有更多的能力。首先,像在 C 里写程序那样创建一个函数:
154 |
155 | ```c
156 | int add(int i, int j) {
157 | return i + j;
158 | }
159 | ```
160 |
161 | 看起来和 C 程序没什么区别,但是很快你就可以看出区别来。接下来我们扩展 Java 的编译器以便支持这种代码,也就是说,当我们写下以上的程序编译器会把它转化成下面的 Java 程序(别忘了,所有的变量都是 final 的):
162 |
163 | ```java
164 | class add_function_t {
165 | int add(int i, int j) {
166 | return i + j;
167 | }
168 | }
169 |
170 | add_function_t add = new add_function_t();
171 | ```
172 |
173 | 在这里,符号`add`并不是一个函数,它是只有一个函数作为其成员的简单的类。这样做有很多好处,可以在程序中把`add`当成参数传给其他的函数,也可以把`add`赋给另外一个符号,还可以在运行时创建`add_function_t`的实例然后在不再需要这些实例的时候由系统回收机制处理掉。这样做使得函数成为和`integer`或是`string`这样的[第一类对象](http://zh.wikipedia.org/zh/%E7%AC%AC%E4%B8%80%E9%A1%9E%E7%89%A9%E4%BB%B6)。对其他函数进行操作(比如说把这些函数当成参数)的函数,就是所谓的高阶函数。别让这个看似高深的名字吓倒你(译者:好死不死起个这个名字,初一看还准备搬出已经尘封的高数教材……),它和 Java 中操作其他类(也就是把一个类实例传给另外的类)的类没有什么区别。可以称这样的类为“高阶类”,但是没人会在意,因为 Java 圈里就没有什么很强的学术社团。(译者:这是高级黑吗?)
174 |
175 | 那么什么时候该用高阶函数,又怎样用呢?我很高兴有人问这个问题。设想一下,你写了一大堆程序而不考虑什么类结构设计,然后发现有一部分代码重复了几次,于是你就会把这部分代码独立出来作为一个函数以便多次调用(所幸学校里至少会教这个)。如果你发现这个函数里有一部分逻辑需要在不同的情况下实现不同的行为,那么你可以把这部分逻辑独立出来作为一个高阶函数。搞晕了?下面来看看我工作中的一个真实的例子。
176 |
177 | 假设有一段 Java 的客户端程序用来接收消息,用各种方式对消息做转换,然后发给一个服务器。
178 |
179 | ```java
180 | class MessageHandler {
181 | void handleMessage(Message msg) {
182 | // ...
183 | msg.setClientCode("ABCD_123");
184 | // ...
185 |
186 | sendMessage(msg);
187 | }
188 |
189 | // ...
190 | }
191 | ```
192 |
193 | 再进一步假设,整个系统改变了,现在需要发给两个服务器而不再是一个了。系统其他部分都不变,唯独客户端的代码需要改变:额外的那个服务器需要用另外一种格式发送消息。应该如何处理这种情况呢?我们可以先检查一下消息要发送到哪里,然后选择相应的格式把这个消息发出去:
194 |
195 | ```java
196 | class MessageHandler {
197 | void handleMessage(Message msg) {
198 | // ...
199 | if(msg.getDestination().equals("server1") {
200 | msg.setClientCode("ABCD_123");
201 | } else {
202 | msg.setClientCode("123_ABC");
203 | }
204 | // ...
205 |
206 | sendMessage(msg);
207 | }
208 |
209 | // ...
210 | }
211 | ```
212 |
213 | 可是这样的实现是不具备扩展性的。如果将来需要增加更多的服务器,上面函数的大小将呈线性增长,使得维护这个函数最终变成一场噩梦。面向对象的编程方法告诉我们,可以把`MessageHandler`变成一个基类,然后将针对不同格式的消息编写相应的子类。
214 |
215 | ```java
216 | abstract class MessageHandler {
217 | void handleMessage(Message msg) {
218 | // ...
219 | msg.setClientCode(getClientCode());
220 | // ...
221 |
222 | sendMessage(msg);
223 | }
224 |
225 | abstract String getClientCode();
226 |
227 | // ...
228 | }
229 |
230 | class MessageHandlerOne extends MessageHandler {
231 | String getClientCode() {
232 | return "ABCD_123";
233 | }
234 | }
235 |
236 | class MessageHandlerTwo extends MessageHandler {
237 | String getClientCode() {
238 | return "123_ABCD";
239 | }
240 | }
241 | ```
242 |
243 | 这样一来就可以为每一个接收消息的服务器生成一个相应的类对象,添加服务器就变得更加容易维护了。可是,这一个简单的改动引出了很多的代码。仅仅是为了支持不同的客户端行为代码,就要定义两种新的类型!现在来试试用我们刚才改造的语言来做同样的事情,注意,这种语言支持高阶函数:
244 |
245 | ```java
246 | class MessageHandler {
247 | void handleMessage(Message msg, Function getClientCode) {
248 | // ...
249 | Message msg1 = msg.setClientCode(getClientCode());
250 | // ...
251 |
252 | sendMessage(msg1);
253 | }
254 |
255 | // ...
256 | }
257 |
258 | String getClientCodeOne() {
259 | return "ABCD_123";
260 | }
261 |
262 | String getClientCodeTwo() {
263 | return "123_ABCD";
264 | }
265 |
266 | MessageHandler handler = new MessageHandler();
267 | handler.handleMessage(someMsg, getClientCodeOne);
268 | ```
269 |
270 | 在上面的程序里,我们没有创建任何新的类型或是多层类的结构。仅仅是把相应的函数作为参数进行传递,就做到了和用面向对象编程一样的事情,而且还有额外的好处:一是不再受限于多层类的结构。这样做可以做运行时传递新的函数,可以在任何时候改变这些函数,而且这些改变不仅更加精准而且触碰的代码更少。这种情况下编译器其实就是在替我们编写面向对象的“粘合”代码(译者:又称胶水代码,粘接代码)!除此之外我们还可以享用 FP 编程的其他所有优势。函数式编程能提供的抽象服务还远不止于此。高阶函数只不过是个开始。
271 |
272 | ## 7.Currying
273 |
274 | 我遇见的大多数码农都读过“[四人帮](http://baike.baidu.com/view/66964.htm#2)”的那本《设计模式》。任何稍有自尊心的码农都会说这本书和语言无关,因此无论你用什么编程语言,当中提到的那些模式大体上适用于所有软件工程。听起来很厉害,然而事实却不是这样。
275 |
276 | 函数式语言的表达能力很强。用这种语言编程的时候基本不需要设计模式,因为这种语言层次已经足够高,使得使用者可以以概念编程,从而完全不需要设计模式了。以[适配器模式](http://zh.wikipedia.org/wiki/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F)为例(有人知道这个模式和[外观模式](http://zh.wikipedia.org/wiki/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F)有什么区别吗?怎么觉得有人为了出版合同的要求而硬生生凑页数?)(译者:您不愧是高级黑啊)。对于一个支持`currying`技术的语言来说,这个模式就是多余的。
277 |
278 | 在 Java 中最有名的适配器模式就是在其“默认”抽象单元中的应用:类。在函数式语言中这种模式其实就是函数。在这个模式中,一个接口被转换成另外一个接口,让不同的用户代码调用。接下来就有一个适配器模式的例子:
279 |
280 | ```java
281 | int pow(int i, int j);
282 | int square(int i)
283 | {
284 | return pow(i, 2);
285 | }
286 | ```
287 |
288 | 上面的代码中`square`函数计算一个整数的平方,这个函数的接口被转换成计算一个整数的任意整数次幂。在学术圈里这种简单的技术就被叫做`currying`(因为逻辑学家[哈斯凯尔·加里](https://zh.wikipedia.org/wiki/%E5%93%88%E6%96%AF%E5%87%B1%E7%88%BE%C2%B7%E5%8A%A0%E9%87%8C)用其数学技巧将这种技术描述出来,于是就以他的名字来命名了)。在一个 FP 语言中函数(而不是类)被作为参数进行传递,currying 常常用于转化一个函数的接口以便于其他代码调用。函数的接口就是它的参数,于是 currying 通常用于减少函数参数的数量(见前例)。
289 |
290 | 函数式语言生来就支持这一技术,于是没有必要为某个函数手工创建另外一个函数去包装并转换它的接口,这些函数式语言已经为你做好了。我们继续拓展 Java 来支持这一功能。
291 |
292 | ```java
293 | square = int pow(int i, 2);
294 | ```
295 |
296 | 上面的语句实现了一个平方计算函数,它只需要一个参数。它会继而调用 pow 函数并且把第二个参数置为 2。编译过后将生成以下 Java 代码:
297 |
298 | ```java
299 | class square_function_t {
300 | int square(int i) {
301 | return pow(i, 2);
302 | }
303 | }
304 | square_function_t square = new square_function_t();
305 | ```
306 |
307 | 从上面的例子可以看到,很简单的,函数 pow 的封装函数就创建出来了。在 FP 语言中 currying 就这么简单:一种可以快速且简单的实现函数封装的捷径。我们可以更专注于自己的设计,编译器则会为你编写正确的代码!什么时候使用 currying 呢?很简单,当你想要用适配器模式(或是封装函数)的时候,就是用 currying 的时候。
308 |
309 | ## 8.[惰性求值](http://zh.wikipedia.org/zh/%E6%83%B0%E6%80%A7%E6%B1%82%E5%80%BC)
310 |
311 | 惰性求值(或是延迟求值)是一种有趣的技术,而当我们投入函数式编程的怀抱后这种技术就有了得以实现的可能。前面介绍并发执行的时候已经提到过如下代码:
312 |
313 | ```java
314 | String s1 = somewhatLongOperation1();
315 | String s2 = somewhatLongOperation2();
316 | String s3 = concatenate(s1, s2);
317 | ```
318 |
319 | 在指令式语言中以上代码执行的顺序是显而易见的。由于每个函数都有可能改动或者依赖于其外部的状态,因此必须顺序执行。先是计算`somewhatLongOperation1`,然后到`somewhatLongOperation2`,最后执行`concatenate`。函数式语言就不一样了。
320 |
321 | 在前面讨论过,`somewhatLongOperation1`和`somewhatLongOperation2`是可以并发执行的,因为函数式语言保证了一点:没有函数会影响或者依赖于全局状态。可是万一我们不想要这两个函数并发执行呢?这种情况下是不是也还是要顺序执行这些函数?答案是否定的。只有到了执行需要 s1、s2 作为参数的函数的时候,才真正需要执行这两个函数。于是在`concatenate`这个函数没有执行之前,都没有需要去执行这两个函数:这些函数的执行可以一直推迟到`concatenate()`中需要用到 s1 和 s2 的时候。假如把`concatenate`换成另外一个函数,这个函数中有条件判断语句而且实际上只会需要两个参数中的其中一个,那么就完全没有必要执行计算另外一个参数的函数了!Haskell 语言就是一个支持惰性求值的例子。Haskell 不能保证任何语句会顺序执行(甚至完全不会执行到),因为 Haskell 的代码只有在需要的时候才会被执行到。
322 |
323 | 除了这些优点,惰性求值也有缺点。这里介绍了它的优点,我们将在下一章节介绍这些缺点以及如何克服它们。
324 |
325 | ### 8.1.代码优化
326 |
327 | 惰性求值使得代码具备了巨大的优化潜能。支持惰性求值的编译器会像数学家看待代数表达式那样看待函数式程序:抵消相同项从而避免执行无谓的代码,安排代码执行顺序从而实现更高的执行效率甚至是减少错误。在此基础上优化是不会破坏代码正常运行的。严格使用形式系统的基本元素进行编程带来的最大的好处,是可以用数学方法分析处理代码,因为这样的程序是完全符合数学法则的。
328 |
329 | ### 8.2.抽象化控制结构
330 |
331 | 惰性求值技术提供了更高阶的抽象能力,这提供了实现程序设计独特的方法。比如说下面的控制结构:
332 |
333 | ```java
334 | unless(stock.isEuropean()) {
335 | sendToSEC(stock);
336 | }
337 | ```
338 |
339 | 我们想要除了`stock`是`European`之外的情况下都执行函数`sendToSEC`。如何实现例子中的 unless?如果没有惰性求值就需要求助于某种形式的宏(译者:用 if 不行么?),不过在像 Haskell 这样的语言中就不需要那么麻烦了。直接实现一个`unless`函数就可以!
340 |
341 | ```haskell
342 | void unless(boolean condition, List code) {
343 | if(!condition)
344 | code;
345 | }
346 | ```
347 |
348 | 请注意,如果 condition 值为真,那就不会计算 code。在其他严格语言(见[严格求值](http://zh.wikipedia.org/wiki/%E6%B1%82%E5%80%BC%E7%AD%96%E7%95%A5#.E4.B8.A5.E6.A0.BC.E6.B1.82.E5.80.BC_.28Strict_evaluation.29))中这种行为是做不到的,因为在进入 unless 这个函数之前,作为参数的 code 已经被计算过了。
349 |
350 | ### 8.3.无穷数据结构
351 |
352 | 惰性求值技术允许定义无穷数据结构,这要在严格语言中实现将非常复杂。例如一个储存 Fibonacci 数列数字的列表。很明显这样一个列表是无法在有限的时间内计算出这个无穷的数列并存储在内存中的。在像 Java 这样的严格语言中,可以定义一个 Fibonacci 函数,返回这个序列中的某个数。而在 Haskell 或是类似的语言中,可以把这个函数进一步抽象化并定义一个 Fibonacci 数列的无穷列表结构。由于语言本身支持惰性求值,这个列表中只有真正会被用到的数才会被计算出来。这让我们可以把很多问题抽象化,然后在更高的层面上解决它们(比如可以在一个列表处理函数中处理无穷多数据的列表)。
353 |
354 | ### 8.4.不足之处
355 |
356 | 俗话说天下没有免费的午餐 ™。惰性求值当然也有其缺点。其中最大的一个就是,嗯,惰性。现实世界中很多问题还是需要严格求值的。比如说下面的例子:
357 |
358 | ```java
359 | System.out.println("Please enter your name: ");
360 | System.in.readLine();
361 | ```
362 |
363 | 在惰性语言中没人能保证第一行会在第二行之前执行!这也就意味着我们不能处理 IO,不能调用系统函数做任何有用的事情(这些函数需要按照顺序执行,因为它们依赖于外部状态),也就是说不能和外界交互了!如果在代码中引入支持顺序执行的代码原语,那么我们就失去了用数学方式分析处理代码的优势(而这也意味着失去了函数式编程的所有优势)。幸运的是我们还不算一无所有。数学家们研究了不同的方法用以保证代码按一定的顺序执行(in a functional setting?)。这一来我们就可以同时利用到函数式和指令式编程的优点了!这些方法有`continuations`,`monads`以及`uniqueness typing`。这篇文章仅仅介绍了`continuations`,以后再讨论`monads`和`uniqueness typing`。有意思的是,`coutinuations`除了可以强制代码以特定顺序执行之外还有其他很多用处,这些我们在后面也会提及。
364 |
365 | ## 9.Continuation
366 |
367 | `continuation`对于编程,就像是达芬奇密码对于人类历史一样:它揭开了人类有史以来最大的谜团。好吧,也许没有那么夸张,不过它们的影响至少和当年发现负数有平方根不相上下。
368 |
369 | 我们对函数的理解只有一半是正确的,因为这样的理解基于一个错误的假设:函数一定要把其返回值返回给调用者。按照这样的理解,`continuation`就是更加广义的函数。这里的函数不一定要把返回值传回给调用者,相反,它可以把返回值传给程序中的任意代码。`continuation`就是一种特别的参数,把这种参数传到函数中,函数就能够根据`continuation`将返回值传递到程序中的某段代码中。说得很高深,实际上没那么复杂。直接来看看下面的例子好了:
370 |
371 | ```java
372 | int i = add(5, 10);
373 | int j = square(i);
374 | ```
375 |
376 | `add`这个函数将返回`15`然后这个值会赋给`i`,这也是`add`被调用的地方。接下来 i 的值又会被用于调用`square`。请注意支持惰性求值的编译器是不能打乱这段代码执行顺序的,因为第二个函数的执行依赖于第一个函数成功执行并返回结果。这段代码可以用 Continuation Pass Style(CPS)技术重写,这样一来 add 的返回值就不是传给其调用者,而是直接传到`square`里去了。
377 |
378 | ```java
379 | int j = add(5, 10, square);
380 | ```
381 |
382 | 在上例中,add 多了一个参数:一个函数,add 必须在完成自己的计算后,调用这个函数并把结果传给它。这时`square`就是`add`的一个`continuation`。上面两段程序中 j 的值都是`225`。
383 |
384 | 这样,我们学习到了强制惰性语言顺序执行两个表达式的第一个技巧。再来看看下面 IO 程序(是不是有点眼熟?):
385 |
386 | ```java
387 | System.out.println("Please enter your name: ");
388 | System.in.readLine();
389 | ```
390 |
391 | 这两行代码彼此之间没有依赖关系,因此编译器可以随意的重新安排它们的执行顺序。可是只要用 CPS 重写它,编译器就必须顺序执行了,因为重写后的代码存在依赖关系了。
392 |
393 | ```java
394 | System.out.println("Please enter your name: ", System.in.readLine);
395 | ```
396 |
397 | 这段新的代码中`println`需要结合其计算结果调用`readLine`,然后再返回`readLine`的返回值。这使得两个函数得以保证按顺序执行而且`readLine`总被执行(这是由于整个运算需要它的返回值作为最终结果)。Java 的`println`是没有返回值的,但是如果它可以返回一个能被`readLine`接受的抽象值,问题就解决了!(译者:别忘了,这里作者一开始就在 Java 的基础上修改搭建自己的语言)当然,如果一直把函数按照这种方法串下去,代码很快就变得不可读了,可是没有人要求你一定要这样做。可以通过在语言中添加[语法糖](http://zh.wikipedia.org/wiki/%E8%AF%AD%E6%B3%95%E7%B3%96)的方式来解决这个问题,这样程序员只要按照顺序写代码,编译器负责自动把它们串起来就好了。于是就可以任意安排代码的执行顺序而不用担心会失去 FP 带来的好处了(包括可以用数学方法来分析我们的程序)!如果到这里还有人感到困惑,可以这样理解,函数只是有唯一成员的类的实例而已。试着重写上面两行程序,让`println`和`readLine`变成这种类的实例,所有问题就都搞清楚了。
398 |
399 | 到这里本章基本可以结束了,而我们仅仅了解到`continuation`的一点皮毛,对它的用途也知之甚少。我们可以用 CPS 完成整个程序,程序里所有的函数都有一个额外的`continuation`作为参数接受其他函数的返回值。还可以把任何程序转换为 CPS 的,需要做的只是把当中的函数看作是特殊的`continuation`(总是将返回值传给调用者的`continuation`)就可以了,简单到完全可以由工具自动完成(史上很多编译器就是这样做的)。
400 |
401 | 一旦将程序转为 CPS 的风格,有些事情就变得显而易见了:每一条指令都会有一些`continuation`,都会将它的计算结果传给某一个函数并调用它,在一个普通的程序中这个函数就是该指令被调用并且返回的地方。随便找个之前提到过的代码,比如说`add(5,10)`好了。如果`add`属于一个用 CPS 风格写出的程序,`add`的`continuation`很明显就是当它执行结束后要调用的那个函数。可是在一个非 CPS 的程序中,`add`的`continuation`又是什么呢?当然我们还是可以把这段程序转成 CPS 的,可是有必要这样做吗?
402 |
403 | 事实上没有必要。注意观察整个 CPS 转换过程,如果有人尝试要为 CPS 程序写编译器并且认真思考过就会发现:CPS 的程序是不需要栈的!在这里完全没有函数需要做传统意义上的 “return” 操作,函数执行完后仅需要接着调用另外一个函数就可以了。于是就不需要在每次调用函数的时候把参数压栈再将它们从中取出,只要把这些参数存放在一片内存中然后使用跳转指令就解决问题了。也完全不需要保留原来的参数:因为这种程序里的函数都不返回,所以它们不会被用第二次!
404 |
405 | 简单点说呢,用 CPS 风格写出来的程序不需要栈,但是每次调用函数的时候都会要多加一个参数。非 CPS 风格的程序不需要额外的参数但又需要栈才能运行。栈里面存的是什么?仅仅是参数还有一个供函数运行结束后返回的程序指针而已。这个时候你是不是已经恍然大悟了?对啊,栈里面的数据实际上就是`continuation`的信息!栈上的程序返回指针实质上就是 CPS 程序中需要调用的下一个函数!想要知道`add(5, 10)`的`continuation`是什么?只要看它运行时栈的内容就可以了。
406 |
407 | 接下来就简单多了。`continuation`和栈上指示函数返回地址的指针其实是同一样东西,只是`continuation`是显式的传递该地址并且因此代码就不局限于只能返回到函数被调用的地方了。前面说过,`continuation`就是函数,而在我们特制的语言中函数就是类的实例,那么可以得知栈上指向函数返回地址的指针和`continuation`的参数是一样的,因为我们所谓的函数(就像类的一个实例)其实就是指针。这也意味着在程序运行的任何时候,你都可以得到当前的`continuation`(就是栈上的信息)。
408 |
409 | 好了,我们已经搞清楚当前的`continuation`是什么了。接下来要弄明白它的存在有什么意义。只要得到了当前的`continuation`并将它保存起来,就相当于保存了程序的当前状态:在时间轴上把它冻结起来了。这有点像操作系统进入休眠状态。`continuation`对象保存了足够的信息随时可以从指定的某个状态继续运行程序。在切换线程的时候操作系统也是这样做的。唯一的区别在于它保留了所有的控制权利。当请求某个`continuation`对象时(在 Scheme 语言中是通过调用`call-with-current-continuation`函数实现的)得到的是一个存有当前`continuation`的对象,也就是栈对象(在 CPS 中也就是下一个要执行的函数)。可以把这个对象保存做一个变量中(或者是存在磁盘上)。当以该`continuation`对象“重启”该程序时,程序的状态就会立即“转换”为该对象中保存的状态。这一点和切换回一个被暂停的线程或是从系统休眠中唤醒很相像,唯一不同的是`continuatoin`对象可以反复的这样使用。当系统唤醒后,休眠前保存的信息就会销毁,否则你也可以反复的从该点唤醒系统,就像乘时光机回到过去一样。有了`continuation`你就可以做到这一点!
410 |
411 | 那么`continuation`在什么情况下有用呢?有一些应用程序天生就没有状态,如果要在这样的系统中模拟出状态以简化工作的时候,就可以用到`continuation`。最合适的应用场合之一就是网页应用程序。微软的 ASP.NET 为了让程序员更轻松的编写应用程序,花了大量的精力去模拟各种状态。假如 C#支持`continuation`的话,那么 ASP.NET 的复杂度将减半:因为只要把某一时刻的`continuation`保存起来,下次用户再次发起同样请求的时候,重新载入这个`continuation`即可。对于网络应用的程序员来说就再也没有中断了:轻轻松松程序就从下一行开始继续运行了!对于一些实际问题来说,`continuation`是一种非常有用的抽象工具。如今大量的传统胖客户端(见[瘦客户端](http://zh.wikipedia.org/wiki/%E7%98%A6%E5%AE%A2%E6%88%B7%E7%AB%AF))正纷纷走进网络,`continuation`在未来将扮演越来越重要的角色。
412 |
413 | ## 10.模式匹配
414 |
415 | 模式匹配并不是什么新功能。而事实上它和函数式编程也没有什么太大的关系。它之所以常常被认为是 FP 的一个特性,是因为在函数式语言已经支持模式匹配很长一段时间后的今天,指令式语言是还没有这个功能。
416 |
417 | 还是直接用例子来看看什么是模式匹配吧,这是一个用 Java 写的 Fibonacci 函数:
418 |
419 | ```java
420 | int fib(int n) {
421 | if(n == 0) return 1;
422 | if(n == 1) return 1;
423 |
424 | return fib(n - 2) + fib(n - 1);
425 | }
426 | ```
427 |
428 | 再看看用我们基于 Java 修改过的新语言写出来的 Fibonacci 函数,这种新语言就支持模式匹配:
429 |
430 | ```java
431 | int fib(0) {
432 | return 1;
433 | }
434 | int fib(1) {
435 | return 1;
436 | }
437 | int fib(int n) {
438 | return fib(n - 2) + fib(n - 1);
439 | }
440 | ```
441 |
442 | 区别在哪里呢?在于后者的编译器替我们实现了程序的分支。
443 |
444 | 这有什么了不起的?确实也没什么。只是有人注意到很多函数中有非常复杂的 switch 结构(对于函数式程序而言更是如此),于是想到如果能把这层结构也抽象化就更好了。然后就把这个复杂的函数拆分成若干新的函数,并在这些函数的某些参数中应用模式(这和[重载](http://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD)有点类似)。当这个函数被调用的时候,编译器会在运行时将调用者传入的参数与各个新函数的参数定义进行比较,找出合适的那个函数来执行。合适的函数往往是参数定义上最具体最接近传入参数的那个函数。在这个例子中,当 n 为 1 时,可以用函数 int fib(int n),不过真正调用的是 int fib(1)因为这个函数更具体更接近调用者的要求。
445 |
446 | 模式匹配一般来说要比这里举的例子更加复杂。比如说,高级模式匹配系统可以支持下面的操作:
447 |
448 | ```java
449 | int f(int n < 10) { ... }
450 | int f(int n) { ... }
451 | ```
452 |
453 | 那么什么情况下模式匹配会有用呢?在需要处理一大堆程序分支的时候!每当需要实现复杂的嵌套 if 语句的时候,模式匹配可以帮助你用更少的代码更好的完成任务。我所知道的一个这样的函数是标准的 WndProc 函数,该函数是所有 Win32 应用程序必须具备的(尽管它经常会被抽象化)。模式匹配系统一般都可以像匹配简单数值一样匹配数据集合。举个例子,对于一个接受数组作为参数的函数,可以通过模式匹配数组中第一个数字为 1 并且第三个数字大于 3 的输入。
454 |
455 | 模式匹配的另外一个好处是每当需要添加或者修改程序分支时,再也不用面对那个庞大臃肿的函数了。只要添加(或者修改)相关的函数定义即可。有了模式匹配就不再需要四人帮的很多设计模式了。程序分支越多越复杂,模式匹配就越有用。而在习惯使用这一技术之后,你可能会怀疑没有它你一天都过不下去了。
456 |
457 | ## 11.Closure
458 |
459 | 目前为止关于函数式编程各种功能的讨论都只局限在“纯”函数式语言范围内:这些语言都是 lambda 演算的实现并且都没有那些和阿隆佐形式系统相冲突的特性。然而,很多函数式语言的特性哪怕是在 lambda 演算框架之外都是很有用的。确实,如果一个公理系统的实现可以用数学思维来看待程序,那么这个实现还是很有用的,但这样的实现却不一定可以付诸实践。很多现实中的语言都选择吸收函数式编程的一些元素,却又不完全受限于函数式教条的束缚。很多这样的语言(比如 Common Lisp)都不要求所有的变量必须为`final`,可以修改他们的值。也不要求函数只能依赖于它们的参数,而是可以读写函数外部的状态。同时这些语言又包含了 FP 的特性,如高阶函数。与在 lambda 演算限制下将函数作为参数传递不同,在指令式语言中要做到同样的事情需要支持一个有趣的特性,人们常把它称为`lexical closure`。还是来看看例子。要注意的是,这个例子中变量不是 final,而且函数也可以读写其外部的变量:
460 |
461 | ```java
462 | Function makePowerFn(int power) {
463 | int powerFn(int base) {
464 | return pow(base, power);
465 | }
466 |
467 | return powerFn;
468 | }
469 |
470 | Function square = makePowerFn(2);
471 | square(3); // returns 9
472 | ```
473 |
474 | `makePowerFn`函数返回另一个函数,这个新的函数需要一个整数参数然后返回它的平方值。执行 square(3)的时候具体发生了什么事呢?变量`power`并不在`powerFn`的域内,因为`makePowerFn`早就运行结束返回了,所以它的栈也已经不存在了。那么 square 又是怎么正常工作的呢?这个时候需要语言通过某种方式支持继续存储`power`的值,以便`square`后面继续使用。那么如果再定义一个函数,cube,用来计算立方,又应该怎么做呢?那么运行中的程序就必须存储两份`power`的值,提供给`makePowerFn`生成的两个函数分别使用。这种保存变量值的方法就叫做 closure。closure 不仅仅保存宿主函数的参数值,还可以用在下例的用法中:
475 |
476 | ```java
477 | Function makeIncrementer() {
478 | int n = 0;
479 |
480 | int increment() {
481 | return ++n;
482 | }
483 | }
484 |
485 | Function inc1 = makeIncrementer();
486 | Function inc2 = makeIncrementer();
487 |
488 | inc1(); // returns 1;
489 | inc1(); // returns 2;
490 | inc1(); // returns 3;
491 | inc2(); // returns 1;
492 | inc2(); // returns 2;
493 | inc2(); // returns 3;
494 | ```
495 |
496 | 运行中的程序负责存储 n 的值,以便`incrementer`稍后可以访问它。与此同时,程序还会保存多份 n 的拷贝,虽然这些值应该在`makeIncrementer`返回后就消失,但在这个情况下却继续保留下来给每一个`incrementer`对象使用。这样的代码编译之后会是什么样子?closure 幕后的真正工作机理又是什么?这次运气不错,我们有一个后台通行证,可以一窥究竟。
497 |
498 | 一点小常识往往可以帮大忙。乍一看这些本地变量已经不再受限于基本的域限制并拥有无限的生命周期了。于是可以得出一个很明显的结论:它们已经不是存在栈上,而是堆上了[8](#f8)。这么说来 closure 的实现和前面讨论过的函数差不多,只不过 closure 多了一个额外的引用指向其外部的变量而已:
499 |
500 | ```java
501 | class some_function_t {
502 | SymbolTable parentScope;
503 |
504 | // ...
505 | }
506 | ```
507 |
508 | 当 closure 需要访问不在它本地域的变量时,就可以通过这个引用到更外一层的父域中寻找该变量。谜底揭开了!closure 将函数编程与面向对象的方法结合了起来。下一次为了保存并传递某些状态而创建类的时候,想想 closure。它能在运行时从相应的域中获得变量,从而可以把该变量当成“成员变量”来访问,也因为这样,就不再需要去创建一个成员变量了。
509 |
510 | ## 12.路在何方?
511 |
512 | 这篇文章仅仅涉及到函数式编程的一些皮毛。考虑到有时候星星之火可以燎原,所以如果它能给你一些帮助那就再好不过了。接下来我计划就[范畴论](http://zh.wikipedia.org/wiki/%E8%8C%83%E7%95%B4%E8%AE%BA)、monads、函数式编程数据结构、函数式语言中的[类型系统](http://zh.wikipedia.org/wiki/%E9%A1%9E%E5%9E%8B%E7%B3%BB%E7%B5%B1)、并行函数式编程、数据库的函数式编程以及更多的话题写些类似的文章。如果我可以写出(在我学习的同时)以上清单的一半,我的人生就完整了。于此同时,Google 将是我们的良师益友。
513 |
514 | ## 13.欢迎联系
515 |
516 | 如果您有任何问题,评价或者建议,请发邮件到coffeemug@gmail.com(译者:如果翻译方面的问题/建议请发到yang.huang@ymail.com:))。期待您的回复。
517 |
518 | 注:
519 |
520 | 1当我在 2005 年求职时的的确确经常问别人这个问题。看着那些茫然的面孔实在是很好玩的事情。你们这些年薪 30 万美金的家伙,至少应该对自己可以利用的工具有个起码的理解嘛。[↩](#a1)
521 |
522 | 2这是个有争议的问题。物理学家和数学家不得不承认目前还无法确定宇宙万物是不是都遵从可以用数学方法描述的各种法则。[↩](#a2)
523 |
524 | 3我一直一来都很讨厌在历史课上罗列一堆枯燥无味的时间、人名、事件。对我来说历史就是关于那些改变世界的人们活生生的故事,是他们行为背后的个人动机,是那些他们用以影响芸芸众生的方法和工具。从这个角度来说,接下来的这堂历史课是不完整的,很遗憾。只有那些非常相关的人和事会被提及。[↩](#a3)
525 |
526 | 4在我学习函数式编程的时候,“lambda”这个术语搞得我很烦,因为我不知道它到底是什么意思。在这里 lambda 就是一个函数,在数学符号中用这个希腊字母只是因为它更容易写。所以以后在谈及函数式编程的时候只要你听到 lambda,把它在脑中翻译为“函数”就可以了。[↩](#a4)
527 |
528 | 5有意思的是不论如何 Java 中的字符串总是不可修改的。讨论这种背叛 Java 的设计背后的原因会很有意思,可惜这样会让我们跑题的。[↩](#a5)
529 |
530 | 6大部分函数式语言的编译器都会尽量将迭代函数转换为对等的循环语句。这种做法叫做[尾调用优化](http://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8)。[↩](#a6)
531 |
532 | 7反之则不一定成立。尽管有时候可以证明两段代码是等价的,但不是在所有的情况下都可以得出这样的结论。[↩](#a7)
533 |
534 | 8实际上这样做并不比栈上存储要慢,因为在引入[垃圾回收机制](