MIT 总结
时间:2023-06-23 12:07:00
设计模式分为创造型、结构型、行为型三种类型
- 创建型:用于创建对象,为设计实例化的新对象提供指南
- 结构类型:如何设计处理类或对象的组合,以形成更大的结构
- 行为类型:用于描述类或对象之间的互动和职责分配,并为类之间的互动和职责分配提供指导
权威软考的定义
常见的设计模式有以下几种
创造型:构造者、单例、工厂
结构类型:适配器、代理、外观
行为:战略、观察者、责任链
正文
Builder构建者
-
场景
用于生成对象的类的内部结构过于复杂。为了屏蔽类的复杂性,需要将类的结构和表示分开,用最少的参数生成对象 -
例子
OkHttpClient
从builder取出对象
即使没有参数Builder也会生成默认参数,以支持对象的生成
Builder
记得每次添加参数记得返回Builder对象用于链式传输
这是OkHttpClient对象传输参数的微妙之处在于源代码中的许多地方
Singleton单例
- 场景
为了保证操作的操作唯一性的唯一存在
实现单例模式有两种:饱汉和饿汉模式
联想记忆:饱汉不饿,需要时再吃;饿汉很饿,提前赶紧吃
饱汉其实是懒加载,不需要提前占用内存
- 例子
//饱汉模式 public class SingletonClass{ private static volatile SingletonClass instance=null;///静态变量保证对象的独特性,volatile防止指令重排关键字 public static SingletonClass getInstance(){//使用内锁,避免每次调用时不必要的锁,提高性能 if(instance==null)如果{///指令重排,其他线程可以看到instance不空,不会进入或锁定,直接返回半成品instance,导致异常 synchronized(SingletonClass.class){//锁对象最好是类对象(个人观点) if(instance==null)///再判断,上一行同时锁定多个线程,释放后进入,再次实例对象 instance=new SingletonClass() } } return instance; } private SingletonClass(){//私有构造方法,防止实例化 } }
///饿汉模式 public static class Singleton常量池直接进入静态类 private static final Singleton instance = new Singleton() private Singleton(){//私有结构方法,防止实例化 } public static Singleton getInstance(){///静态方法 return instance; } }
饿汉模式,由于饱汉没有多线程问题,但其常住内存会导致成本问题,不同的场景需要使用不同的方法来确保消费和效率的平衡。
Factory工厂
-
场景
在多态场景下,用于管理和生成不同的对象,耦合度不能太高 -
例子
工厂模式简单
public interface Phone {//Phone是产品抽象 } public class Honor implements Phone{//Honor是特定的产品 } public class Mate20 implements Phone{//Mate20是特定的产品 } public class HuaWei {//具体的厂商 public static Phone create(String str){//厂家按要求生产手机 if(str.equalsIgnoreCase("honor")){ return new Honor(); } else if(str.equalsIgnoreCase("mate20")){ return new Mate20(); } return null; } }
工厂方法模式
public interface Phone {//Phone是产品抽象 } public interface Factory {//Factory是厂商抽象 public Phone create(); } public class HWPhone implements Phone{///华为手机 } public class MPhone implements Phone{///小米手机 } public class HuaWei implements Factory 华为产华为手机 public Phone create(){ return new HWPhone(); } } public class XiaoMi implements Factory {///小米生小米手机 public Phone create(){ return new MPhone(); } }
简单的工厂模式:工厂类别,工厂生产各种产品
工厂方法模式:一个工厂接口,多个工厂类别,不同的工厂生产不同的产品
工厂方法模式的改进在于减少了简单工厂模式中工厂类的复杂性,具体产出交给具体工厂类,降低了耦合度。
抽象工厂模式
public interface Phone {//Phone是产品抽象 } public interface OS {//OS是产品抽象 } public interface Factory {//Factory是厂商抽象 public Phone create(); public OS develop(); } public class HWPhone implements Phone{///华为手机 } public class MPhone implements Phone{///小米手机 } public class MIUI implements OS{//系统 } public class EMUI implements OS{//系统 } public class HuaWei implements Factory华为产华为手机 public Phone create(){ return new HWPhone(); } public OS develop(){ return new EMUI(); } } public class XiaoMi implements Factory{///小米生小米手机 public Phone create(){ return new MPhone(); } public OS develop(){ return new MIUI(); } }
抽象工厂模式与工厂模式的区别在于,不生产单一产品,使工厂能够生产不同类型的相关产品,从而提高可扩展性
在Retrofit中
采用工厂方法模式,抽象工厂
在初始阶段,将不同的工厂放在实例化中List根据不同的参数使用场景,在中间List取出合适的工厂进行处理。
Adapter适配器
-
场景
现有功能不满足需求,需要转换功能以实现匹配 -
例子
类适配器模式
通过创建,原始类没有任何功能Adapter继承原功能,实现目标功能接口,满足功能要求
public class Source {//现有类 public void method1() { System.out.println("this is original method!"); } } public interface Targetable {//目标功能 public void method1(); public void method()//缺乏功能 } public class Adapter extends Source implements Targetable { @Override public void method2() {//补足缺失 System.out.println("this is the targetable method!"); } }
对象适配器模式
与类适配器模式不同,这种高耦合手段是通过继承和实现来满足功能要求的。对象适配器模式采用聚合创建新类别Wrapper除持有原类对象具有原功能外;且类Wrapper实现功能接口补足缺失功能
如OkHttpClient中的Cache类
在OkHttpClient中缓存拦截器一层使用的是InternalCache,见注释,官方已经不建议使用此类改用Cache
Cache类本身并没有实现InternalCache接口,而是持有一个InternalCache对象,且对象内的方法都是调用Cache内的
接口适配器模式
不同于类适配器模式、对象适配器模式的补足功能,接口适配器模式恰恰相反:不暴露功能。
当接口方法过多,而我们关心的却寥寥可数时,通过实现接口的方式就会空实现很多不必要的方法。通过,创建新类(或抽象类)空实现所有方法,实际使用时继承自新类重写关心的方法,就避免了每次实现方法过多的问题,这种设计的原则是接口隔离,对接口知道的越少越好
如OkHttp中的回调监听EventListener
所有阶段的回调都空实现,当我关心某一阶段的回调时,就重写对应方法,否则20个回调方法每添加一次监听就都要实现一遍,代码的易读性也不好
Proxy代理
代理分静态代理和动态代理,这里列举的是静态代理
-
场景
原有类的功能需要进行扩充 -
例子
public interface Sourceable {
public void method();
}
public class Source implements Sourceable {//原类
@Override
public void method() {
System.out.println("the original method!");
}
}
public class Proxy implements Sourceable {//代理类
private Source source;
public Proxy(){
super();
this.source = new Source();
}
@Override
public void method() {
before();
source.method();
atfer();
}
private void atfer() {
System.out.println("after proxy!");
}
private void before() {
System.out.println("before proxy!");
}
}
原类与代理类都实现了相同的接口,方法的实际执行是由代理对象触发的,只不过代理对象在方法触发前后增加了其他功能。
有点和对象适配器模式类似都实现相关接口、持有原类对象,但
- 对象适配器模式是为了弥补功能上的不足
- 代理模式是给功能进行加强
一个是弥补功能,一个是加强功能这是两种模式的区别
从上来看对象适配器模式中的Cache例子放错位置了,它应该属于静态代理模式,Cache是被代理类,InternalCache是代理类
Facade外观
-
场景
多个类间有依赖关系时,由多个类组合创建新类 -
例子
OkHttpClient和Retrofit类就是明显的外观模式,其内部由多个功能不同的类对象组成,外部的单一操作实际影响内部多个对象间的联动。
Strategy策略
-
场景
相同的方法,根据不同的情景有不同的实现 -
例子
以前做过比特币的自动交易软件,实际操作就两种:买、卖,但是针对不同的行情、手上的持仓和现金情况,买和卖就变得有学问了:
行情好的时候采用激进策略,大胆买卖;
行情差的时候就要高抛低吸,慢慢来;
Observer观察者
-
场景
某一对象状态发生变化时,其他对象需要及时的告知 -
例子
android中的回调就是观察者模式,简单点的:给一个button设置onClickListener,此时观察者就是onClickListener被观察者是button,setOnClickListener是给两者创建关联,当被观察者button被单击,观察者onClickListener就会作出回应。
Chain of Responsibility责任链
-
场景
问题一次得不到解决,需要层层处理并传递,每次将任务细化 -
例子
最有名的当数OkHttp,其中的链是环形链
有些自己用的比较熟的或没太多重点要记录的就没有贴例子,一般这种模式比较常见且简单
目标
今天课后,你应该:
- 理解语法生成和正则表达式运算符的思想
- 能够阅读语法或正则表达式并确定它是否与字符序列相匹配
- 能够编写语法或正则表达式来匹配一组字符序列并将其解析为数据结构
介绍
今天的阅读介绍了几个想法:
- 语法,带产生式、非终结符、终结符和运算符
- 正则表达式
一些程序模块以字节序列或字符序列的形式接收输入或输出,这称为一串当它被简单地存储在内存中时,或者流动当它流入或流出模块时。在今天的阅读中,我们将讨论如何为这样的序列编写规范。具体地说,字节或字符序列可以是:
- 磁盘上的文件,在这种情况下,规范称为文件格式
- 通过网络发送的消息,在这种情况下,规范是有线协议
- 用户在控制台上键入的命令,在这种情况下,规范是命令行界面
- 存储在内存中的字符串
对于这类序列,我们引入了a的概念语法,它不仅允许我们区分合法和非法序列,而且还可以将序列解析为程序可以使用的数据结构。从语法生成的数据结构通常是递归数据类型,就像我们在递归数据类型读取.
我们还讨论了一种特殊的语法形式,称为正则表达式. 除了用于规范和解析之外,正则表达式也是许多字符串处理任务的广泛使用工具,这些任务需要反汇编字符串、从中提取信息或转换字符串。
下一篇文章将讨论解析器生成器,一种将语法自动转换为语法分析器的工具。
语法
为了描述一系列符号,不管它们是字节、字符还是从固定集合中抽取的其他类型的符号,我们使用一种称为语法.
A语法定义一组字符串。假设我们要编写一个表示url的语法。我们的url语法将指定一组在HTTP协议中是合法url的字符串。
语法中的字符串称为终端. 它们被称为终端,因为它们是解析树表示字符串结构的。他们没有孩子,不能再扩大。我们通常用引号写结尾,比如“http”
或':'
.
语法由一组制作,其中每个产品定义非终结符.你可以把一个非终结符看作是一个代表一组字符串的变量,而产生式是这个变量在其他变量(非终结符)、运算符和常量(终端)方面的定义。非终端是表示字符串的树的内部节点。
语法中的产生式有
非终结符::=终结符、非终结符和运算符的表达式
语法的一个非终结符被指定为根.语法识别的字符串集是与根非终结符匹配的字符串集。这个非终结符有时被称为根
或开始
或者只是S
,但在下面的语法中,我们通常会为根选择更可读的名称,比如网址
,html格式
,和降价
.
因此,表示一个单例集的语法只允许一个特定的URL,可能只有一个产品定义非终端网址
,右侧有一个终端:
网址::= 'http://mit.edu/'
语法运算符
产品可以使用运算符在右侧组合终端和非终端。产生式表达式中最重要的三个运算符是:
重复,代表*
:
十::=是的*x与零或更多y匹配
串联,不是用符号表示,而是用空格表示:
十::=y z x匹配y后跟z
工会,也称为交替,表示为|
:
十::=是的|z x匹配y或z
按照惯例,后缀运算符喜欢*
具有最高优先级,这意味着它们首先被应用。接下来将应用串联。交替|
具有最低优先级,这意味着它是最后应用的。括号可用于重写优先级:
米::=a(b)|c) d m与a匹配,后跟b或c,然后是d十::=(y z)|a和b)*x匹配零个或多个yz或ab对
让我们用这些运算符来推广网址
匹配其他主机名的语法,例如http://stanford.edu/
和http://google.com/
.
网址::= 'http://'主机名'/'主机名::= 麻省理工学院。埃杜 | “斯坦福。埃杜 | “谷歌。通信'
这个语法的第一个规则演示了连接。这个网址
非终结符匹配以文本字符串开头的字符串http地址://
,后跟匹配主机名
非终结符,后跟文本字符串/
.
这个主机名
规则显示联合。A主机名
可以匹配三个字符串中的一个,麻省理工学院。埃杜
或斯坦福大学。埃杜
或谷歌。通用域名格式
.
所以这个语法代表了三个字符串的集合,http://mit.edu/
,http://google.com/
,和http://stanford.edu/
.
让我们更进一步,允许任何小写单词代替麻省理工学院
,斯坦福大学
,谷歌
,通用域名格式
和埃杜
:
网址::= 'http://'主机名'/'主机名::=单词'.'单词单词::=(“a” | “b” | “c” | “d” | “e” | “f” | “g” | “h” | “我”
| ‘j’ | “k” | “我” | “m” | “n” | “o” | “p” | ‘q’
| “r” | “s” | “t” | “u” | “v” | “w” | “x” | “是的” | “z”)*
新的单词
规则匹配一个零个或多个小写字母的字符串,因此整个语法现在可以匹配了http://alibaba.com/
和http://zyxw.edu/
也。不幸的是单词
也可以匹配空字符串,因此网址
语法也匹配http:///
,它不是合法的URL。这里有一个冗长的方法来解决这个问题,它需要单词
至少匹配一个字母。
单词::=(“a” | “b” | “c” | “d” | “e” | “f” | “g” | “h” | “我”
| ‘j’ | “k” | “我” | “m” | “n” | “o” | “p” | ‘q’
| “r” | “s” | “t” | “u” | “v” | “w” | “x” | “是的” | “z”)(“a” | “b” | “c” | “d” | “e” | “f” | “g” | “h” | “我”
| ‘j’ | “k” | “我” | “m” | “n” | “o” | “p” | ‘q’
| “r” | “s” | “t” | “u” | “v” | “w” | “x” | “是的” | “z”)*
我们将看到一种更简单的写作方法单词
在下一节。
更多语法运算符
你也可以使用一些附加的操作符,它们只是语法上的糖(也就是说,它们相当于三个主要操作符的组合)。
0或1次代表为?
:
十::=是的?x是y或者是空字符串
一次或多次代表为+
:
十::=是的+x是一个或多个y(相当于x::=是的,是的*)
A字符类 [...]
表示与方括号中列出的任何字符匹配的一组单字符字符串:
十::= 【aeiou】相当于x::= “a” | “e” | “我” | “o” | “u”十::= [空调]相当于x::= “a” | “b” | “c”
安倒置字符类 [^...]
表示与字符匹配的一组单字符字符串不括号内列出:
十::= [^a-c]相当于x::= “d” | “e” | “f” | ... | “0” | “1” | '2' | ... | '!' | '@'
| ...(所有其他可能的字符)
这些附加运算符允许单词
更紧凑地表达生产:
网址::= 'http://'主机名'/'主机名::=单词'.'单词单词::= 【a-z】+
语法中的递归
我们还需要怎样概括呢?主机名可以有两个以上的组件,并且可以有一个可选的端口号:
http://didit.csail.mit.edu:4949/
要处理这种字符串,语法如下:
网址::= 'http://'主机名(':'港口)? '/'主机名::=单词'.'主机名|单词'.'单词港口::= [0-9]+单词::= 【a-z】+
请注意,主机名现在是如何根据自身递归定义的。主机名定义的哪一部分是基本情况,哪部分是递归步骤?允许什么类型的主机名?
使用重复运算符,我们也可以写主机名
没有递归,像这样:
主机名::=(文字'.')+单词
使用这样的运算符有时可以从语法中消除递归,但并不总是这样。
另一件要注意的事情是,这种语法允许端口号在技术上不合法,因为端口号的范围只能是0到65535(216-1) 一。我们可以写一个更复杂的定义港口
这将只匹配这些整数,但在语法中通常不会这样做。相反,约束0≤港口
≤ 65535将在使用语法的程序中指定。
为了走得更远,我们应该做更多的事情:
- 泛化
http协议
以支持url可以具有的附加协议 - 概括
/
在斜线分隔路径的末尾 - 允许主机名包含完整的合法字符集而不是a-z
阅读练习
阅读语法1
阅读语法2
写语法
解析树
将语法与字符串匹配可以生成解析树它显示了字符串的各个部分如何与语法部分相对应。
解析树的叶用终端标记,表示已解析的字符串部分。他们没有孩子,不能再扩大。如果我们把叶子连在一起,我们就得到原始字符串。一个简单的例子是我们开始使用的一行URL语法,其(唯一可能的)解析树显示在右侧:
网址::= 'http://mit.edu/'
解析树的内部节点用非终结符标记。非终结点节点的直接子节点必须遵循语法中非终结点的产生式规则的模式。例如,在我们更详细的URL语法中,允许任何由两部分组成的主机名主机名
树中的节点必须遵循主机名
规则,单词“.”单词
.右图显示了通过将此语法与http://mit.edu/
:
网址::= 'http://'主机名'/'主机名::=单词'.'单词单词::= 【a-z】+
对于一个更详细的示例,这里是递归URL语法的解析树。这棵树现在有了更多的结构。这个主机名
和单词
非终结符是树的标记节点,其子树与语法中的规则相匹配。
网址::= 'http://'主机名(':'港口)? '/'主机名::=单词'.'主机名|单词'.'单词港口::= [0-9]+单词::= 【a-z】+
阅读练习
解析树1
解析树2
示例:Markdown和HTML
现在让我们看看一些文件格式的语法。我们将使用两种不同的标记语言来表示文本中的排版样式。它们在这里:
降价
这是“斜体”。
(要了解有关降价的信息,请参阅降价语法文档或降价维基百科.)
HTML格式
这是一个<我>斜体我>字。
(要了解HTML,请参阅HTML生活标准,或HTML格式维基百科.)
为了简单起见,我们的示例HTML和Markdown语法将只指定斜体,但其他文本样式当然是可能的。为了简单起见,我们假设格式分隔符之间的纯文本不允许使用任何格式标点,比如_
或<
.
以下是简化版Markdown的语法:
降价::=(正常|斜体)*斜体::= '_'正常的'_'正常的::=文本文本::= [^_]*
以下是简化版HTML的语法:
html格式::=(正常|斜体)*斜体::= ''html格式''正常的::=文本文本::= [^<>]*
阅读练习
递归语法
正则表达式
A有规律的语法有一个特殊的特性:通过将每个非终结符(根词根除外)替换为它的右手边,就可以将它简化为根的一个产生式,右手边只有终端和运算符。
我们的URL语法是规则的。通过将非终结符替换为它们的结果,可以将其简化为一个表达式:
网址::= 'http://'(【a-z】+ '.')+ 【a-z】+(':' [0-9]+)? '/'
降价语法也是有规律的:
降价::=([^_]* | '_' [^_]* '_')*
但是我们的HTML语法不能完全简化。通过将右侧替换为非终结符,您最终可以将其简化为如下所示:
html格式::=([^<>]* | ''html格式'')*
…但是递归使用html格式
右手边不能被消除,也不能简单地用重复运算符代替。所以HTML语法是不规则的。
终端和运算符的简化表达式可以用更紧凑的形式编写,称为正则表达式. 正则表达式去掉了端子周围的引号,以及端子和运算符之间的空格,因此它只由端子字符、用于分组的圆括号和运算符字符组成。例如,我们的降价
格式只是
([^_]*|_[^_]*_)*
正则表达式也称为正则表达式简而言之。正则表达式的可读性远不如原始语法,因为它缺少记录每个子表达式含义的非终结名名称。但是许多编程语言都支持regex(而不是语法),而且regex的匹配速度比语法快得多。
在编程语言库中通常实现的regex语法除了上面我们在语法中使用的操作符之外,还有一些更特殊的操作符。以下是一些常用的方法:
. 任何单个字符(但有时不包括换行符,具体取决于regex库)\d任意数字,与[0-9]相同\任何空格字符,包括空格、制表符、换行符\w任何包含下划线的单词字符,与[a-zA-Z_0-9]相同\. 反斜杠还用于“转义”运算符或特殊字符,以便\(字面上是匹配的。这些是一些常见的特殊字符\)你需要逃离:\* . ( ) * + | [ ] \\+如左边的例子所示。等。
当有可能与特殊字符混淆的终端字符时,使用反斜杠非常重要。因为我们网址
正则表达式具有.
作为终端,我们需要使用反斜杠来转义:
http://([a-z]+\)+[a-z]+(:[0-9]+)/
阅读练习
正则表达式
在Java中使用正则表达式
regex在编程中被广泛使用,您应该在工具箱中使用它们。
在Java中,可以使用正则表达式来操作字符串(请参见字符串。分裂,字符串。比赛,爪哇。利用率。正则表达式。图案). 它们是现代脚本语言(如Python、Ruby和JavaScript)的一类内置功能,您可以在许多文本编辑器中使用它们来查找和替换。正则表达式是你的朋友!大多数时候。这里有一些例子。
将所有空间段替换为单个空间:
String singleSpacedString=字符串。全部替换(" +"," ");
匹配URL:
Pattern regex=模式。编译(“http://([a-z]+\\)+[a-z]+(:[0-9]+)/");Matcher m=正则表达式。火柴(串);如果(m.matches()){//那么字符串就是一个url}
注意上面例子中的反斜杠。我们要匹配一个文字句点.
,所以我们必须先把它\.
为了防止它被解释为正则表达式匹配任何字符运算符,然后我们必须进一步将其转义为\\.
以防止反斜杠被解释为Java字符串转义字符。经常需要双反斜杠转义,这使得正则表达式的可读性仍然较低。
提取日期的一部分:
Pattern regex=模式。编译(“(?<年>[0-9][0-9][0-9][0-9])-(?<月>[0-9][0-9])-(?<天>[0-9][0-9]”);Matcher m=正则表达式。火柴(串);如果(m.matches()){字符串年份=m.group(“年份”);字符串月份=m.group(“月”);String day=m.group(“天”);//火柴。group(name)返回匹配的字符串部分(?…)}
此示例使用命名捕获组喜欢(?
提取匹配字符串的一部分并为其指定名称。这个(?
语法与正则表达式匹配...
在括号内,然后赋值名称匹配的字符串。请注意?
这里有不平均0或1次重复。在这种情况下,在左括号后?
表示这些括号具有特殊含义,而不仅仅是分组。
成功匹配后,可以使用group()方法检索命名的捕获组。如果这个正则表达式与“2025-03-18”
,例如m、 集团(“年”)
会回来的“2025年”
,m、 组(“月”)
会回来的“03”
,和m、 组(“日”)
会回来的“18”
.
阅读练习
在Java中使用regex
使用正则表达式进行字符串分析
上下文无关文法
一般来说,可以用我们的语法系统来表达的语言叫做上下文无关。并非所有上下文无关的语言也是规则的;也就是说,有些语法不能简化为单个的非递归结果。我们的HTML语法是上下文无关的,但不是常规的。
大多数编程语言的语法也是上下文无关的。一般来说,任何具有嵌套结构的语言(如嵌套括号或大括号)都是上下文无关的,但不是常规的。该描述适用于Java语法,如下部分所示:
陈述::=
'{'陈述* '}'
| '如果' '('表达')'声明(“其他”声明)?
| '为' '('穹窿? ';'表达? ';'更新? ')'陈述| '当' '('表达')'陈述| “做”陈述'当' '('表达')' ';'
| “尝试” '{'陈述* '}'(接住|捕捉? “终于” '{'陈述* '}')| '开关' '('表达')' '{'开关组'}'
| '同步' '('表达')' '{'陈述* '}'
| '返回'表达? ';'
| “投掷”表达';'
| '休息'标识符? ';'
| '继续'标识符? ';'
|表达';'
|标识符':'陈述| ';'
摘要
机器处理的文本语言在计算机科学中无处不在。语法是描述这类语言最流行的形式主义,正则表达式是语法的一个重要子类,可以不用递归来表达。
今天阅读的主题与我们优秀软件的三个特性相关,如下所示:
-
远离虫子。语法和正则表达式是字符串和流的声明性规范,可由库和工具直接使用。这些规范通常比解析手工编写的代码更简单、更直接,更不容易出错。
-
容易理解。语法以比手工编写的解析代码更容易理解的形式捕获序列的形状。唉,正则表达式通常不容易理解,因为它们是原本更容易理解的正则语法的一种简化形式。
-
准备好改变了。语法可以很容易地编辑,但不幸的是,正则表达式更难更改,因为复杂的正则表达式是晦涩难懂的。