设计和架构的原则
命令者模式
命令者是一个对象,它封装了调用另一个方法的所有细节,命令者模式使用该对象,可以编写出根据运行期条件,顺序调用方法的一般化代码。
命令者模式中有四个类参与其中:
命令接收者
执行实际任务
命令者
封装了所有调用命令执行者的信息
发起者
控制一个或多个命令的顺序和执行
客户端
创建具体的命令者实例
1 2 3 4 5 6 7 8 9 10 11
| [发起者] -> [命令者]
↑ ↑ |创建 |实现
[客户端] -> [具体命令者]
调用| ↓
[命令接收者]
|
举个栗子:
假设有个 GUI Editor
组件,可以执行 open
、save
等一系列操作。
现在我们像实现宏功能——就是把一系列操作录下来,日后作为一个操作执行,这就是命令的接受者。
文本编辑器可能有的一般功能:
1 2 3 4 5 6 7
| public interface Editor { void save();
void open();
void close(); }
|
像 open
、save
这样的操作称为命令,我们需要一个统一的接口来概括这些不同的操作。
通过 Action
接口,所有操作均可实现:
1 2 3
| public interface Action { void perform(); }
|
现在让每个操作都实现该接口:
1 2 3 4 5 6 7 8 9 10 11 12
| public class Save implements Action { private final Editor editor;
Save(Editor editor) { this.editor = editor; }
@Override public void perform() { editor.save(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class Open implements Action { private final Editor editor;
Open(Editor editor) { this.editor = editor; }
@Override public void perform() { editor.open(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class Close implements Action { private final Editor editor;
Close(Editor editor) { this.editor = editor; }
@Override public void perform() { editor.close(); } }
|
实现一个宏:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Macro { private final List<Action> actions;
Macro() { actions = new ArrayList<>(); }
void record(Action action) { actions.add(action); }
void run() { actions.forEach(Action::perform); } }
|
别忘了实现一个具体的文本编辑器 EditorImpl
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class EditorImpl implements Editor { @Override public void save() { System.out.println("success save"); }
@Override public void open() { System.out.println("success open"); }
@Override public void close() { System.out.println("success close"); } }
|
现在就可以通过录制这些操作做一个宏,来方便自己的工作了:
命令者模式构建宏:
1 2 3 4 5
| Macro macro = new Macro(); macro.record(new Open(editor)); macro.record(new Save(editor)); macro.record(new Close(editor)); macro.run();
|
lambda
表达式构建宏:
1 2 3 4 5
| Macro macro = new Macro(); macro.record(() -> editor.open()); macro.record(() -> editor.save()); macro.record(() -> editor.close()); macro.run();
|
方法引用构建宏:
1 2 3 4 5
| Macro macro = new Macro(); macro.record(editor::open); macro.record(editor::save); macro.record(editor::close); macro.run();
|
宏只是使用使用命令者模式中的一个例子,它被大量用在实现组件化的图形界面系统、撤销功能、线程池、事务和向导中。
策略模式
策略模式能在运行时改变软件的算法模式。
其主要思想是定义一个通用的问题。使用不同的算法来实现,然后将这些算法都封装在统一接口的背后。
以文件压缩为例,我们为用户提供压缩各种文件的方式,可以使用 zip
算法,也可以使用 gzip
算法,我们实现一个通用的 Compressor
类,能用任何算法压缩文件。
首先,为策略定义 API
CompressionStrategy
,每种文件压缩算法都要实现该接口。
该接口有一个 compress
方法,接受并返回一个压缩后 OutputStream
对象。
1 2 3 4 5
| 压缩器 -调用-> 压缩策略 ↗ ↖ 实现/ \实现 / \ zip压缩 gzip压缩
|
定义压缩数据的策略接口:
1 2 3 4
| public interface CompressionStrategy { OutputStream compress(OutputStream data) throws IOException; }
|
使用 gzip
算法压缩数据:
1 2 3 4 5 6
| public class GzipCompressionStrategy implements CompressionStrategy { @Override public OutputStream compress(OutputStream data) throws IOException { return new GZIPOutputStream(data); } }
|
使用 zip
算法压缩数据:
1 2 3 4 5 6
| public class ZipCompressionStrategy implements CompressionStrategy { @Override public OutputStream compress(OutputStream data) throws IOException { return new ZipOutputStream(data); } }
|
压缩器 Compressor
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Compressor { private final CompressionStrategy strategy;
public Compressor(CompressionStrategy strategy) { this.strategy = strategy; }
public void compress(Path inFile, File outFile) throws IOException { try (OutputStream outputStream = new FileOutputStream(outFile)) { Files.copy(inFile, strategy.compress(outputStream)); } } }
|
到此就可以开始使用我们的压缩器来压缩文件了:
使用具体策略类初始化 Compressor
:
1 2 3 4 5
| Compressor gzipCompressor = new Compressor(new GzipCompressionStrategy()); gzipCompressor.compress(inFile, outFile);
Compressor zipCompressor = new Compressor(new ZipCompressionStrategy()); zipCompressor.compress(inFile, outFile);
|
使用方法引用初始化 Compressor
:
1 2 3 4 5
| Compressor gzipCompressor = new Compressor(GZIPOutputStream::new); gzipCompressor.compress(inFile, outFile);
Compressor zipCompressor = new Compressor(ZipOutputStream::new); zipCompressor.compress(inFile, outFile);
|
观察者模式
观察者模式是另一种可被 lambda
表达式简化和改进的行为模式。
在观察者模式中,被观察者持有一个观察者列表。当被观察者的状态发生改变,会通知观察者。
观察者模式被大量应用于基于 MVC
的 GUI
工具中,以此让模型状态发生变化时,自动刷新视图模块,达到二者之间的解耦。
举个栗子:
NASA 和外星人都对登陆到月球上的东西感兴趣,都希望可以记录这些信息。
NASA 希望确保阿波罗号上的航天员成功登月;外星人则希望在 NASA 注意力分散时进攻地球。
这里他们的观察对象就是登陆到月球的东西。
首先,定义观察者的 API
LandingObserver
,它只有 observeLanding
方法,当有东西登陆到月球上时会调用该方法:
1 2 3
| public interface LandingObserver { void observerLanding(String name); }
|
被观察者就是月球 Moon
,它持有一组 LandingObserver
实例,有东西着陆时会通知这些观察者,还可以增加新的 LandingObserver
实例观测 Moon
对象:
1 2 3 4 5 6 7 8 9 10 11
| public class Moon { private final List<LandingObserver> observers = new ArrayList<>();
public void land(String name) { observers.forEach(observer -> observer.observerLanding(name)); }
public void startSpying(LandingObserver observer) { observers.add(observer); } }
|
外星人观察到阿波罗号登陆月球,就开始发出进攻地球的信号:
1 2 3 4 5 6 7 8
| public class Aliens implements LandingObserver { @Override public void observerLanding(String name) { if (name.contains("Apollo")) { System.out.println("They're distracted, lets invade earth!"); } } }
|
NASA 观察到阿波罗号登陆到月球,会很兴奋:
1 2 3 4 5 6 7 8
| public class Nasa implements LandingObserver { @Override public void observerLanding(String name) { if (name.contains("Apollo")) { System.out.println("We made it!"); } } }
|
传统方式,就是使用以上写好的模版类 Aliens
和 Nasa
来调用:
1 2 3 4 5 6
| Moon moon = new Moon(); moon.startSpying(new Nasa()); moon.startSpying(new Aliens());
moon.land("An asteroid"); moon.land("Apollo 11");
|
但使用 lambda
表达式的话,就不用写以上的模版类了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Moon moon = new Moon(); moon.startSpying(name -> { if (name.contains("Apollo")) { System.out.println("We made it!"); } }); moon.startSpying(name -> { if (name.contains("Apollo")) { System.out.println("They're distracted, lets invade earth!"); } });
moon.land("An asteroid"); moon.land("Apollo 11");
|
注意:
无论是使用观察者还是策略模式,实现时采用 lambda
表达式,还是传统的类,取决于观察者和策略代码的复杂度。
这里举的例子很简单,所以更能展示新的语言特性。
使用 lambda 表达式的 SOLID 原则
SOLID
原则是涉及面向对象程序是的一些基本原则,分别是:
Single responsibility
Open/closed
Liskov substitution
Interface segregation
Dependency inversion
这里主要关注如何 lambda
表达式的环境下应用其中的三条原则。
单一功能原则
程序中的类或方法只能有一个改变的理由。
当软件的需求发生变化,实现这些功能的类和方法也需要变化。
如果你的类有多个功能,一个功能引起的代码变化会影响该类其他功能。这可能会引入缺陷,还会影响代码演进的能力。
举个栗子:
有一个程序,可以由资产列表生成 BalanceSheet
表格,然后输出一份 PDF
格式的报告。
如果实现时将制表和输出功能都放进同一个类,那么该类就有两个变化的理由。
你可能想改变输出功能,输出不同的格式,比如 HTML
,可能还想改变 BalanceSheet
的细节。
这将问题分解成两个类提供了很好的理由:一个负责将 BalanceSheet
生成表格,一个负责输出。
单一功能原则不止于此:一个类不仅要功能单一,而且还需要将功能封装好。
以上面的例子就是:如果我想改变输出格式,那么只需要改变负责输出的类,而不必关心负责制表的类。
这是强内聚性设计的一部分。说一个类是内聚的,是指它的方法和属性需要统一对待,因为它们紧密相关。
如果你试着将一个内聚的类拆分,可能会得到刚才创建的那两个类。
那么问题来了,这和 lambda
表达式有什么关系?
lambda
表达式在方法级别能更容易实现单一功能原则。
举个栗子:
计算质数个数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public long countPrimes(int upTo) { long total = 0; for (int i = 1; i < upTo; i++) { boolean isPrime = true; for (int j = 2; j < i; j++) { if (i % j == 0) { isPrime = false; } } if (isPrime) { total++; } } return total; }
|
显然,上面的方法塞了两个职责:判断一个数是否是质数、计数。
拆分这两个功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public long countPrimes(int upTo) { long total = 0; for (int i = 1; i < upTo; i++) { if (isPrime(i)) { total++; } } return total; }
public boolean isPrime(int num) { for (int i = 2; i < num; i++) { if (num % i == 0) { return false; } } return true; }
|
既然遵守单一功能原则,那么我们可以对迭代过程封装:
1 2 3 4 5 6 7 8 9 10
| public long countPrimes(int upTo) { return IntStream.range(1, upTo) .filter(this::isPrime) .count(); }
public boolean isPrime(int num) { return IntStream.range(2, num) .allMatch(x -> (num % x) != 0); }
|
如果我们想利用多核加速计数,可以使用 parallel
方法,而不用修改任何其他代码:
1 2 3 4 5 6
| public long countPrimes(int upTo) { return IntStream.range(1, upTo) .parallel() .filter(this::isPrime) .count(); }
|
开闭原则
软件应该对扩展开放,对修改闭合。
开闭原则的首要目标和单一功能原则类似:让软件易于修改。
一个新增功能或一处改动,会影响整个代码,容易引入新的缺陷。
开闭原则保证已有的类在不修改内部实现的基础上可扩展,这样就努力避免了上述问题。
举个栗子:
现在我们有个描述计算机花在用户空间、内核空间和输入输出上的时间散点图 MetricDataGraph
接口:
1 2 3 4 5 6 7
| public interface MetricDataGraph { void updateUserTime(int value);
void updateSystemTime(int value);
void updateIOTime(int value); }
|
但这个接口有点问题:对扩展不友好。因为要想添加新的时间点,比如 XXTime
,就要修改这个接口,添加对应的 updateXXTime
方法。
如何解决扩展问题呢?一般是通过引入抽象解决。
使用新的类 TimeSeries
来表示各种时间点,这样 MetricDataGraph
接口也得以简化,不必依赖某项具体指标。
1 2 3
| public interface MetricDataGraph { void updateTimeSeries(TimeSeries time); }
|
1 2 3
| public interface TimeSeries { int getValue(); }
|
每项具体指标都实现 TimeSeries
接口,在需要时能直接插入:
1 2 3 4 5 6 7 8
| public class UserTime implements TimeSeries { private int value;
@Override public int getValue() { return this.value; } }
|
现在,要添加新的时间点,比如,“被浪费的CPU时间”:
1 2 3 4 5 6 7 8
| public class WasteTime implements TimeSeries { private int value;
@Override public int getValue() { return this.value; } }
|
高阶函数也展示了同样的特性:对扩展开放,对修改闭合。
ThreadLocal
类有一个特殊变量,每个线程都有一个该变量的副本与之交互。该类的静态方法 withInitial
是一个高阶函数,传入一个负责生成初始值的 lambda
表达式。即不用修改 ThreadLocal
类就能获得新的行为,所以符合开闭原则。
给 withInitial
方法传入不同的工厂方法,就能得到有着不同行为的 ThreadLocal
实例。
比如,使用 ThreadLocal
生成一个 DateFormatter
实例,该实例是线程安全的:
1 2 3 4 5 6
| ThreadLocal<DateFormat> localFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat());
DateFormat formatter = localFormatter.get();
|
或者为每个 Java
线程创建唯一,有序的标识符:
1 2 3 4 5 6
| AtomicInteger threadId = new AtomicInteger();
ThreadLocal<Integer> localId = ThreadLocal.withInitial(() -> threadId.getAndIncrement());
int idForeThisThread = localId.get();
|
依赖反转原则
抽象不应依赖细节,细节应该依赖抽象。
该原则的目的:让程序猿脱离底层粘合代码,编写上层业务逻辑代码。这就让上层代码依赖于底层细节的抽象,从而可以重用上层代码。
这种模块化和重用方式是双向的:既可以替换不同的细节重用上层代码,也可以替换不同的业务逻辑重用细节的实现。
以下代码是从一种标记语言中提取标题,其中标题以冒号(:)结尾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public List<String> findHeadings(Reader input) { try (BufferedReader reader = new BufferedReader(input)) { return reader.lines() .filter(line -> line.endsWith(":")) .map(line -> line.substring(0, line.length() - 1)) .collect(toList()); } catch (IOException e) { throw new HeadingLookupException(e); } }
|
这段代码,将提取标题,资源管理,文件处理都混在了一起。我们真正想要的是编写提取标题的代码,而将操作文件相关细节交给另一个方法。
剥离文件处理功能后的业务逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public List<String> findHeadings2(Reader input) { return withLinesOf(input, lines -> lines.filter(line -> line.endsWith(":")) .map(line -> line.substring(0, line.length() - 1)) .collect(toList()), HeadingLookupException::new); }
private <T> T withLinesOf(Reader input, Function<Stream<String>, T> handler, Function<IOException, RuntimeException> error) { try (BufferedReader reader = new BufferedReader(input)) { return handler.apply(reader.lines()); } catch (IOException e) { throw error.apply(e); } }
|