chapter11:面向可复用性和可维护性的设计模式
设计模式更强调多个类和对象之间的关系和交互过程,比接口/类的复用粒度更大,设计模式分为三种类型:创建型模式、结构型模式、行为类模式。
创建型模式:
1.工厂方法模式:也叫做虚拟构造器。当用户不知道应该创建哪个具体类的实例时,或者不想在客户端指明要具体创建的实例时使用。定义一个用于创建对象的接口,让该接口的子类型来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
可以在ADT内实现,也可以独立为一个类
public class SystemTraceFactory{
public static Trace getTrace() {
return new SystemTrace();
} }
public class TraceFactory {
public static Trace getTrace(String type) {
if(type.equals("file")
return new FileTrace();
else if (type.equals("system")
return new SystemTrace();
} }
优势:
1. 静态工厂方法可具有指定的更有意义的名称
2. 不必在每次调用的时候都创建新的工厂对象
结构型模式:
1.适配器模式:
将某个类/接口转换为用户期望的其他形式,通过增加一个接口,将已经存在的子类封装起来,客户端只需要对接口编程,从而实现隐藏子类,具体技术是继承和委托。可以用于解决类之间接口不兼容的问题。
interface Shape {
void display(int x1, int y1, int x2, int y2);
}
class Rectangle implements Shape {//适配器类实现抽象接口
void display(int x1, int y1, int x2, int y2) {
new LegacyRectangle().display(x1, y1, x2-x1, y2-y1);
}
}
class LegacyRectangle {//具体实现方法的适配
void display(int x1, int y1, int w, int h) {...}
}
class Client {//对抽象接口进行编程,达到与实现方法的隔离
Shape shape = new Rectangle();
public display() {
shape.display(x1, y1, x2, y2);
}
}
2.装饰器模式:
对每一个特性都构造一个子类,通过委托机制增加到对象上。可以为对象增加不同的侧面特性,一般以递归实现。基于公共功能构造一个接口,然后实现之,并通过委托增加侧面功能,之后可以继续继承增加新功能。
总之,Decorator在运行时合成特性,继承在编译时组合功能;装饰器由多个协作对象组成,继承生成一个单一的、类型明确的对象;可以混搭多种装饰,多重继承在概念上很困难。
行为类模式:
1.策略模式:
为不同的实现算法构造抽象接口,利用委托机制,在运行时客户端根据需要将想要的策略传入算法类实例。
2.模板模式:
某些功能做事情的步骤一样,但是具体方法不同,这个时候我们可以将共性的步骤在抽象类种实现,差异化的步骤在子类中体现。要用到继承和重写技术。
public abstract class CarBuilder {
protected abstract void BuildSkeleton();
protected abstract void InstallEngine();
protected abstract void InstallDoor();
// Template Method that specifies the general logic
public void BuildCar() { //通用逻辑
BuildSkeleton();
InstallEngine();
InstallDoor();
}
}
//两个继承上述模板的子类
public class PorcheBuilder extends CarBuilder {
protected void BuildSkeleton() {
System.out.println("Building Porche Skeleton");
}
protected void InstallEngine() {
System.out.println("Installing Porche Engine");
}
protected void InstallDoor() {
System.out.println("Installing Porche Door");
}
}
public class BeetleBuilder extends CarBuilder {
protected void BuildSkeleton() {
System.out.println("Building Beetle Skeleton");
}
protected void InstallEngine() {
System.out.println("Installing Beetle Engine");
}
protected void InstallDoor() {
System.out.println("Installing Beetle Door");
}
}
3.遍历器模式:
期望对放入容器中的ADT类型进行遍历访问且不关心其具体类型时使用。
Java中有一个可遍历接口、迭代器接口,设计一个集合类实现可迭代接,实现自己独特的迭代器(根据需要决定是否重写hasNext、Next、remove方法)
public class Pair<E> implements Iterable<E> {
private final E first, second;
public Pair(E f, E s) { first = f; second = s; }
public Iterator<E> iterator() {
return new PairIterator();
}
private class PairIterator implements Iterator<E> {
private boolean seenFirst = false, seenSecond = false;
public boolean hasNext() { return !seenSecond; }
public E next() {
if (!seenFirst) { seenFirst = true; return first; }
if (!seenSecond) { seenSecond = true; return second; }
throw new NoSuchElementException();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}
4.访问者模式:
对特定类型的特定操作,将二者动态绑定到一起,这个操作可以灵活改变不影响被操作类。本质上就是把数据和它的操作分离开
为ADT预留一个将来可扩展功能的接入点,外部实现的功能代码可以在不该变ADT的情况下在需要时通过委托机制接入ADT。
示例如下:
策略模式和访问者模式都是通过委托建立两个对象的动态联系, 但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。
二者区别在于visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置
几种模式的实例与一般设计的对比:
chapter12:面向正确性与健壮性的软件构造技术
正确性:设计是否符合规约
健壮性:软件在输入异常或在外部环境异常的状况下仍能表现正常的程度
程序员应该总是考虑用户会输入任何信息(尤其是有危害的)
面向健壮性编程:处理未期望的行为和错误终止,目的是为了尽可能保持程序的运行状态,若终止也要准确向用户展示全面的错误信息,有助于debug。容错,优化用户体验
面向正确性编程:程序按照规约执行的能力,永远不会给出错误结果,优化开发者体验
一般来说要尽可能让内外隔离,防止错误逸界扩散。对外接口要着重考虑健壮性,而对内具体实现要尽量考虑正确性。
提高正确性、健壮性的过程和技术:
健壮性、正确性的测量:
外部:
MTBF(描述可修复系统):故障平均间隔时间/平均无故障运行时间,用总运行时间/故障总次数,该计算依赖于对系统故障的定义
一般可修复系统认为导致系统不可使用的错误为系统故障,系统仍可运行则不认为是故障。
MTTF(描述不可修复系统):故障前平均时间
内部观察者角度(间接测量):代码的残余缺陷率,即每N行代码中遗留的bug数量。
Java中的Error和Exception:
具体分类如下,二者共为可抛出接口的实现类
Error:一般是内部错误,程序员无法处理,若发生应该想办法让程序优雅结束运行,Error类一般不需要实例化。包含用户输入错误、物理限制、设备错误。
异常:程序执行中的非正常事件,导致程序无法按照预期流程执行。一般是程序本身的错误,可以捕获、处理。应该将错误信息和异常发生的现场信息汇报给上层调用者。若无指定处理,直接退出,是return之外的第二种退出方法。
经典的Error:
虚拟机Error:超出内存范围、堆栈溢出、未知的内部错误
链接错误:类未找到等
Error + 运行时异常(一般是程序员处理不当)都是unchecked异常,可以不做处理。其他异常例如I/O异常、AWT异常,是checked异常,必须捕获并指定错误处理器,否则编译不通过。
unchecked与checked区别在于前者编译时不会提示,后者会有报错提示
异常处理:
用try{}包括执行代码,catch捕获异常并写处理代码,finally执行善后处理工作。
这样做的好处是能将正常的逻辑代码与处理异常代码区分开,结构更清晰。
当然也可以像一起C语言一样在逻辑代码之间穿插处理
但是不推荐
运行时异常的实例:越界、空指针操作、错误的类型引用/转换/操作,在代码中预防/处理可有效避免
其他异常实例:读取文件不存在、读取内容已过文本内容、类未找到,无能为力
通过客户端能否补救区分
因此对于unchecked异常我们没有捕获的必要,因为处理不来
对于checked异常,需要从异常类中派生出子类型
throw用于抛出某种异常(可以多个多种),在规约中@throw引出对抛出异常的要求描述,必须将所有可能会抛出的checked异常列举出来以便客户端处理
抛出异常之后,方法不再将它的控制权交给调用它的客户端,因此可以不考虑返回错误代码
throw new EOFException();
or
EOFException e = new EOFException();
throw e;
throws声明此方法内可能会生成的异常,声明位置在方法签名与实现体之间,应该包含调用的方法带来的异常和自身产生的异常
public static void main(String args[]) throws IOException {
FileInputStream fis = null;
fis = new FileInputStream("sample.txt");
int k;
while ((k = fis.read()) != -1)
System.out.print((char) k);
fis.close(); }
前面谈到的try-catch-finally:finally可以没有这里的try-catch-finally中都可以抛出异常,但是最好保留根源原因
public static void main(String args[]) {
FileInputStream fis = null;
try {
fis = new FileInputStream("sample.txt");
int c;
while ((c = fis.read()) != -1)
System.out.print((char) c);
fis.close(); }
catch (FileNotFoundException e)
{ e.printStackTrace(); }
catch (IOException e)
{ e.printStackTrace(); }
catch (Exception e)
{ e.printStackTrace(); } }
try {
access the database
}
catch (SQLException e) {//在catch中抛出其他类型异常
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
如果客户端对某种异常无法处理时,可以将之捕获并抛出一个unchecked异常将程序挂起等待处理
try{
..some code that throws SQLException
}catch(SQLException ex){
throw new RuntimeException(ex);
}
出现checked异常可以将之传递给调用者处理,但是最好在本地处理,以免扩散、混乱。
利用异常的构造函数将现场信息返回给调用者
回扣LSP原则,子类产生的异常不能超出父类规约定义的范畴。
若JDK预设的异常类中没有适合的目的异常,可以自定义异常(可以继承某个已有的异常)
public class FooException extends Exception {
public FooException() { super(); }
public FooException(String message) { super(message); }
public FooException(String message, Throwable cause) {
super(message, cause);
}
public FooException(Throwable cause) { super(cause); }
}
要利用异常的方法尽可能地提供多的信息,以方便定位和处理:如下
跟踪轨迹,便于定位异常和后续的分析:
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
analyze frame
断言:主要在开发阶段使用,检测某种假设是否成立,成立则可继续运行,否则会停止。在运行时会关闭,不影响性能,针对正确性发现问题,能快速发现错误并防止扩散,它是一种设计假设的文档化,在设计时应该只关注程序内的数据,程序无法掌控的不管。形式如下:
assert x >= 0;
//或者可以输出一些更详细的信息
assert x >= 0 : “x is ” + x;
assert false : "Unknown operator: " + operator;
断言可以用于检测前置条件(不一定合适)、后置条件是否满足规约,可以检测内部不变量、表示不变量是否变为false,也可以在控制流中检测错误输入(default,但是这种情况异于其他情况,程序运行时可能需要到达这一步,因此不能直接使用断言,最好应该抛出异常
default: throw new AssertionError("must be a vowel, but was: " + vowel);)
综上,外部错误应该抛出异常,开发阶段时可以用断言检测内部状态是否合格。异常处理是针对预料之外的事情,断言处理绝对不能发生的事情。因此对于方法使用的参数,不应该使用断言直接fail,这是客户端输入所致,我们无法控制,最好抛出异常。但是后置条件就取决于我们的程序,这边就可以使用断言
防御式编程:
大部分知识其实就是平时编程的一些习惯,处处设置一些检测机制。
对public的方法接收的外界的输入做充分的检验,设置”路障“,确保其合法且正确之后才可以使用,尤其是传给private方法之前。这个处理叫做隔离舱,隔离舱外部的函数用异常,内部用断言
现代的IDE都非常方便,很多bug都能给出提示(静态检查)