├── .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 | ![设计模式总览](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230611213808.png) 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 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230418222524.png) 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 | ![SOLID](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230416204138.png) 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 | ![单一职责原则之间的关系](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230416204211.png) 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 | ![](http://www.nowamagic.net/librarys/images/201306/2013_06_25_01.jpg) 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 | ![设计模式之间的关系](https://pic.imgdb.cn/item/615167512ab3f51d91510099.jpg) 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 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522114533.png) 8 | 9 | 适配器假扮成一个圆钉(Round­Peg),其半径等于方钉(Square­Peg)横截面对角线的一半(即能够容纳方钉的最小外接圆的半径)。 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 | ![依赖注入示意图](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230418223034.png) 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 | ![模块齿轮示意图](https://s2.ax1x.com/2020/01/07/lcpfte.png) 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 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230417210121.png) 2 | 3 | # 单例 4 | 5 | 单例是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。单例模式主要解决了以下问题: 6 | 7 | - 保证一个类只有一个实例,以控制某些共享资源(例如数据库或文件)的访问权限。 8 | - 为该实例提供一个全局访问节点,和全局变量一样,单例模式也允许在程序的任何地方访问特定对象。但是它可以保护该实例不被其他代码覆盖。 9 | 10 | 如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。 11 | 12 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230417210231.png) 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 | 34 | ``` 35 | 36 | 现在想让当前所选页面的标题具有一个蓝色的背景,以便在视觉上突出显示。使用 CSS 实现起来非常简单: 37 | 38 | ```css 39 | li.selected > p { 40 | background-color: blue; 41 | } 42 | ``` 43 | 44 | 这里的 CSS 选择器`li.selected> p`声明了我们想要应用蓝色样式的元素的模式:即其直接父元素是具有`selected`CSS 类的`元素的所有`元素。示例中的元素`Sharks`匹配此模式,但`Whales`不匹配,因为其``父元素缺少`class =“selected”`。 45 | 46 | 如果使用 XSL 而不是 CSS,你可以做类似的事情: 47 | 48 | ```xml 49 | 50 | 51 | 52 | 53 | 54 | ``` 55 | 56 | 这里的 XPath 表达式`li[@class='selected']/p`相当于上例中的 CSS 选择器`li.selected> p`。CSS 和 XSL 的共同之处在于,它们都是用于指定文档样式的声明式语言。 57 | 58 | 想象一下,必须使用命令式方法的情况会是如何。在 Javascript 中,使用 **文档对象模型(DOM)** API,其结果可能如下所示: 59 | 60 | ```js 61 | var liElements = document.getElementsByTagName("li"); 62 | for (var i = 0; i < liElements.length; i++) { 63 | if (liElements[i].className === "selected") { 64 | var children = liElements[i].childNodes; 65 | for (var j = 0; j < children.length; j++) { 66 | var child = children[j]; 67 | if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "P") { 68 | child.setAttribute("style", "background-color: blue"); 69 | } 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | 这段 JavaScript 代码命令式地将元素设置为蓝色背景,但是代码看起来很糟糕。不仅比 CSS 和 XSL 等价物更长,更难理解,而且还有一些严重的问题: 76 | 77 | - 如果选定的类被移除(例如,因为用户点击了不同的页面),即使代码重新运行,蓝色背景也不会被移除 - 因此该项目将保持突出显示,直到整个页面被重新加载。使用 CSS,浏览器会自动检测`li.selected> p`规则何时不再适用,并在选定的类被移除后立即移除蓝色背景。 78 | - 如果你想要利用新的 API(例如`document.getElementsBy ClassName(“selected”`)甚至`document.evaluate()`)来提高性能,则必须重写代码。另一方面,浏览器供应商可以在不破坏兼容性的情况下提高 CSS 和 XPath 的性能。 79 | 80 | 在 Web 浏览器中,使用声明式 CSS 样式比使用 JavaScript 命令式地操作样式要好得多。类似地,在数据库中,使用像 SQL 这样的声明式查询语言比使用命令式查询 API 要好得多。 81 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/01~创建型模式/原型.md: -------------------------------------------------------------------------------- 1 | # 原型 2 | 3 | 原型是一种创建型设计模式,使你能够复制已有对象,而又无需使代码依赖它们所属的类。 4 | 5 | # 案例:几何复制 6 | 7 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430223314.png) 8 | 9 | ```ts 10 | // 基础原型。 11 | abstract class Shape is 12 | field X: int 13 | field Y: int 14 | field color: string 15 | 16 | // 常规构造函数。 17 | constructor Shape() is 18 | // ... 19 | 20 | // 原型构造函数。使用已有对象的数值来初始化一个新对象。 21 | constructor Shape(source: Shape) is 22 | this() 23 | this.X = source.X 24 | this.Y = source.Y 25 | this.color = source.color 26 | 27 | // 克隆(clone)操作会返回一个形状子类。 28 | abstract method clone():Shape 29 | 30 | 31 | // 具体原型。克隆方法会创建一个新对象并将其传递给构造函数。直到构造函数运 32 | // 行完成前,它都拥有指向新克隆对象的引用。因此,任何人都无法访问未完全生 33 | // 成的克隆对象。这可以保持克隆结果的一致。 34 | class Rectangle extends Shape is 35 | field width: int 36 | field height: int 37 | 38 | constructor Rectangle(source: Rectangle) is 39 | // 需要调用父构造函数来复制父类中定义的私有成员变量。 40 | super(source) 41 | this.width = source.width 42 | this.height = source.height 43 | 44 | method clone():Shape is 45 | return new Rectangle(this) 46 | 47 | 48 | class Circle extends Shape is 49 | field radius: int 50 | 51 | constructor Circle(source: Circle) is 52 | super(source) 53 | this.radius = source.radius 54 | 55 | method clone():Shape is 56 | return new Circle(this) 57 | 58 | 59 | // 客户端代码中的某个位置。 60 | class Application is 61 | field shapes: array of Shape 62 | 63 | constructor Application() is 64 | Circle circle = new Circle() 65 | circle.X = 10 66 | circle.Y = 10 67 | circle.radius = 20 68 | shapes.add(circle) 69 | 70 | Circle anotherCircle = circle.clone() 71 | shapes.add(anotherCircle) 72 | // 变量 `anotherCircle`(另一个圆)与 `circle`(圆)对象的内 73 | // 容完全一样。 74 | 75 | Rectangle rectangle = new Rectangle() 76 | rectangle.width = 10 77 | rectangle.height = 20 78 | shapes.add(rectangle) 79 | 80 | method businessLogic() is 81 | // 原型是很强大的东西,因为它能在不知晓对象类型的情况下生成一个与 82 | // 其完全相同的复制品。 83 | Array shapesCopy = new Array of Shapes. 84 | 85 | // 例如,我们不知晓形状数组中元素的具体类型,只知道它们都是形状。 86 | // 但在多态机制的帮助下,当我们在某个形状上调用 `克隆`(clone) 87 | // 方法时,程序会检查其所属的类并调用其中所定义的克隆方法。这样, 88 | // 我们将获得一个正确的复制品,而不是一组简单的形状对象。 89 | foreach (s in shapes) do 90 | shapesCopy.add(s.clone()) 91 | 92 | // `shapesCopy`(形状副本)数组中包含 `shape`(形状)数组所有 93 | // 子元素的复制品。 94 | ``` 95 | -------------------------------------------------------------------------------- /03~软件架构设计/2024~软件架构的考虑维度.md: -------------------------------------------------------------------------------- 1 | # 软件架构的考虑维度 2 | 3 | ![思维脑图](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/uPic/5eGG1FtAmfrP.png) 4 | 5 | ## 1. 代码架构视角 (Development Perspective) 6 | 7 | - 代码质量 (Code Quality) 8 | 9 | - 可测试性 (Testability):代码设计便于编写和执行自动化测试的程度 10 | - 可维护性 (Maintainability):代码易于理解、修改和扩展的特性 11 | - 可读性 (Readability):代码结构清晰、命名规范、易于阅读理解的程度 12 | - 可重构性 (Refactorability):代码能够在保持功能不变的情况下易于优化和改进的特性 13 | 14 | - 研发效能 (Development Efficiency) 15 | 16 | - 可构建性 (Buildability):项目能够快速、可靠地完成编译和打包的能力 17 | - 可调试性 (Debuggability):系统提供充分的信息和工具支持以便于问题诊断和修复 18 | - 工具支持 (Tooling Support):开发环境、框架和工具链对开发过程的支持程度 19 | - 版本管理 (Version Management):代码和资源的版本控制、分支管理的有效性 20 | - 知识共享机制 (Knowledge Sharing):团队间技术文档、经验和最佳实践的传递效率 21 | 22 | ## 2. 业务架构视角 (Business & Architecture Perspective) 23 | 24 | - 逻辑架构特性 (Logical Architecture Characteristics) 25 | 26 | - 模块化 (Modularity):系统被合理划分为独立、可重用模块的程度 27 | - 松耦合 (Loose Coupling):系统各组件之间依赖关系最小化的设计原则 28 | - 高内聚 (High Cohesion):模块内部功能紧密相关、职责单一的特性 29 | - 可扩展性 (Extensibility):系统能够方便地添加新功能而无需大规模修改的能力 30 | - 可演化性 (Evolvability):系统能够平滑地进行版本升级和功能演进的能力 31 | 32 | - 用户体验 (User Experience) 33 | 34 | - 易用性 (Usability):系统界面直观、操作简单、学习成本低的特性 35 | - 响应性 (Responsiveness):系统对用户操作的快速响应能力 36 | - 可访问性 (Accessibility):系统对不同用户群体(包括残障人士)的适用性 37 | - 一致性 (Consistency):系统在界面、交互和功能上保持统一风格的程度 38 | 39 | - 业务适应性 (Business Adaptability) 40 | 41 | - 可配置性 (Configurability):系统通过配置方式适应不同业务需求的能力 42 | - 可定制性 (Customizability):系统支持按客户需求进行个性化改造的程度 43 | - 业务连续性 (Business Continuity):系统保障业务不中断运行的能力 44 | - 国际化与本地化 (I18n & L10n):系统支持多语言、多地区使用的能力 45 | - 法律合规 (Legal Compliance):系统符合相关法律法规和行业标准的程度 46 | 47 | ## 3. 系统架构视角 (System Architecture Perspective) 48 | 49 | - 性能指标 (Performance Metrics) 50 | 51 | - 高性能 (High Performance):系统在资源利用和响应时间上的优化程度 52 | - 高并发 (High Concurrency):系统同时处理大量请求的能力 53 | - 高吞吐 (High Throughput):系统在单位时间内处理业务请求的数量 54 | - 可扩展性 (Scalability):系统通过扩展资源提升处理能力的便利性 55 | 56 | - 可靠性保障 (Reliability Assurance) 57 | 58 | - 高可用性 (High Availability):系统保持持续服务的能力和时间比例 59 | - 容错性 (Fault Tolerance):系统在部分组件失效时保持功能的能力 60 | - 韧性 (Resilience):系统在面对故障时快速恢复的能力 61 | - 安全性 (Security):系统防御各类安全威胁和保护数据的能力 62 | 63 | - 运维支持 (Operations Support) 64 | 65 | - 可观测性 (Observability):系统运行状态的监控、追踪和分析能力 66 | - 可部署性 (Deployability):系统快速、可靠地完成部署和更新的能力 67 | - 可控性 (Controllability):对系统运行状态进行精确控制和调节的能力 68 | 69 | - 成本效益 (Cost Effectiveness) 70 | 71 | - TCO 优化 (Total Cost of Ownership):系统全生命周期总拥有成本的优化 72 | - ROI 考量 (Return on Investment):系统投资回报率的评估和优化 73 | - 资源效率 (Resource Efficiency):系统对计算、存储等资源的利用效率 74 | - 维护成本 (Maintenance Cost):系统日常运维和问题修复的投入成本 75 | 76 | ## 4. 数据架构视角 (Data Perspective) 77 | 78 | - 数据质量 (Data Quality) 79 | 80 | - 一致性 (Consistency):数据在不同节点和时间点保持一致的程度 81 | - 完整性 (Integrity):数据的准确性和完整性得到保障的程度 82 | - 可用性 (Availability):数据能够被及时、可靠访问的程度 83 | 84 | - 集成能力 (Integration Capability) 85 | 86 | - 互操作性 (Interoperability):系统与其他系统进行数据交换的能力 87 | - 标准遵从性 (Standards Compliance):系统对数据标准和协议的遵循程度 88 | - 接口稳定性 (Interface Stability):系统对外接口的稳定性和向后兼容性 89 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/迭代器.md: -------------------------------------------------------------------------------- 1 | # 迭代器 2 | 3 | 迭代器是一种行为设计模式,让你能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。 4 | 5 | # 案例:通讯录 6 | 7 | ```ts 8 | // 集合接口必须声明一个用于生成迭代器的工厂方法。如果程序中有不同类型的迭 9 | // 代器,你也可以声明多个方法。 10 | interface SocialNetwork is 11 | method createFriendsIterator(profileId):ProfileIterator 12 | method createCoworkersIterator(profileId):ProfileIterator 13 | 14 | 15 | // 每个具体集合都与其返回的一组具体迭代器相耦合。但客户并不是这样的,因为 16 | // 这些方法的签名将会返回迭代器接口。 17 | class WeChat implements SocialNetwork is 18 | // ...大量的集合代码应该放在这里... 19 | 20 | // 迭代器创建代码。 21 | method createFriendsIterator(profileId) is 22 | return new WeChatIterator(this, profileId, "friends") 23 | method createCoworkersIterator(profileId) is 24 | return new WeChatIterator(this, profileId, "coworkers") 25 | 26 | 27 | // 所有迭代器的通用接口。 28 | interface ProfileIterator is 29 | method getNext():Profile 30 | method hasMore():bool 31 | 32 | 33 | // 具体迭代器类。 34 | class WeChatIterator implements ProfileIterator is 35 | // 迭代器需要一个指向其遍历集合的引用。 36 | private field weChat: WeChat 37 | private field profileId, type: string 38 | 39 | // 迭代器对象会独立于其他迭代器来对集合进行遍历。因此它必须保存迭代器 40 | // 的状态。 41 | private field currentPosition 42 | private field cache: array of Profile 43 | 44 | constructor WeChatIterator(weChat, profileId, type) is 45 | this.weChat = weChat 46 | this.profileId = profileId 47 | this.type = type 48 | 49 | private method lazyInit() is 50 | if (cache == null) 51 | cache = weChat.socialGraphRequest(profileId, type) 52 | 53 | // 每个具体迭代器类都会自行实现通用迭代器接口。 54 | method getNext() is 55 | if (hasMore()) 56 | currentPosition++ 57 | return cache[currentPosition] 58 | 59 | method hasMore() is 60 | lazyInit() 61 | return cache.length < currentPosition 62 | 63 | 64 | // 这里还有一个有用的绝招:你可将迭代器传递给客户端类,无需让其拥有访问整 65 | // 个集合的权限。这样一来,你就无需将集合暴露给客户端了。 66 | // 67 | // 还有另一个好处:你可在运行时将不同的迭代器传递给客户端,从而改变客户端 68 | // 与集合互动的方式。这一方法可行的原因是客户端代码并没有和具体迭代器类相 69 | // 耦合。 70 | class SocialSpammer is 71 | method send(iterator: ProfileIterator, message: string) is 72 | while (iterator.hasNext()) 73 | profile = iterator.getNext() 74 | System.sendEmail(profile.getEmail(), message) 75 | 76 | 77 | // 应用程序(Application)类可对集合和迭代器进行配置,然后将其传递给客户 78 | // 端代码。 79 | class Application is 80 | field network: SocialNetwork 81 | field spammer: SocialSpammer 82 | 83 | method config() is 84 | if working with WeChat 85 | this.network = new WeChat() 86 | if working with LinkedIn 87 | this.network = new LinkedIn() 88 | this.spammer = new SocialSpammer() 89 | 90 | method sendSpamToFriends(profile) is 91 | iterator = network.createFriendsIterator(profile.getId()) 92 | spammer.send(iterator, "非常重要的消息") 93 | 94 | method sendSpamToCoworkers(profile) is 95 | iterator = network.createCoworkersIterator(profile.getId()) 96 | spammer.send(iterator, "非常重要的消息") 97 | ``` 98 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/02~结构型模式/装饰.md: -------------------------------------------------------------------------------- 1 | # 装饰 2 | 3 | 装饰是一种结构型设计模式,允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。 4 | 5 | # 案例:数据加密 6 | 7 | ```ts 8 | // 装饰可以改变组件接口所定义的操作。 9 | interface DataSource is 10 | method writeData(data) 11 | method readData():data 12 | 13 | // 具体组件提供操作的默认实现。这些类在程序中可能会有几个变体。 14 | class FileDataSource implements DataSource is 15 | constructor FileDataSource(filename) { ... } 16 | 17 | method writeData(data) is 18 | // 将数据写入文件。 19 | 20 | method readData():data is 21 | // 从文件读取数据。 22 | 23 | // 装饰基类和其他组件遵循相同的接口。该类的主要任务是定义所有具体装饰的封 24 | // 装接口。封装的默认实现代码中可能会包含一个保存被封装组件的成员变量,并 25 | // 且负责对其进行初始化。 26 | class DataSourceDecorator implements DataSource is 27 | protected field wrappee: DataSource 28 | 29 | constructor DataSourceDecorator(source: DataSource) is 30 | wrappee = source 31 | 32 | // 装饰基类会直接将所有工作分派给被封装组件。具体装饰中则可新增额外行 33 | // 为。 34 | method writeData(data) is 35 | wrappee.writeData(data) 36 | 37 | // 具体装饰可调用其父类的操作实现,而不是直接调用被封装对象。这种方式 38 | // 可简化装饰类的扩展工作。 39 | method readData():data is 40 | return wrappee.readData() 41 | 42 | // 具体装饰必须在被封装对象上调用方法,不过也可以自行在结果中添加一些内容。 43 | // 装饰必须在调用封装对象之前或之后执行额外的行为。 44 | class EncryptionDecorator extends DataSourceDecorator is 45 | method writeData(data) is 46 | // 1. 对传递数据进行加密。 47 | // 2. 将加密后的数据传递给被封装对象的 writeData(写入数据)方 48 | // 法。 49 | 50 | method readData():data is 51 | // 1. 通过被封装对象的 readData(读取数据)方法获取数据。 52 | // 2. 如果数据被加密就尝试解密。 53 | // 3. 返回结果。 54 | 55 | // 你可以将对象封装在多层装饰中。 56 | class CompressionDecorator extends DataSourceDecorator is 57 | method writeData(data) is 58 | // 1. 压缩传递数据。 59 | // 2. 将压缩后的数据传递给被封装对象的 writeData(写入数据)方 60 | // 法。 61 | 62 | method readData():data is 63 | // 1. 通过被封装对象的 readData(读取数据)方法获取数据。 64 | // 2. 如果数据被压缩就尝试解压。 65 | // 3. 返回结果。 66 | 67 | 68 | // 选项 1:装饰组件的简单示例 69 | class Application is 70 | method dumbUsageExample() is 71 | source = new FileDataSource("somefile.dat") 72 | source.writeData(salaryRecords) 73 | // 已将明码数据写入目标文件。 74 | 75 | source = new CompressionDecorator(source) 76 | source.writeData(salaryRecords) 77 | // 已将压缩数据写入目标文件。 78 | 79 | source = new EncryptionDecorator(source) 80 | // 源变量中现在包含: 81 | // Encryption > Compression > FileDataSource 82 | source.writeData(salaryRecords) 83 | // 已将压缩且加密的数据写入目标文件。 84 | 85 | 86 | // 选项 2:客户端使用外部数据源。SalaryManager(工资管理器)对象并不关心 87 | // 数据如何存储。它们会与提前配置好的数据源进行交互,数据源则是通过程序配 88 | // 置器获取的。 89 | class SalaryManager is 90 | field source: DataSource 91 | 92 | constructor SalaryManager(source: DataSource) { ... } 93 | 94 | method load() is 95 | return source.readData() 96 | 97 | method save() is 98 | source.writeData(salaryRecords) 99 | // ...其他有用的方法... 100 | 101 | 102 | // 程序可在运行时根据配置或环境组装不同的装饰堆桟。 103 | class ApplicationConfigurator is 104 | method configurationExample() is 105 | source = new FileDataSource("salary.dat") 106 | if (enabledEncryption) 107 | source = new EncryptionDecorator(source) 108 | if (enabledCompression) 109 | source = new CompressionDecorator(source) 110 | 111 | logger = new SalaryManager(source) 112 | salary = logger.load() 113 | // ... 114 | ``` 115 | -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | - [1 01~编程范式 [5]](/01~编程范式/README.md) 2 | - [1.1 事件驱动编程](/01~编程范式/事件驱动编程/README.md) 3 | 4 | - [1.2 元编程](/01~编程范式/元编程/README.md) 5 | 6 | - [1.3 函数式编程 [2]](/01~编程范式/函数式编程/README.md) 7 | - [1.3.1 函数组合](/01~编程范式/函数式编程/函数组合.md) 8 | - [1.3.2 术语概念](/01~编程范式/函数式编程/术语概念.md) 9 | - 1.4 基础范式 [2] 10 | - [1.4.1 命令式编程](/01~编程范式/基础范式/命令式编程.md) 11 | - [1.4.2 声明式编程](/01~编程范式/基础范式/声明式编程.md) 12 | - [1.5 面向对象编程 [2]](/01~编程范式/面向对象编程/README.md) 13 | - [1.5.1 OOP 的缺陷](/01~编程范式/面向对象编程/OOP%20的缺陷.md) 14 | - [1.5.2 继承与组合](/01~编程范式/面向对象编程/继承与组合.md) 15 | - [2 02~面向对象的设计模式 [6]](/02~面向对象的设计模式/README.md) 16 | - [2.1 00~SOLID [6]](/02~面向对象的设计模式/00~SOLID/README.md) 17 | - [2.1.1 依赖倒置](/02~面向对象的设计模式/00~SOLID/依赖倒置.md) 18 | - [2.1.2 单一职责](/02~面向对象的设计模式/00~SOLID/单一职责.md) 19 | - [2.1.3 开放封闭](/02~面向对象的设计模式/00~SOLID/开放封闭.md) 20 | - [2.1.4 接口隔离](/02~面向对象的设计模式/00~SOLID/接口隔离.md) 21 | - [2.1.5 最少知识](/02~面向对象的设计模式/00~SOLID/最少知识.md) 22 | - [2.1.6 里氏替换](/02~面向对象的设计模式/00~SOLID/里氏替换.md) 23 | - [2.2 01~创建型模式 [5]](/02~面向对象的设计模式/01~创建型模式/README.md) 24 | - [2.2.1 单例](/02~面向对象的设计模式/01~创建型模式/单例.md) 25 | - [2.2.2 原型](/02~面向对象的设计模式/01~创建型模式/原型.md) 26 | - [2.2.3 工厂方法](/02~面向对象的设计模式/01~创建型模式/工厂方法.md) 27 | - [2.2.4 抽象工厂](/02~面向对象的设计模式/01~创建型模式/抽象工厂.md) 28 | - [2.2.5 构建器](/02~面向对象的设计模式/01~创建型模式/构建器.md) 29 | - 2.3 02~结构型模式 [7] 30 | - [2.3.1 享元](/02~面向对象的设计模式/02~结构型模式/享元.md) 31 | - [2.3.2 代理](/02~面向对象的设计模式/02~结构型模式/代理.md) 32 | - [2.3.3 外观](/02~面向对象的设计模式/02~结构型模式/外观.md) 33 | - [2.3.4 桥接](/02~面向对象的设计模式/02~结构型模式/桥接.md) 34 | - [2.3.5 组合](/02~面向对象的设计模式/02~结构型模式/组合.md) 35 | - [2.3.6 装饰](/02~面向对象的设计模式/02~结构型模式/装饰.md) 36 | - [2.3.7 适配器](/02~面向对象的设计模式/02~结构型模式/适配器.md) 37 | - [2.4 03~行为型模式 [10]](/02~面向对象的设计模式/03~行为型模式/README.md) 38 | - [2.4.1 中介者](/02~面向对象的设计模式/03~行为型模式/中介者.md) 39 | - [2.4.2 命令](/02~面向对象的设计模式/03~行为型模式/命令.md) 40 | - [2.4.3 备忘录](/02~面向对象的设计模式/03~行为型模式/备忘录.md) 41 | - [2.4.4 模板方法](/02~面向对象的设计模式/03~行为型模式/模板方法.md) 42 | - [2.4.5 状态](/02~面向对象的设计模式/03~行为型模式/状态.md) 43 | - [2.4.6 策略](/02~面向对象的设计模式/03~行为型模式/策略.md) 44 | - [2.4.7 职责链](/02~面向对象的设计模式/03~行为型模式/职责链.md) 45 | - [2.4.8 观察者](/02~面向对象的设计模式/03~行为型模式/观察者.md) 46 | - [2.4.9 访问者](/02~面向对象的设计模式/03~行为型模式/访问者/README.md) 47 | 48 | - [2.4.10 迭代器](/02~面向对象的设计模式/03~行为型模式/迭代器.md) 49 | - [2.5 04~其他模式](/02~面向对象的设计模式/04~其他模式/README.md) 50 | 51 | - 2.6 99~参考资料 [4] 52 | - [2.6.1 2015~《设计模式之禅》](/02~面向对象的设计模式/99~参考资料/2015~《设计模式之禅》/README.md) 53 | 54 | - 2.6.2 2017~《Design Patterns for Humans》 [3] 55 | - [2.6.2.1 cpp design patterns for humans](/02~面向对象的设计模式/99~参考资料/2017~《Design%20Patterns%20for%20Humans》/2017-cpp-design-patterns-for-humans.md) 56 | - [2.6.2.2 design patterns for humans](/02~面向对象的设计模式/99~参考资料/2017~《Design%20Patterns%20for%20Humans》/2017-design-patterns-for-humans.md) 57 | - [2.6.2.3 javascript design patterns for humans](/02~面向对象的设计模式/99~参考资料/2017~《Design%20Patterns%20for%20Humans》/2017-javascript-design-patterns-for-humans.md) 58 | - [2.6.3 《Refactoring Guru》](/02~面向对象的设计模式/99~参考资料/《Refactoring%20Guru》/README.md) 59 | 60 | - [2.6.4 李兴华~《研磨设计模式》](/02~面向对象的设计模式/99~参考资料/李兴华~《研磨设计模式》/README.md) 61 | 62 | - 3 03~CleanCode [1] 63 | - 3.1 99~参考资料 [1] 64 | - [3.1.1 Clean Code Notes](/03~CleanCode/99~参考资料/Clean%20Code%20Notes.md) 65 | - [4 INTRODUCTION](/INTRODUCTION.md) -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/状态.md: -------------------------------------------------------------------------------- 1 | # 状态 2 | 3 | 状态是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。 4 | 5 | # 案例:媒体播放器 6 | 7 | ```ts 8 | // 音频播放器(AudioPlayer)类即为上下文。它还会维护指向状态类实例的引用, 9 | // 该状态类则用于表示音频播放器当前的状态。 10 | class AudioPlayer is 11 | field state: State 12 | field UI, volume, playlist, currentSong 13 | 14 | constructor AudioPlayer() is 15 | this.state = new ReadyState(this) 16 | 17 | // 上下文将处理用户输入的工作委派给状态对象。由于每个状态都以不同 18 | // 的方式处理输入,其结果自然将依赖于当前所处的状态。 19 | UI = new UserInterface() 20 | UI.lockButton.onClick(this.clickLock) 21 | UI.playButton.onClick(this.clickPlay) 22 | UI.nextButton.onClick(this.clickNext) 23 | UI.prevButton.onClick(this.clickPrevious) 24 | 25 | // 其他对象必须能切换音频播放器当前所处的状态。 26 | method changeState(state: State) is 27 | this.state = state 28 | 29 | // UI 方法将执行工作委派给当前状态。 30 | method clickLock() is 31 | state.clickLock() 32 | method clickPlay() is 33 | state.clickPlay() 34 | method clickNext() is 35 | state.clickNext() 36 | method clickPrevious() is 37 | state.clickPrevious() 38 | 39 | // 状态可调用上下文的一些服务方法。 40 | method startPlayback() is 41 | // ... 42 | method stopPlayback() is 43 | // ... 44 | method nextSong() is 45 | // ... 46 | method previousSong() is 47 | // ... 48 | method fastForward(time) is 49 | // ... 50 | method rewind(time) is 51 | // ... 52 | 53 | 54 | // 所有具体状态类都必须实现状态基类声明的方法,并提供反向引用指向与状态相 55 | // 关的上下文对象。状态可使用反向引用将上下文转换为另一个状态。 56 | abstract class State is 57 | protected field player: AudioPlayer 58 | 59 | // 上下文将自身传递给状态构造函数。这可帮助状态在需要时获取一些有用的 60 | // 上下文数据。 61 | constructor State(player) is 62 | this.player = player 63 | 64 | abstract method clickLock() 65 | abstract method clickPlay() 66 | abstract method clickNext() 67 | abstract method clickPrevious() 68 | 69 | 70 | // 具体状态会实现与上下文状态相关的多种行为。 71 | class LockedState extends State is 72 | 73 | // 当你解锁一个锁定的播放器时,它可能处于两种状态之一。 74 | method clickLock() is 75 | if (player.playing) 76 | player.changeState(new PlayingState(player)) 77 | else 78 | player.changeState(new ReadyState(player)) 79 | 80 | method clickPlay() is 81 | // 已锁定,什么也不做。 82 | 83 | method clickNext() is 84 | // 已锁定,什么也不做。 85 | 86 | method clickPrevious() is 87 | // 已锁定,什么也不做。 88 | 89 | 90 | // 它们还可在上下文中触发状态转换。 91 | class ReadyState extends State is 92 | method clickLock() is 93 | player.changeState(new LockedState(player)) 94 | 95 | method clickPlay() is 96 | player.startPlayback() 97 | player.changeState(new PlayingState(player)) 98 | 99 | method clickNext() is 100 | player.nextSong() 101 | 102 | method clickPrevious() is 103 | player.previousSong() 104 | 105 | 106 | class PlayingState extends State is 107 | method clickLock() is 108 | player.changeState(new LockedState(player)) 109 | 110 | method clickPlay() is 111 | player.stopPlayback() 112 | player.changeState(new ReadyState(player)) 113 | 114 | method clickNext() is 115 | if (event.doubleclick) 116 | player.nextSong() 117 | else 118 | player.fastForward(5) 119 | 120 | method clickPrevious() is 121 | if (event.doubleclick) 122 | player.previous() 123 | else 124 | player.rewind(5) 125 | ``` 126 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/02~结构型模式/代理.md: -------------------------------------------------------------------------------- 1 | # 代理 2 | 3 | 代理是一种结构型设计模式,让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。 4 | 5 | 在所有种类的代理模式中,虚拟(Virtual)代理、远程(Remote)代理、智能引用代理(Smart Reference Proxy)和保护(Protect or Access)代理是最为常见的代理模式。 6 | 7 | ![](http://images.cnitblog.com/blog/159936/201307/08121626-9d4dea10762a482f8813a4df931f4000.png) 8 | 9 | Subject:声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题。 10 | 11 | Proxy:代理主题角色内部含有对真是主题的引用,从而可以在任何时候操作真实主题对象;代理主题角色提供一个与真实主题角色相同的接口,以便可以在任何时候都可以替代真实主体;控制真实主题的应用,负责在需要的时候创建真实主题对象(和删除真实主题对象);代理角色通常在将客户端调用传递给真实的主题之前或之后,都要执行某个操作,而不是单纯的将调用传递给真实主题对象。 12 | 13 | ConcreteSubject:定义了代理角色所代表的真实对象。 14 | 15 | # 问题分析 16 | 17 | ## 优劣对比 18 | 19 | ## 实现方式 20 | 21 | 为其他对象提供一种代理以控制对这个对象的访问。如果按照使用目的来划分,代理有以下几种: 22 | 23 | - 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个局域代表对象。这个不同的地址空间可以是在本机器中,也可是在另一台机器中。远程代理又叫做大使(Ambassador)。也就是为一个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实。 24 | 25 | - 虚拟(Virtual)代理:根据需要创建一个资源消耗较大的对象,使得此对象只在需要时才会被真正创建。是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象。 26 | 27 | - Copy-on-Write 代理:虚拟代理的一种。把复制(克隆)拖延到只有在客户端需要时,才真正采取行动。 28 | 29 | - 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候。 30 | 31 | - Cache 代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。防火墙(Firewall)代理:保护目标,不让恶意用户接近。同步化(Synchronization)代理:使几个用户能够同时使用一个对象而没有冲突。 32 | 33 | - 智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,比如将对此对象调用的次数记录下来等。是指当调用真实的对象时,代理处理另外一些事。如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它;或当第一次引用一个持久对象时,将它装入内存;或在访问一个实际对象前,检查是否已经锁定它,以确保其他对象不能改变它。它们都是通过代理在访问一个对象时附加一些内务处理。 34 | 35 | # 案例:视频缓存 36 | 37 | ```ts 38 | // 远程服务接口。 39 | interface ThirdPartyTVLib is 40 | method listVideos() 41 | method getVideoInfo(id) 42 | method downloadVideo(id) 43 | 44 | // 服务连接器的具体实现。该类的方法可以向腾讯视频请求信息。请求速度取决于 45 | // 用户和腾讯视频的互联网连接情况。如果同时发送大量请求,即使所请求的信息 46 | // 一模一样,程序的速度依然会减慢。 47 | class ThirdPartyTVClass implements ThirdPartyTVLib is 48 | method listVideos() is 49 | // 向腾讯视频发送一个 API 请求。 50 | 51 | method getVideoInfo(id) is 52 | // 获取某个视频的元数据。 53 | 54 | method downloadVideo(id) is 55 | // 从腾讯视频下载一个视频文件。 56 | 57 | // 为了节省网络带宽,我们可以将请求结果缓存下来并保存一段时间。但你可能无 58 | // 法直接将这些代码放入服务类中。比如该类可能是第三方程序库的一部分或其签 59 | // 名是`final`(最终)。因此我们会在一个实现了服务类接口的新代理类中放入 60 | // 缓存代码。当代理类接收到真实请求后,才会将其委派给服务对象。 61 | class CachedTVClass implements ThirdPartyTVLib is 62 | private field service: ThirdPartyTVClass 63 | private field listCache, videoCache 64 | field needReset 65 | 66 | constructor CachedTVClass(service: ThirdPartyTVLib) is 67 | this.service = service 68 | 69 | method listVideos() is 70 | if (listCache == null || needReset) 71 | listCache = service.listVideos() 72 | return listCache 73 | 74 | method getVideoInfo(id) is 75 | if (videoCache == null || needReset) 76 | videoCache = service.getVideoInfo(id) 77 | return videoCache 78 | 79 | method downloadVideo(id) is 80 | if (!downloadExists(id) || needReset) 81 | service.downloadVideo(id) 82 | 83 | // 之前直接与服务对象交互的 GUI 类不需要改变,前提是它仅通过接口与服务对 84 | // 象交互。我们可以安全地传递一个代理对象来代替真实服务对象,因为它们都实 85 | // 现了相同的接口。 86 | class TVManager is 87 | protected field service: ThirdPartyTVLib 88 | 89 | constructor TVManager(service: ThirdPartyTVLib) is 90 | this.service = service 91 | 92 | method renderVideoPage(id) is 93 | info = service.getVideoInfo(id) 94 | // 渲染视频页面。 95 | 96 | method renderListPanel() is 97 | list = service.listVideos() 98 | // 渲染视频缩略图列表。 99 | 100 | method reactOnUserInput() is 101 | renderVideoPage() 102 | renderListPanel() 103 | 104 | // 程序可在运行时对代理进行配置。 105 | class Application is 106 | method init() is 107 | aTVService = new ThirdPartyTVClass() 108 | aTVProxy = new CachedTVClass(aTVService) 109 | manager = new TVManager(aTVProxy) 110 | manager.reactOnUserInput() 111 | ``` 112 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/01~创建型模式/构建器.md: -------------------------------------------------------------------------------- 1 | # 构建器 2 | 3 | 构建器是一种创建型设计模式,使你能够分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。 4 | 5 | 构建对象时,如果碰到类有很多参数——其中很多参数类型相同而且很多参数可以为空时,我更喜欢 Builder 模式来完成。当参数数量不多、类型不同而且都是必须出现时,通过增加代码实现 Builder 往往无法体现它的优势。在这种情况下,理想的方法是调用传统的构造函数。再者,如果不需要保持不变,那么就使用无参构造函数调用相应的 set 方法吧。 6 | 7 | - 如果类的构造器或静态工厂中有多个参数,设计这样类时,最好使用 Builder 模式,特别是当大多数参数都是可选的时候。 8 | 9 | - 如果现在不能确定参数的个数,最好一开始就使用构建器即 Builder 模式。 10 | 11 | # 案例:汽车制造 12 | 13 | ![Car 案例](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230430223242.png) 14 | 15 | ```ts 16 | / 只有当产品较为复杂且需要详细配置时,使用生成器模式才有意义。下面的两个 17 | // 产品尽管没有同样的接口,但却相互关联。 18 | class Car is 19 | // 一辆汽车可能配备有 GPS 设备、行车电脑和几个座位。不同型号的汽车( 20 | // 运动型轿车、SUV 和敞篷车)可能会安装或启用不同的功能。 21 | 22 | class Manual is 23 | // 用户使用手册应该根据汽车配置进行编制,并介绍汽车的所有功能。 24 | 25 | 26 | // 生成器接口声明了创建产品对象不同部件的方法。 27 | interface Builder is 28 | method reset() 29 | method setSeats(...) 30 | method setEngine(...) 31 | method setTripComputer(...) 32 | method setGPS(...) 33 | 34 | // 具体生成器类将遵循生成器接口并提供生成步骤的具体实现。你的程序中可能会 35 | // 有多个以不同方式实现的生成器变体。 36 | class CarBuilder implements Builder is 37 | private field car:Car 38 | 39 | // 一个新的生成器实例必须包含一个在后续组装过程中使用的空产品对象。 40 | constructor CarBuilder() is 41 | this.reset() 42 | 43 | // 重置(reset)方法可清除正在生成的对象。 44 | method reset() is 45 | this.car = new Car() 46 | 47 | // 所有生成步骤都会与同一个产品实例进行交互。 48 | method setSeats(...) is 49 | // 设置汽车座位的数量。 50 | 51 | method setEngine(...) is 52 | // 安装指定的引擎。 53 | 54 | method setTripComputer(...) is 55 | // 安装行车电脑。 56 | 57 | method setGPS(...) is 58 | // 安装全球定位系统。 59 | 60 | // 具体生成器需要自行提供获取结果的方法。这是因为不同类型的生成器可能 61 | // 会创建不遵循相同接口的、完全不同的产品。所以也就无法在生成器接口中 62 | // 声明这些方法(至少在静态类型的编程语言中是这样的)。 63 | // 64 | // 通常在生成器实例将结果返回给客户端后,它们应该做好生成另一个产品的 65 | // 准备。因此生成器实例通常会在 `getProduct`(获取产品)方法主体末尾 66 | // 调用重置方法。但是该行为并不是必需的,你也可让生成器等待客户端明确 67 | // 调用重置方法后再去处理之前的结果。 68 | method getProduct():Car is 69 | product = this.car 70 | this.reset() 71 | return product 72 | 73 | // 生成器与其他创建型模式的不同之处在于:它让你能创建不遵循相同接口的产品。 74 | class CarManualBuilder implements Builder is 75 | private field manual:Manual 76 | 77 | constructor CarManualBuilder() is 78 | this.reset() 79 | 80 | method reset() is 81 | this.manual = new Manual() 82 | 83 | method setSeats(...) is 84 | // 添加关于汽车座椅功能的文档。 85 | 86 | method setEngine(...) is 87 | // 添加关于引擎的介绍。 88 | 89 | method setTripComputer(...) is 90 | // 添加关于行车电脑的介绍。 91 | 92 | method setGPS(...) is 93 | // 添加关于 GPS 的介绍。 94 | 95 | method getProduct():Manual is 96 | // 返回使用手册并重置生成器。 97 | 98 | 99 | // 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时 100 | // 会很有帮助。由于客户端可以直接控制生成器,所以严格来说主管类并不是必需 101 | // 的。 102 | class Director is 103 | private field builder:Builder 104 | 105 | // 主管可同由客户端代码传递给自身的任何生成器实例进行交互。客户端可通 106 | // 过这种方式改变最新组装完毕的产品的最终类型。 107 | method setBuilder(builder:Builder) 108 | this.builder = builder 109 | 110 | // 主管可使用同样的生成步骤创建多个产品变体。 111 | method constructSportsCar(builder: Builder) is 112 | builder.reset() 113 | builder.setSeats(2) 114 | builder.setEngine(new SportEngine()) 115 | builder.setTripComputer(true) 116 | builder.setGPS(true) 117 | 118 | method constructSUV(builder: Builder) is 119 | // ... 120 | 121 | 122 | // 客户端代码会创建生成器对象并将其传递给主管,然后执行构造过程。最终结果 123 | // 将需要从生成器对象中获取。 124 | class Application is 125 | 126 | method makeCar() is 127 | director = new Director() 128 | 129 | CarBuilder builder = new CarBuilder() 130 | director.constructSportsCar(builder) 131 | Car car = builder.getProduct() 132 | 133 | CarManualBuilder builder = new CarManualBuilder() 134 | director.constructSportsCar(builder) 135 | 136 | // 最终产品通常需要从生成器对象中获取,因为主管不知晓具体生成器和 137 | // 产品的存在,也不会对其产生依赖。 138 | Manual manual = builder.getProduct() 139 | ``` 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://cdn-images-1.medium.com/max/2000/1*Vv0HNvRhU0ihKVaBIpDUww.jpeg) 2 | 3 | [![Contributors][contributors-shield]][contributors-url] 4 | [![Forks][forks-shield]][forks-url] 5 | [![Stargazers][stars-shield]][stars-url] 6 | [![Issues][issues-shield]][issues-url] 7 | [![license: CC BY-NC-SA 4.0](https://img.shields.io/badge/license-CC%20BY--NC--SA%204.0-lightgrey.svg)][license-url] 8 | 9 | 10 |
11 |

12 | 13 | Logo 14 | 15 | 16 |

编程范式与设计模式

17 | 18 |

19 | SOLID、经典设计模式、函数式编程 20 |
21 | 在线阅读 >> 22 |
23 |
24 | 速览手册 25 | · 26 | Report Bug 27 | · 28 | 参考资料 29 |

30 |

31 | 32 | 33 | 34 | # Introduction | 前言 35 | 36 | 本篇的所有参考代码归纳在 [design-pattern-snippets](https://github.com/wx-chevalier/design-pattern-snippets) 仓库中。 37 | 38 | ![mindmap](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230418222449.png) 39 | 40 | # Nav | 关联导航 41 | 42 | 本书的精排目录导航版请参考 [https://ng-tech.icu/books/DesignPattern-Notes](https://ng-tech.icu/books/DesignPattern-Notes)。 43 | 44 | # About | 关于 45 | 46 | 47 | 48 | ## Contributing 49 | 50 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 51 | 52 | 1. Fork the Project 53 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 54 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 55 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 56 | 5. Open a Pull Request 57 | 58 | 59 | 60 | ## Acknowledgements 61 | 62 | - [Source Making](https://sourcemaking.com/design_patterns/) 63 | 64 | - [Awesome-Lists](https://github.com/wx-chevalier/Awesome-Lists): 📚 Guide to Galaxy, curated, worthy and up-to-date links/reading list for ITCS-Coding/Algorithm/SoftwareArchitecture/AI. 💫 ITCS-编程/算法/软件架构/人工智能等领域的文章/书籍/资料/项目链接精选。 65 | 66 | - [Awesome-CS-Books](https://github.com/wx-chevalier/Awesome-CS-Books): :books: Awesome CS Books/Series(.pdf by git lfs) Warehouse for Geeks, ProgrammingLanguage, SoftwareEngineering, Web, AI, ServerSideApplication, Infrastructure, FE etc. :dizzy: 优秀计算机科学与技术领域相关的书籍归档。 67 | 68 | ## Copyright & More | 延伸阅读 69 | 70 | 笔者所有文章遵循[知识共享 署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh),欢迎转载,尊重版权。您还可以前往 [NGTE Books](https://ng-tech.icu/books-gallery/) 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表: 71 | 72 | [![NGTE Books](https://s2.ax1x.com/2020/01/18/19uXtI.png)](https://ng-tech.icu/books-gallery/) 73 | 74 | 75 | 76 | 77 | [contributors-shield]: https://img.shields.io/github/contributors/wx-chevalier/DesignPattern-Notes.svg?style=flat-square 78 | [contributors-url]: https://github.com/wx-chevalier/DesignPattern-Notes/graphs/contributors 79 | [forks-shield]: https://img.shields.io/github/forks/wx-chevalier/DesignPattern-Notes.svg?style=flat-square 80 | [forks-url]: https://github.com/wx-chevalier/DesignPattern-Notes/network/members 81 | [stars-shield]: https://img.shields.io/github/stars/wx-chevalier/DesignPattern-Notes.svg?style=flat-square 82 | [stars-url]: https://github.com/wx-chevalier/DesignPattern-Notes/stargazers 83 | [issues-shield]: https://img.shields.io/github/issues/wx-chevalier/DesignPattern-Notes.svg?style=flat-square 84 | [issues-url]: https://github.com/wx-chevalier/DesignPattern-Notes/issues 85 | [license-shield]: https://img.shields.io/github/license/wx-chevalier/DesignPattern-Notes.svg?style=flat-square 86 | [license-url]: https://github.com/wx-chevalier/DesignPattern-Notes/blob/master/LICENSE.txt 87 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/01~创建型模式/工厂方法.md: -------------------------------------------------------------------------------- 1 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522114706.png) 2 | 3 | # Factory Method | 工厂方法 4 | 5 | 工厂方法是一种创建型设计模式,其在父类中提供一个创建对象的接口,允许子类决定实例化对象的类型,即子类可以修改工厂方法返回的对象类型。工厂方法模式建议使用特殊的工厂方法代替对于对象构造函数的直接调用(即使用 new 运算符)。对象仍将通过 new 运算符创建,只是该运算符改在工厂方法中调用罢了;工厂方法返回的对象通常被称作“产品”。 6 | 7 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522150336.png) 8 | 9 | 但有一点需要注意:仅当这些产品具有共同的基类或者接口时,子类才能返回不同类型的产品,同时基类中的工厂方法还应将其返回类型声明为这一共有接口。 10 | 11 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522150313.png) 12 | 13 | 譬如上例中,卡车(Truck)和轮船(Ship)类都必须实现运输(Transport)接口,该接口声明了一个名为交付(deliver)的方法。每个类都将以不同的方式实现该方法:卡车走陆路交付货物,轮船走海路交付货物。陆路运输(Road­Logistics)类中的工厂方法返回卡车对象,而海路运输(Sea­Logistics)类则返回轮船对象。 14 | 15 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522150237.png) 16 | 17 | ## 优劣对比 18 | 19 | 工厂方法可以避免创建者和具体产品之间的紧密耦合,将产品创建代码放在程序的单一位置,从而使得代码更容易维护;并且无需更改现有客户端代码,你就可以在程序中引入新的产品类型。不过应用该模式需要引入许多新的子类,代码可能会因此变得更复杂。最好的情况是将该模式引入创建者类的现有层次结构中。 20 | 21 | 总结而言,工厂方法适用于以下场景: 22 | 23 | - 在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。工厂方法将创建产品的代码与实际使用产品的代码分离,从而能在不影响其他代码的情况下扩展产品创建部分代码。例如,如果需要向应用中添加一种新产品,你只需要开发新的创建者子类,然后重写其工厂方法即可。 24 | 25 | - 希望用户能扩展你软件库或框架的内部组件,可使用工厂方法。将各框架中构造组件的代码集中到单个工厂方法中,并在继承该组件之外允许任何人对该方法进行重写。 26 | 27 | - 复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法。在处理大型资源密集型对象(比如数据库连接、文件系统和网络资源)时,需要有一个既能够创建新对象,又可以重用现有对象的普通方法。 28 | 29 | ## 实现方式 30 | 31 | 产品(Product)将会对接口进行声明。对于所有由创建者及其子类构建的对象,这些接口都是通用的。具体产品(Concrete Products)是产品接口的不同实现。创建者(Creator)类声明返回产品对象的工厂方法。该方法的返回对象类型必须与产品接口相匹配。 32 | 33 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522150217.png) 34 | 35 | - 首先让所有产品都遵循同一接口,该接口必须声明对所有产品都有意义的方法。 36 | 37 | - 然后在创建类中添加一个空的工厂方法。该方法的返回类型必须遵循通用的产品接口。 38 | 39 | - 在创建者代码中找到对于产品构造函数的所有引用。将它们依次替换为对于工厂方法的调用,同时将创建产品的代码移入工厂方法。 40 | 41 | - 最后,为工厂方法中的每种产品编写一个创建者子类,然后在子类中重写工厂方法,并将基本方法中的相关创建代码移动到工厂方法中。如果应用中的产品类型太多,那么为每个产品创建子类并无太大必要,这时你也可以在子类中复用基类中的控制参数。 42 | 43 | 你可以将工厂方法声明为抽象方法,强制要求每个子类以不同方式实现该方法。或者,你也可以在基础工厂方法中返回默认产品类型。注意,尽管它的名字是创建者,但他最主要的职责并不是创建产品。一般来说,创建者类包含一些与产品相关的核心业务逻辑。工厂方法将这些逻辑处理从具体产品类中分离出来。打个比方,大型软件开发公司拥有程序员培训部门。但是,这些公司的主要工作还是编写代码,而非生产程序员。 44 | 45 | 具体创建者(Concrete Creators)将会重写基础工厂方法,使其返回不同类型的产品。注意,并不一定每次调用工厂方法都会创建新的实例。工厂方法也可以返回缓存、对象池或其他来源的已有对象。 46 | 47 | # 案例:跨平台组件 48 | 49 | 以下示例演示了如何使用工厂方法开发跨平台 UI(用户界面)组件,并同时避免客户代码与具体 UI 类之间的耦合。 50 | 51 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230522114643.png) 52 | 53 | 基础对话框类使用不同的 UI 组件渲染窗口。在不操作系统下,这些组件外观或许略有不同,但其功能保持一致。Windows 系统中的按钮在 Linux 系统中仍然是按钮。 54 | 55 | 如果使用工厂方法,就不需要为每种操作系统重写对话框逻辑。如果我们声明了一个在基本对话框类中生成按钮的工厂方法,那么我们就可以创建一个对话框子类,并使其通过工厂方法返回 Windows 样式按钮。子类将继承对话框基础类的大部分代码,同时在屏幕上根据 Windows 样式渲染按钮。 56 | 57 | 如需该模式正常工作,基础对话框类必须使用抽象按钮(例如基类或接口),以便将其扩展为具体按钮。这样一来,无论对话框中使用何种类型的按钮,其代码都可以正常工作。 58 | 59 | ```ts 60 | // 创建者类声明的工厂方法必须返回一个产品类的对象。创建者的子类通常会提供 61 | // 该方法的实现。 62 | class Dialog is 63 | // 创建者还可提供一些工厂方法的默认实现。 64 | abstract method createButton() 65 | 66 | // 请注意,创建者的主要职责并非是创建产品。其中通常会包含一些核心业务 67 | // 逻辑,这些逻辑依赖于由工厂方法返回的产品对象。子类可通过重写工厂方 68 | // 法并使其返回不同类型的产品来间接修改业务逻辑。 69 | method render() is 70 | // 调用工厂方法创建一个产品对象。 71 | Button okButton = createButton() 72 | // 现在使用产品。 73 | okButton.onClick(closeDialog) 74 | okButton.render() 75 | 76 | 77 | // 具体创建者将重写工厂方法以改变其所返回的产品类型。 78 | class WindowsDialog extends Dialog is 79 | method createButton() is 80 | return new WindowsButton() 81 | 82 | class WebDialog extends Dialog is 83 | method createButton() is 84 | return new HTMLButton() 85 | 86 | 87 | // 产品接口中将声明所有具体产品都必须实现的操作。 88 | interface Button is 89 | method render() 90 | method onClick(f) 91 | 92 | // 具体产品需提供产品接口的各种实现。 93 | class WindowsButton implements Button is 94 | method render(a, b) is 95 | // 根据 Windows 样式渲染按钮。 96 | method onClick(f) is 97 | // 绑定本地操作系统点击事件。 98 | 99 | class HTMLButton implements Button is 100 | method render(a, b) is 101 | // 返回一个按钮的 HTML 表述。 102 | method onClick(f) is 103 | // 绑定网络浏览器的点击事件。 104 | 105 | 106 | class Application is 107 | field dialog: Dialog 108 | 109 | // 程序根据当前配置或环境设定选择创建者的类型。 110 | method initialize() is 111 | config = readApplicationConfigFile() 112 | 113 | if (config.OS == "Windows") then 114 | dialog = new WindowsDialog() 115 | else if (config.OS == "Web") then 116 | dialog = new WebDialog() 117 | else 118 | throw new Exception("错误!未知的操作系统。") 119 | 120 | // 当前客户端代码会与具体创建者的实例进行交互,但是必须通过其基本接口 121 | // 进行。只要客户端通过基本接口与创建者进行交互,你就可将任何创建者子 122 | // 类传递给客户端。 123 | method main() is 124 | this.initialize() 125 | dialog.render() 126 | ``` 127 | 128 | # 工厂模式比较 129 | 130 | 简单工厂/静态工厂,工厂方法,抽象工厂都属于设计模式中的创建型模式。其主要功能都是帮助我们把对象的实例化部分抽取了出来,优化了系统的架构,并且增强了系统的扩展性。 131 | 132 | - 简单工厂模式对具体产品的创建类别和创建时机的判断是混和在一起的,不能形成简单工厂模式的继承结构,不符合开放封闭原则。 133 | 134 | - 在工厂方法模式中,对于存在继承等级结构的产品树,产品的创建是通过相应等级结构的工厂创建的。 135 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/03~行为型模式/命令.md: -------------------------------------------------------------------------------- 1 | # 命令 2 | 3 | 命令是一种行为设计模式,它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。 4 | 5 | # 案例:编辑器 6 | 7 | ```ts 8 | // The base command class defines the common interface for all 9 | // concrete commands. 10 | abstract class Command is 11 | protected field app: Application 12 | protected field editor: Editor 13 | protected field backup: text 14 | 15 | constructor Command(app: Application, editor: Editor) is 16 | this.app = app 17 | this.editor = editor 18 | 19 | // Make a backup of the editor's state. 20 | method saveBackup() is 21 | backup = editor.text 22 | 23 | // Restore the editor's state. 24 | method undo() is 25 | editor.text = backup 26 | 27 | // The execution method is declared abstract to force all 28 | // concrete commands to provide their own implementations. 29 | // The method must return true or false depending on whether 30 | // the command changes the editor's state. 31 | abstract method execute() 32 | 33 | 34 | // The concrete commands go here. 35 | class CopyCommand extends Command is 36 | // The copy command isn't saved to the history since it 37 | // doesn't change the editor's state. 38 | method execute() is 39 | app.clipboard = editor.getSelection() 40 | return false 41 | 42 | class CutCommand extends Command is 43 | // The cut command does change the editor's state, therefore 44 | // it must be saved to the history. And it'll be saved as 45 | // long as the method returns true. 46 | method execute() is 47 | saveBackup() 48 | app.clipboard = editor.getSelection() 49 | editor.deleteSelection() 50 | return true 51 | 52 | class PasteCommand extends Command is 53 | method execute() is 54 | saveBackup() 55 | editor.replaceSelection(app.clipboard) 56 | return true 57 | 58 | // The undo operation is also a command. 59 | class UndoCommand extends Command is 60 | method execute() is 61 | app.undo() 62 | return false 63 | 64 | 65 | // The global command history is just a stack. 66 | class CommandHistory is 67 | private field history: array of Command 68 | 69 | // Last in... 70 | method push(c: Command) is 71 | // Push the command to the end of the history array. 72 | 73 | // ...first out 74 | method pop():Command is 75 | // Get the most recent command from the history. 76 | 77 | 78 | // The editor class has actual text editing operations. It plays 79 | // the role of a receiver: all commands end up delegating 80 | // execution to the editor's methods. 81 | class Editor is 82 | field text: string 83 | 84 | method getSelection() is 85 | // Return selected text. 86 | 87 | method deleteSelection() is 88 | // Delete selected text. 89 | 90 | method replaceSelection(text) is 91 | // Insert the clipboard's contents at the current 92 | // position. 93 | 94 | 95 | // The application class sets up object relations. It acts as a 96 | // sender: when something needs to be done, it creates a command 97 | // object and executes it. 98 | class Application is 99 | field clipboard: string 100 | field editors: array of Editors 101 | field activeEditor: Editor 102 | field history: CommandHistory 103 | 104 | // The code which assigns commands to UI objects may look 105 | // like this. 106 | method createUI() is 107 | // ... 108 | copy = function() { executeCommand( 109 | new CopyCommand(this, activeEditor)) } 110 | copyButton.setCommand(copy) 111 | shortcuts.onKeyPress("Ctrl+C", copy) 112 | 113 | cut = function() { executeCommand( 114 | new CutCommand(this, activeEditor)) } 115 | cutButton.setCommand(cut) 116 | shortcuts.onKeyPress("Ctrl+X", cut) 117 | 118 | paste = function() { executeCommand( 119 | new PasteCommand(this, activeEditor)) } 120 | pasteButton.setCommand(paste) 121 | shortcuts.onKeyPress("Ctrl+V", paste) 122 | 123 | undo = function() { executeCommand( 124 | new UndoCommand(this, activeEditor)) } 125 | undoButton.setCommand(undo) 126 | shortcuts.onKeyPress("Ctrl+Z", undo) 127 | 128 | // Execute a command and check whether it has to be added to 129 | // the history. 130 | method executeCommand(command) is 131 | if (command.execute) 132 | history.push(command) 133 | 134 | // Take the most recent command from the history and run its 135 | // undo method. Note that we don't know the class of that 136 | // command. But we don't have to, since the command knows 137 | // how to undo its own action. 138 | method undo() is 139 | command = history.pop() 140 | if (command != null) 141 | command.undo() 142 | ``` 143 | -------------------------------------------------------------------------------- /02~面向对象的设计模式/01~创建型模式/抽象工厂.md: -------------------------------------------------------------------------------- 1 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230525124936.png) 2 | 3 | # 抽象工厂 4 | 5 | 抽象工厂是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式来生成这些类的方法。 6 | 7 | # 问题分析 8 | 9 | 譬如某家具店有一系列相关产品,例如椅子(Chair)、沙发(Sofa)和咖啡桌(Coffee­Table);系列产品的不同变体。例如,你可以使用现代(Mordern)、维多利亚(Victorian),装饰风艺术(Art­Deco)等风格生成椅子、沙发和咖啡桌。我们需要根据用户的场景动态地为他们构建不同的产品。 10 | 11 | 抽象工厂模式建议为系列中的每件产品明确声明接口(例如椅子、沙发或咖啡桌)。然后,确保所有产品变体都继承这些接口。例如,所有风格的椅子都实现椅子接口;所有风格的咖啡桌都实现咖啡桌接口,以此类推。接下来,我们需要声明抽象工厂,包含系列中所有产品构造方法的接口。例如创建椅子(create­Chair)、创建沙发(create­Sofa)和创建咖啡桌(create­Coffee­Table)。这些方法必须返回抽象产品类型,即我们之前抽取的那些接口:椅子,沙发和咖啡桌等等。 12 | 13 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230525162053.png) 14 | 15 | 对于系列产品的每个变体,我们都将基于抽象工厂接口创建不同的工厂类。每个工厂类都只能返回特定类别的产品,例如,现代家具工厂(Modern­Furniture­Factory)只能创建现代椅子(Mordern­Chair)、现代沙发(Modern­Sofa)和现代咖啡桌(Modern­Coffee­Table)对象。客户端代码可以通过相应的抽象接口调用工厂和产品类。你无需修改实际客户端代码,就能更改传递给客户端的工厂类,也能更改客户端代码接收的产品变体。 16 | 17 | ## 优劣对比 18 | 19 | 如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望代码基于产品的具体类进行构建,在这种情况下,你可以使用抽象工厂。 20 | 21 | 抽象工厂为你提供了一个接口,可用于创建每个系列产品的对象。只要代码通过该接口创建对象,那么你就不会生成与应用程序已生成的产品类型不一致的产品。如果你有一个基于一组抽象方法的类,且其主要功能因此变得不明确,那么在这种情况下,你可以考虑使用抽象工厂。在设计良好的程序中,如果一个类与多种类型产品交互,就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。 22 | 23 | ## 实现方式 24 | 25 | 抽象产品(Abstract Product)为构成系列产品的一组不同但相关的产品声明接口。具体产品(Concrete Product)是抽象产品的多种不同类型实现。所有变体(维多利亚/现代)都必须实现相应的抽象产品(椅子/沙发)。抽象工厂(Abstract Factory)接口声明了一组创建各种抽象产品的方法。具体工厂(Concrete Factory)实现抽象工厂的构建方法。每个具体工厂都对应特定的产品变体,并且仅创建此种产品变体。 26 | 27 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230525162012.png) 28 | 29 | - 首先以不同的产品类型与产品变体为维度绘制矩阵,为所有产品声明抽象产品接口,然后让所有具体产品类实现这些接口。 30 | 31 | - 然后声明抽象工厂接口,并且在接口中为所有抽象产品提供一组构造方法。 32 | 33 | - 接下来为每种产品变体实现一个具体工厂类,在应用程序中开发初始化代码。该代码根据应用程序配置或当前环境,对特定具体工厂类进行初始化。 34 | 35 | - 最后将该工厂对象传递给所有需要创建产品的类。找出代码中所有对产品构造方法的直接调用,将其替换为对工厂对象中相应构造方法的调用。 36 | 37 | 尽管具体工厂会对具体产品进行初始化,其构造函数签名必须返回相应的 抽象 产品。这样,使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。客户端(Client)只需通过抽象接口调用工厂和产品对象,就能与任何具体工厂/产品变体交互。 38 | 39 | # 案例:跨平台组件 40 | 41 | 通过应用抽象工厂模式,使得客户端代码无需与具体 UI 类耦合,就能创建跨平台的 UI 元素,同时确保所创建的元素与指定的操作系统匹配。 42 | 43 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230523232811.png) 44 | 45 | 跨平台应用中的相同 UI 元素功能类似,但是在不同操作系统下的外观有一定差异。此外,你需要确保 UI 元素与当前操作系统风格一致。你一定不希望在 Windows 系统下运行的应用程序中显示 macOS 的控件。抽象工厂接口声明一系列构造函数,客户端代码可调用它们生成不同风格的 UI 元素。每个具体工厂对应特定操作系统,并负责生成符合该操作系统风格的 UI 元素。 46 | 47 | ```ts 48 | // 抽象工厂接口声明了一组能返回不同抽象产品的方法。这些产品属于同一个系列 49 | // 且在高层主题或概念上具有相关性。同系列的产品通常能相互搭配使用。系列产 50 | // 品可有多个变体,但不同变体的产品不能搭配使用。 51 | interface GUIFactory is 52 | method createButton():Button 53 | method createCheckbox():Checkbox 54 | 55 | 56 | // 具体工厂可生成属于同一变体的系列产品。工厂会确保其创建的产品能相互搭配 57 | // 使用。具体工厂方法签名会返回一个抽象产品,但在方法内部则会对具体产品进 58 | // 行实例化。 59 | class WinFactory implements GUIFactory is 60 | method createButton():Button is 61 | return new WinButton() 62 | method createCheckbox():Checkbox is 63 | return new WinCheckbox() 64 | 65 | // 每个具体工厂中都会包含一个相应的产品变体。 66 | class MacFactory implements GUIFactory is 67 | method createButton():Button is 68 | return new MacButton() 69 | method createCheckbox():Checkbox is 70 | return new MacCheckbox() 71 | 72 | 73 | // 系列产品中的特定产品必须有一个基础接口。所有产品变体都必须实现这个接口。 74 | interface Button is 75 | method paint() 76 | 77 | // 具体产品由相应的具体工厂创建。 78 | class WinButton implements Button is 79 | method paint() is 80 | // 根据 Windows 样式渲染按钮。 81 | 82 | class MacButton implements Button is 83 | method paint() is 84 | // 根据 macOS 样式渲染按钮 85 | 86 | // 这是另一个产品的基础接口。所有产品都可以互动,但是只有相同具体变体的产 87 | // 品之间才能够正确地进行交互。 88 | interface Checkbox is 89 | method paint() 90 | 91 | class WinCheckbox implements Checkbox is 92 | method paint() is 93 | // 根据 macOS 样式渲染复选框。 94 | 95 | class MacCheckbox implements Checkbox is 96 | method paint() is 97 | // 根据 macOS 样式渲染复选框。 98 | 99 | // 客户端代码仅通过抽象类型(GUIFactory、Button 和 Checkbox)使用工厂 100 | // 和产品。这让你无需修改任何工厂或产品子类就能将其传递给客户端代码。 101 | class Application is 102 | private field button: Button 103 | constructor Application(factory: GUIFactory) is 104 | this.factory = factory 105 | method createUI() is 106 | this.button = factory.createButton() 107 | method paint() is 108 | button.paint() 109 | 110 | 111 | // 程序会根据当前配置或环境设定选择工厂类型,并在运行时创建工厂(通常在初 112 | // 始化阶段)。 113 | class ApplicationConfigurator is 114 | method main() is 115 | config = readApplicationConfigFile() 116 | 117 | if (config.OS == "Windows") then 118 | factory = new WinFactory() 119 | else if (config.OS == "Mac") then 120 | factory = new MacFactory() 121 | else 122 | throw new Exception("错误!未知的操作系统。") 123 | 124 | Application app = new Application(factory) 125 | ``` 126 | 127 | 其运作方式如下:应用程序启动后检测当前操作系统。根据该信息,应用程序通过与该操作系统对应的类创建工厂对象。其余代码使用该工厂对象创建 UI 元素。这样可以避免生成错误类型的元素。使用这种方法,客户端代码只需调用抽象接口,而无需了解具体工厂类和 UI 元素。此外,客户端代码还支持未来添加新的工厂或 UI 元素。 128 | 129 | 这样一来,每次在应用程序中添加新的 UI 元素变体时,你都无需修改客户端代码。你只需创建一个能够生成这些 UI 元素的工厂类,然后稍微修改应用程序的初始代码,使其能够选择合适的工厂类即可。 130 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Design Pattern Series 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | 38 | 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 | ![](https://pic4.zhimg.com/v2-a31b50055c9b896f7e3b9bb4467204e3_b.png) 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]() 17 | 18 | 在计算机科学与技术中,一个函数或者一个表达式被称为有副作用,常常发生在如下几种情况: 19 | 20 | - 修改了部分状态,譬如某个函数修改了全局变量、静态变量、某些传入的参数、引发了一个异常 21 | - 返回了一个类似于 Observable 对象 22 | - 与外部产生了交互,譬如读写文件、发起网络请求或者调用其他包含副作用的方程 23 | 24 | 在一个具有副作用的函数中,一个程序的行为往往依赖于上下文与历史记录。 25 | 26 | ### Mutable VS Immutable(可变对象与不可变对象) 27 | 28 | 顾名思义,不可变对象就是在创建之后其状态不可以再被修改的对象。引用 Effective Java 这本书中的一句话:> Classes should be immutable unless there's a very good reason to make them mutable....If a class cannot be made immutable, limit its mutability as much as possible.1. Immutable 降低了 Mutable 带来的复杂度可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。比如下面一段代码:`function touchAndLog(touchFn) { let data = { key: 'value' }; touchFn(data); console.log(data.key); // 猜猜会打印什么?}`在不查看 `touchFn` 的代码的情况下,因为不确定它对 `data` 做了什么,你是不可能知道会打印什么(这不是废话吗)。但如果 `data` 是 Immutable 的呢,你可以很肯定的知道打印的是 `value`。1. 节省内存 Immutable.js 使用了 Structure Sharing 会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。`import { Map} from 'immutable';let a = Map({ select: 'users', filter: Map({ name: 'Cam' })})let b = a.set('select', 'people');a === b; // falsea.get('filter') === b.get('filter'); // true`上面 a 和 b 共享了没有变化的 `filter` 节点。1. Undo/Redo,Copy/Paste,甚至时间旅行这些功能做起来小菜一碟因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。后面我会提供 Flux 做 Undo 的示例。1. 并发安全传统的并发非常难做,因为要处理各种数据不一致问题,因此聪明人发明了各种锁来解决。但使用了 Immutable 之后,数据天生是不可变的,**并发锁就不需要了**。然而现在并没什么卵用,因为 JavaScript 还是单线程运行的啊。但未来可能会加入,提前解决未来的问题不也挺好吗?1. 拥抱函数式编程 Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。像 ClojureScript,Elm 等函数式编程语言中的数据类型天生都是 Immutable 的,这也是为什么 ClojureScript 基于 React 的框架 --- Om 性能比 React 还要好的原因。 29 | 30 | ### High-Order Function(高阶函数) 31 | 32 | 在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数: 33 | 34 | - 接受一个或多个函数作为输入 35 | 36 | - 输出一个函数 37 | 38 | 在数学中它们也叫做算子(运算符)或泛函。微积分中的导数就是常见的例子,因为它映射一个函数到另一个函数。在无类型 lambda 演算,所有函数都是高阶的;在有类型 lambda 演算(大多数函数式编程语言都从中演化而来)中,高阶函数一般是那些函数型别包含多于一个箭头的函数。在函数式编程中,返回另一个函数的高阶函数被称为 Curry 化的函数。在很多函数式编程语言中能找到的 map 函数是高阶函数的一个例子。它接受一个函数 f 作为参数,并返回接受一个列表并应用 f 到它的每个元素的一个函数。 39 | 40 | ## 定义 41 | 42 | 简单说,"函数式编程"是一种["编程范式"](http://en.wikipedia.org/wiki/Programming_paradigm)(programming paradigm),也就是如何编写程序的方法论。它属于["结构化编程"](http://en.wikipedia.org/wiki/Structured_programming)的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。举例来说,现在有这样一个数学表达式:>   (1 + 2) _ 3 - 4 传统的过程式编程,可能这样写:>    var a = 1 + 2;> >    var b = a _ 3;> >    var c = b - 4;函数式编程要求使用函数,我们可以把运算过程[定义](http://lostechies.com/derickbailey/2012/01/24/some-thoughts-on-functional-javascript/)为不同的函数,然后写成下面这样:>    var result = subtract(multiply(add(1,2), 3), 4);这就是函数式编程。 43 | 44 | ### 特点 45 | 46 | 函数式编程具有五个鲜明的特点。 47 | 48 | **1. 函数是"第一等公民"** 49 | 50 | 所谓["第一等公民"](http://en.wikipedia.org/wiki/First-class_function)(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。 51 | 52 | 举例来说,下面代码中的 print 变量就是一个函数,可以作为另一个函数的参数。 53 | 54 | >    var print = function(i){ console.log(i);}; > >   [1,2,3].forEach(print); 55 | 56 | **2. 只用"表达式",不用"语句"** 57 | 58 | "表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。 59 | 60 | 原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(IO)。"语句"属于对系统的读写操作,所以就被排斥在外。 61 | 62 | 当然,实际应用中,不做 IO 是不可能的。因此,编程过程中,函数式编程只要求把 IO 限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。 63 | 64 | 3.函数是无状态、无副作用的 65 | 66 | 所谓["副作用"](http://en.wikipedia.org/wiki/Side_effect_%28computer_science%29)(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。 67 | 68 | 函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。 69 | 70 | 而无状态性则主要依赖于递归, 71 | 72 | 上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。 73 | 74 | 在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。下面的代码是一个将字符串逆序排列的函数,它演示了不同的参数如何决定了运算所处的"状态"。 75 | 76 | >    function reverse(string) { > >      if(string.length == 0) { > >        return string; > >     } else { > >        return reverse(string.substring(1, string.length)) + string.substring(0, 1); > >     } > >   } 77 | 78 | 由于使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要原因。 79 | 80 | **5. 引用透明** 81 | 82 | 引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。 83 | 84 | 有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫"引用不透明",很不利于观察和理解程序的行为。 85 | 86 | ### 优点 87 | 88 | **1. 代码简洁,开发快速** 89 | 90 | 函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。 91 | 92 | Paul Graham 在[《黑客与画家》](http://www.ruanyifeng.com/docs/pg/)一书中[写道](http://www.ruanyifeng.com/blog/2010/10/why_lisp_is_superior.html):同样功能的程序,极端情况下,Lisp 代码的长度可能是 C 代码的二十分之一。 93 | 94 | 如果程序员每天所写的代码行数基本相同,这就意味着,"C 语言需要一年时间完成开发某个功能,Lisp 语言只需要不到三星期。反过来说,如果某个新功能,Lisp 语言完成开发需要三个月,C 语言需要写五年。"当然,这样的对比故意夸大了差异,但是"在一个高度竞争的市场中,即使开发速度只相差两三倍,也足以使得你永远处在落后的位置。" 95 | 96 | **2. 接近自然语言,易于理解** 97 | 98 | 函数式编程的自由度很高,可以写出很接近自然语言的代码。 99 | 100 | 前文曾经将表达式(1 + 2) \* 3 - 4,写成函数式语言: 101 | 102 | >    subtract(multiply(add(1,2), 3), 4) 103 | 104 | 对它进行变形,不难得到另一种写法: 105 | 106 | >    add(1,2).multiply(3).subtract(4) 107 | 108 | 这基本就是自然语言的表达了。再看下面的代码,大家应该一眼就能明白它的意思吧: 109 | 110 | >    merge([1,2],[3,4]).sort().search("2") 111 | 112 | 因此,函数式编程的代码更容易理解。 113 | 114 | **3. 更方便的代码管理与结果的可缓存** 115 | 116 | 函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。 117 | 118 | 实际上,我们在函数式编程中进行构建的是实体与实体之间的关系。在这种意义上,lisp 虽然不是纯粹的函数式编程,但是也算是函数式编程一员。使用这种定 119 | 120 | 义,大多数提供了原生的 list 支持的脚本语言也可以算混合了函数式语言的功能,但是这不是函数式语言的精髓。知其然,还要知其所以然。我们既然已经有了 121 | 122 | 精确自然的命令式编程,又为什么还需要函数式编程呢?我们举个小例子。 123 | 124 | int fab(int n) { 125 | 126 | return n == 1 || n == 2 ? 1 : fab(n - 1) + fab(n - 2); 127 | 128 | } 129 | 130 | 这是用 C 语言写的求斐波那契数列的第 N 项的程序,相应的 Haskell 代码是这样的: 131 | 132 | fab::(Num a) => a -> a 133 | 134 | fab n = if n == 1 || n == 2 then 1 else fab(n - 1) + fab(n - 2) 135 | 136 | 看 137 | 138 | 上去差不多对不对?但是这两个程序在执行的效率方面有着天差地别的差距。为什么呢?C 语言是标准的命令式编程语言。因此对于你写下的每一行语句,C 程序会 139 | 140 | 原封不动地机械地去执行。如果想效率提高,你必须自己去分析程序,去人工地减少程序中执行的语句的数量。具体到这个 C 程序,我们注意到在每次函数调用时, 141 | 142 | 都会产生两个新的函数调用。这时,实际产生的函数调用的数目是指数级别的!比方说,我们写 fab(5),实际的执行结果是: 143 | 144 | fab(5) 145 | 146 | fab(4) 147 | 148 | fab(3) 149 | 150 | fab(2) 151 | 152 | fab(1) 153 | 154 | fab(2) 155 | 156 | fab(3) 157 | 158 | fab(2) 159 | 160 | fab(1) 161 | 162 | 我们看到,fab(3)被求值了两遍。为了计算 fab(5),我们实际执行了 8 次函数调用。 163 | 164 | 那么函数式语言呢?我们说过,函数式语言里面是没有变量的。换句话说,所有的东西都是不变的。因此在执行 fab(5)的时候,过程是这样的: 165 | 166 | fab(5) 167 | 168 | fab(4) 169 | 170 | fab(3) 171 | 172 | fab(2) 173 | 174 | fab(1) 175 | 176 | fab(3) 177 | 178 | 总 179 | 180 | 共只有五次应用。注意我说的是应用而不是调用。因为函数式语言里的函数本意并不是命令式语言里面的“调用”或者“执行子程序”的语义,而是“函数与函数之 181 | 182 | 间的关系”的意思。比如 fab 函数中出现的两次 fab 的应用,实际上说明要计算 fab 函数,必须先计算后续的两个 fab 函数。这并不存在调用的过程。因为 183 | 184 | 所有的计算都是静态的。haskell 可以认为所有的 fab 都是已知的。因此实际上所有遇到的 fab 函数,haskell 只是实际地计算一次,然后就缓存 185 | 186 | 了结果。 187 | 188 | 本质上,这代表了我们提供给函数式语言的程序其实并不是一行一行的“命令”,而只是对数据变换的说明。这样函数式语言可以深入这 189 | 190 | 些说明中,寻找这些说明中冗余的共性,从而进行优化。这就是函数式语言并不需要精心设计就会比命令式语言高效的秘密。命令式语言当然也可以进行这种优化, 191 | 192 | 但是因为命令式语言是有边界效应的。而且大部分情况下都是利用边界效应进行计算,因此很难推广这种优化,只有少数几种窥孔优化能取得效果。 193 | 194 | 放 195 | 196 | 到这个例子上,因为本质上我们两次的 fab 应用是重叠的。haskell 发现了这个特点,于是将两次 fab 的结果缓存下来(注意,能缓存结果的必要条件是 197 | 198 | 这个函数返回的值是不会变的!而这是函数式语言主要的特性)。如果后续的计算需要用到这两次 fab 的结果,就不需要再次重复计算,而只是直接提取结果就可 199 | 200 | 以了。这就是上面几乎完全一样的两个程序效率相差如此之大的主要原因。 201 | 202 | **4. 易于"并发编程"** 203 | 204 | 函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。 205 | 206 | 请看下面的代码: 207 | 208 | >    var s1 = Op1(); > >    var s2 = Op2(); > >    var s3 = concat(s1, s2); 209 | 210 | 由于 s1 和 s2 互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为 s1 可能会修改系统状态,而 s2 可能会用到这些状态,所以必须保证 s2 在 s1 之后运行,自然也就不能部署到其他线程上了。 211 | 212 | 多核 CPU 是将来的潮流,所以函数式编程的这个特性非常重要。 213 | 214 | **5. 代码的热升级** 215 | 216 | 函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。[Erlang](http://en.wikipedia.org/wiki/Erlang_%28programming_language%29)语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。 217 | 218 | # Reactive Programming(响应式编程) 219 | 220 | > - [Wiki-Reactive_programming](https://en.wikipedia.org/wiki/Reactive_programming) > - [The introduction to Reactive Programming you've been missing -- 中文版](https://github.com/benjycui/introrx-chinese-edition) > - [reactive-programming-using-rxjava-and-akka](https://www.linkedin.com/pulse/20141208023559-947775-reactive-programming-using-rxjava-and-akka) 221 | 222 | 从定义上来说,维基百科提供了如下的定义: 223 | 224 | > Reactive programming is a programming paradigm oriented around data flows and the propagation of change 225 | 226 | 响应式编程式一种面向数据流与变化传播的编程范式。通俗来说,响应式编程就是面向异步数据流的一种编程方式。响应式编程的原则可以看做是并发编程原则以及异步系统原则的一种组合。响应式编程可以看做高级编程范式面向于需要处理分布式状态以及编排异步数据流的一种自然扩展。一般来说,响应式编程的原则如下: 227 | 228 | - Responsive & Fault-tolerant: 应用程序应当能够快速地响应用户,即使在高负载以及存在错误的情况下。 229 | - Resilient and Scalable: The application should be resilient, in order to stay responsive under various conditions. They also should react to changes in the input rate by increasing or decreasing the resources allocated to service these inputs. Today’s applications have more integration complexity, as they are composed of multiple applications. 230 | - Message Driven: A *message-driven*architecture is the foundation of scalable, resilient, and ultimately responsive systems. 231 | 232 | 响应式编程是一种面向异步数据流的编程模型。在实际的编程实现中,响应式编程往往有两种方式,一个是类似于 Scala 中提供的 Actor 模型,另一个就是基于扩展的函数式响应编程模型。 233 | 234 | ## Functional Reactive Programming 235 | 236 | > - [functional-reactive-programming](https://realm.io/news/droidcon-gomez-functional-reactive-programming/) 237 | 238 | ## Actor Model 239 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | Public License 3 | 4 | By exercising the Licensed Rights (defined below), You accept and agree 5 | to be bound by the terms and conditions of this Creative Commons 6 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 7 | ("Public License"). To the extent this Public License may be 8 | interpreted as a contract, You are granted the Licensed Rights in 9 | consideration of Your acceptance of these terms and conditions, and the 10 | Licensor grants You such rights in consideration of benefits the 11 | Licensor receives from making the Licensed Material available under 12 | these terms and conditions. 13 | 14 | 15 | Section 1 -- Definitions. 16 | 17 | a. Adapted Material means material subject to Copyright and Similar 18 | Rights that is derived from or based upon the Licensed Material 19 | and in which the Licensed Material is translated, altered, 20 | arranged, transformed, or otherwise modified in a manner requiring 21 | permission under the Copyright and Similar Rights held by the 22 | Licensor. For purposes of this Public License, where the Licensed 23 | Material is a musical work, performance, or sound recording, 24 | Adapted Material is always produced where the Licensed Material is 25 | synched in timed relation with a moving image. 26 | 27 | b. Adapter's License means the license You apply to Your Copyright 28 | and Similar Rights in Your contributions to Adapted Material in 29 | accordance with the terms and conditions of this Public License. 30 | 31 | c. BY-NC-SA Compatible License means a license listed at 32 | creativecommons.org/compatiblelicenses, approved by Creative 33 | Commons as essentially the equivalent of this Public License. 34 | 35 | d. Copyright and Similar Rights means copyright and/or similar rights 36 | closely related to copyright including, without limitation, 37 | performance, broadcast, sound recording, and Sui Generis Database 38 | Rights, without regard to how the rights are labeled or 39 | categorized. For purposes of this Public License, the rights 40 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 41 | Rights. 42 | 43 | e. Effective Technological Measures means those measures that, in the 44 | absence of proper authority, may not be circumvented under laws 45 | fulfilling obligations under Article 11 of the WIPO Copyright 46 | Treaty adopted on December 20, 1996, and/or similar international 47 | agreements. 48 | 49 | f. Exceptions and Limitations means fair use, fair dealing, and/or 50 | any other exception or limitation to Copyright and Similar Rights 51 | that applies to Your use of the Licensed Material. 52 | 53 | g. License Elements means the license attributes listed in the name 54 | of a Creative Commons Public License. The License Elements of this 55 | Public License are Attribution, NonCommercial, and ShareAlike. 56 | 57 | h. Licensed Material means the artistic or literary work, database, 58 | or other material to which the Licensor applied this Public 59 | License. 60 | 61 | i. Licensed Rights means the rights granted to You subject to the 62 | terms and conditions of this Public License, which are limited to 63 | all Copyright and Similar Rights that apply to Your use of the 64 | Licensed Material and that the Licensor has authority to license. 65 | 66 | j. Licensor means the individual(s) or entity(ies) granting rights 67 | under this Public License. 68 | 69 | k. NonCommercial means not primarily intended for or directed towards 70 | commercial advantage or monetary compensation. For purposes of 71 | this Public License, the exchange of the Licensed Material for 72 | other material subject to Copyright and Similar Rights by digital 73 | file-sharing or similar means is NonCommercial provided there is 74 | no payment of monetary compensation in connection with the 75 | exchange. 76 | 77 | l. Share means to provide material to the public by any means or 78 | process that requires permission under the Licensed Rights, such 79 | as reproduction, public display, public performance, distribution, 80 | dissemination, communication, or importation, and to make material 81 | available to the public including in ways that members of the 82 | public may access the material from a place and at a time 83 | individually chosen by them. 84 | 85 | m. Sui Generis Database Rights means rights other than copyright 86 | resulting from Directive 96/9/EC of the European Parliament and of 87 | the Council of 11 March 1996 on the legal protection of databases, 88 | as amended and/or succeeded, as well as other essentially 89 | equivalent rights anywhere in the world. 90 | 91 | n. You means the individual or entity exercising the Licensed Rights 92 | under this Public License. Your has a corresponding meaning. 93 | 94 | 95 | Section 2 -- Scope. 96 | 97 | a. License grant. 98 | 99 | 1. Subject to the terms and conditions of this Public License, 100 | the Licensor hereby grants You a worldwide, royalty-free, 101 | non-sublicensable, non-exclusive, irrevocable license to 102 | exercise the Licensed Rights in the Licensed Material to: 103 | 104 | a. reproduce and Share the Licensed Material, in whole or 105 | in part, for NonCommercial purposes only; and 106 | 107 | b. produce, reproduce, and Share Adapted Material for 108 | NonCommercial purposes only. 109 | 110 | 2. Exceptions and Limitations. For the avoidance of doubt, where 111 | Exceptions and Limitations apply to Your use, this Public 112 | License does not apply, and You do not need to comply with 113 | its terms and conditions. 114 | 115 | 3. Term. The term of this Public License is specified in Section 116 | 6(a). 117 | 118 | 4. Media and formats; technical modifications allowed. The 119 | Licensor authorizes You to exercise the Licensed Rights in 120 | all media and formats whether now known or hereafter created, 121 | and to make technical modifications necessary to do so. The 122 | Licensor waives and/or agrees not to assert any right or 123 | authority to forbid You from making technical modifications 124 | necessary to exercise the Licensed Rights, including 125 | technical modifications necessary to circumvent Effective 126 | Technological Measures. For purposes of this Public License, 127 | simply making modifications authorized by this Section 2(a) 128 | (4) never produces Adapted Material. 129 | 130 | 5. Downstream recipients. 131 | 132 | a. Offer from the Licensor -- Licensed Material. Every 133 | recipient of the Licensed Material automatically 134 | receives an offer from the Licensor to exercise the 135 | Licensed Rights under the terms and conditions of this 136 | Public License. 137 | 138 | b. Additional offer from the Licensor -- Adapted Material. 139 | Every recipient of Adapted Material from You 140 | automatically receives an offer from the Licensor to 141 | exercise the Licensed Rights in the Adapted Material 142 | under the conditions of the Adapter's License You apply. 143 | 144 | c. No downstream restrictions. You may not offer or impose 145 | any additional or different terms or conditions on, or 146 | apply any Effective Technological Measures to, the 147 | Licensed Material if doing so restricts exercise of the 148 | Licensed Rights by any recipient of the Licensed 149 | Material. 150 | 151 | 6. No endorsement. Nothing in this Public License constitutes or 152 | may be construed as permission to assert or imply that You 153 | are, or that Your use of the Licensed Material is, connected 154 | with, or sponsored, endorsed, or granted official status by, 155 | the Licensor or others designated to receive attribution as 156 | provided in Section 3(a)(1)(A)(i). 157 | 158 | b. Other rights. 159 | 160 | 1. Moral rights, such as the right of integrity, are not 161 | licensed under this Public License, nor are publicity, 162 | privacy, and/or other similar personality rights; however, to 163 | the extent possible, the Licensor waives and/or agrees not to 164 | assert any such rights held by the Licensor to the limited 165 | extent necessary to allow You to exercise the Licensed 166 | Rights, but not otherwise. 167 | 168 | 2. Patent and trademark rights are not licensed under this 169 | Public License. 170 | 171 | 3. To the extent possible, the Licensor waives any right to 172 | collect royalties from You for the exercise of the Licensed 173 | Rights, whether directly or through a collecting society 174 | under any voluntary or waivable statutory or compulsory 175 | licensing scheme. In all other cases the Licensor expressly 176 | reserves any right to collect such royalties, including when 177 | the Licensed Material is used other than for NonCommercial 178 | purposes. 179 | 180 | 181 | Section 3 -- License Conditions. 182 | 183 | Your exercise of the Licensed Rights is expressly made subject to the 184 | following conditions. 185 | 186 | a. Attribution. 187 | 188 | 1. If You Share the Licensed Material (including in modified 189 | form), You must: 190 | 191 | a. retain the following if it is supplied by the Licensor 192 | with the Licensed Material: 193 | 194 | i. identification of the creator(s) of the Licensed 195 | Material and any others designated to receive 196 | attribution, in any reasonable manner requested by 197 | the Licensor (including by pseudonym if 198 | designated); 199 | 200 | ii. a copyright notice; 201 | 202 | iii. a notice that refers to this Public License; 203 | 204 | iv. a notice that refers to the disclaimer of 205 | warranties; 206 | 207 | v. a URI or hyperlink to the Licensed Material to the 208 | extent reasonably practicable; 209 | 210 | b. indicate if You modified the Licensed Material and 211 | retain an indication of any previous modifications; and 212 | 213 | c. indicate the Licensed Material is licensed under this 214 | Public License, and include the text of, or the URI or 215 | hyperlink to, this Public License. 216 | 217 | 2. You may satisfy the conditions in Section 3(a)(1) in any 218 | reasonable manner based on the medium, means, and context in 219 | which You Share the Licensed Material. For example, it may be 220 | reasonable to satisfy the conditions by providing a URI or 221 | hyperlink to a resource that includes the required 222 | information. 223 | 3. If requested by the Licensor, You must remove any of the 224 | information required by Section 3(a)(1)(A) to the extent 225 | reasonably practicable. 226 | 227 | b. ShareAlike. 228 | 229 | In addition to the conditions in Section 3(a), if You Share 230 | Adapted Material You produce, the following conditions also apply. 231 | 232 | 1. The Adapter's License You apply must be a Creative Commons 233 | license with the same License Elements, this version or 234 | later, or a BY-NC-SA Compatible License. 235 | 236 | 2. You must include the text of, or the URI or hyperlink to, the 237 | Adapter's License You apply. You may satisfy this condition 238 | in any reasonable manner based on the medium, means, and 239 | context in which You Share Adapted Material. 240 | 241 | 3. You may not offer or impose any additional or different terms 242 | or conditions on, or apply any Effective Technological 243 | Measures to, Adapted Material that restrict exercise of the 244 | rights granted under the Adapter's License You apply. 245 | 246 | 247 | Section 4 -- Sui Generis Database Rights. 248 | 249 | Where the Licensed Rights include Sui Generis Database Rights that 250 | apply to Your use of the Licensed Material: 251 | 252 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 253 | to extract, reuse, reproduce, and Share all or a substantial 254 | portion of the contents of the database for NonCommercial purposes 255 | only; 256 | 257 | b. if You include all or a substantial portion of the database 258 | contents in a database in which You have Sui Generis Database 259 | Rights, then the database in which You have Sui Generis Database 260 | Rights (but not its individual contents) is Adapted Material, 261 | including for purposes of Section 3(b); and 262 | 263 | c. You must comply with the conditions in Section 3(a) if You Share 264 | all or a substantial portion of the contents of the database. 265 | 266 | For the avoidance of doubt, this Section 4 supplements and does not 267 | replace Your obligations under this Public License where the Licensed 268 | Rights include other Copyright and Similar Rights. 269 | 270 | 271 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 272 | 273 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 274 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 275 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 276 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 277 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 278 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 279 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 280 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 281 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 282 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 283 | 284 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 285 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 286 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 287 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 288 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 289 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 290 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 291 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 292 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 293 | 294 | c. The disclaimer of warranties and limitation of liability provided 295 | above shall be interpreted in a manner that, to the extent 296 | possible, most closely approximates an absolute disclaimer and 297 | waiver of all liability. 298 | 299 | 300 | Section 6 -- Term and Termination. 301 | 302 | a. This Public License applies for the term of the Copyright and 303 | Similar Rights licensed here. However, if You fail to comply with 304 | this Public License, then Your rights under this Public License 305 | terminate automatically. 306 | 307 | b. Where Your right to use the Licensed Material has terminated under 308 | Section 6(a), it reinstates: 309 | 310 | 1. automatically as of the date the violation is cured, provided 311 | it is cured within 30 days of Your discovery of the 312 | violation; or 313 | 314 | 2. upon express reinstatement by the Licensor. 315 | 316 | For the avoidance of doubt, this Section 6(b) does not affect any 317 | right the Licensor may have to seek remedies for Your violations 318 | of this Public License. 319 | 320 | c. For the avoidance of doubt, the Licensor may also offer the 321 | Licensed Material under separate terms or conditions or stop 322 | distributing the Licensed Material at any time; however, doing so 323 | will not terminate this Public License. 324 | 325 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 326 | License. 327 | 328 | 329 | Section 7 -- Other Terms and Conditions. 330 | 331 | a. The Licensor shall not be bound by any additional or different 332 | terms or conditions communicated by You unless expressly agreed. 333 | 334 | b. Any arrangements, understandings, or agreements regarding the 335 | Licensed Material not stated herein are separate from and 336 | independent of the terms and conditions of this Public License. 337 | 338 | 339 | Section 8 -- Interpretation. 340 | 341 | a. For the avoidance of doubt, this Public License does not, and 342 | shall not be interpreted to, reduce, limit, restrict, or impose 343 | conditions on any use of the Licensed Material that could lawfully 344 | be made without permission under this Public License. 345 | 346 | b. To the extent possible, if any provision of this Public License is 347 | deemed unenforceable, it shall be automatically reformed to the 348 | minimum extent necessary to make it enforceable. If the provision 349 | cannot be reformed, it shall be severed from this Public License 350 | without affecting the enforceability of the remaining terms and 351 | conditions. 352 | 353 | c. No term or condition of this Public License will be waived and no 354 | failure to comply consented to unless expressly agreed to by the 355 | Licensor. 356 | 357 | d. Nothing in this Public License constitutes or may be interpreted 358 | as a limitation upon, or waiver of, any privileges and immunities 359 | that apply to the Licensor or You, including from the legal 360 | processes of any jurisdiction or authority. -------------------------------------------------------------------------------- /01~编程范式/函数式编程/99~参考资料/2018~手把手介绍函数式编程:从命令式重构到函数式.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://github.com/oldratlee/translations/blob/master/a-practical-introduction-to-functional-programming/README.md) 2 | 3 | 原文链接: [A practical introduction to functional programming](https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming) - [Mary Rose](https://github.com/maryrosecook),2015-08-10 4 | 译于 2019-09-07 5 | 6 | 7 | 8 | ## 🍎 译序 9 | 10 | 本文是一篇手把手的函数式编程入门介绍,借助代码示例讲解细腻。但又不乏洞见,第一节中列举和点评了函数式种种让眼花缭乱的特质,给出了『理解函数式特质的指南针:函数式代码的核心特质就一条,**无副作用**』,相信这个指南针对于有积极学过挖过函数式的同学看来更是有相知恨晚的感觉。 11 | 12 | 希望看了这篇文章之后,能在学习和使用函数式编程的旅途中不迷路哦,兄 die ~ 13 | 14 | PS:本人是在《[Functional Programming, Simplified(Scala edition)](https://alvinalexander.com/scala/functional-programming-simplified-book)》了解到本文。这本书由浅入深循序渐进地对`FP`做了体系讲解,力荐![书的豆瓣链接](https://book.douban.com/subject/30326807/)。 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 | ![](lazy-small.jpg) 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实际上这样做并不比栈上存储要慢,因为在引入[垃圾回收机制]()后,内存分配操作的时间代价仅为 O(1)。[↩](#a8) 535 | --------------------------------------------------------------------------------