本文罗列了面向对象(OO)程序设计的九个原则(principle)。我们在讲设计模式的时候,要先讲OO原则,千万不要轻视这些OO原则,因为每个设计模式背后都包含了几个OO原则的概念。很多时候,在设计时有两难的情况下,我们必须回归到OO原则,以方便取舍,可以这么说,OO原则是我们的目标,而设计模式则是我们的做法。
原则1: 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
该原则我暂且称之为封装变化原则。在软件开发领域有一个不变的真理是,不管当初软件设计得多好,一段时间后,总是需要成长与改变。该原则的另外一个思考方式是,把会变化的部分取出并封装起来,以便以后可以轻易地改动或者扩充该部分,而不影响不需要变化的其他部分。
原则2: 针对接口编程,而不是针对实现编程
针对接口编程,实际上是针对超类型(supertype)编程。针对超类型编程,关键就在多态,利用多态,程序可以针对超类型编程,执行时会根据实际对象的类型执行真正的行为,不会被绑死在超类型的行为上。“针对超类型编程”这句话,可以更加明确地说成“变量的声明类型应该是超类型,通常是一个抽象类或者一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以赋值给这个变量。”
举个多态的例子,假设有一个抽象类Animal,有两个具体的实现(Dog与Cat)继承Animal,类图如下:
针对实现编程:
Dog d = new Dog();
d.bark();
针对接口编程:
Animal animal = new Dog();
animal.makeSound();
原则3: 多用组合,少用继承
组合(composition)表示的是"has-a"关系,继承表示的是"is-a"关系。继承可以让一类事物的所有行为都保持一致。使用组合建立系统具有更大的弹性,更可以在运行时动态地改变行为。
注意:
这里的组合不是严格意义上的UML组合概念。对于UML组合与聚合的区别可参考文章聚合与组合。
原则4: 为了交互对象之间的松耦合设计而努力
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。典型的设计就是观察者模式,可参考文章《HeadFirst设计模式》读书笔记-第2章-观察者模式。
原则5: 类应该对扩展开放,对修改关闭
这个原则叫开放-关闭原则。我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可以搭配新的行为,这样的设计具有弹性,可以接受新的功能来应对变化的需求。
在选择需要被扩展的那部分的代码时要小心。每个地方都使用开放-关闭原则是一种浪费,也没有必要,还会导致代码变得复杂且难以理解。你需要把注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则。
原则6: 依赖倒置原则
这个原则是指要依赖抽象,不要依赖具体类,和原则2非常的相似,然而这里更强调抽象,原则2更强调多态。概念比较抽象,举个例子吧。
public class DependentPizzaStore {
public Pizza createPizza(String style, String type) {
Pizza pizza = null;
if (style.equals("NY")) {
if (type.equals("cheese")) {
pizza = new NYStyleCheesePizza();
} else if (type.equals("veggie")) {
pizza = new NYStyleVeggiePizza();
} else if (type.equals("clam")) {
pizza = new NYStyleClamPizza();
} else if (type.equals("pepperoni")) {
pizza = new NYStylePepperoniPizza();
}
} else if (style.equals("Chicago")) {
if (type.equals("cheese")) {
pizza = new ChicagoStyleCheesePizza();
} else if (type.equals("veggie")) {
pizza = new ChicagoStyleVeggiePizza();
} else if (type.equals("clam")) {
pizza = new ChicagoStyleClamPizza();
} else if (type.equals("pepperoni")) {
pizza = new ChicagoStylePepperoniPizza();
}
} else {
System.out.println("Error: invalid type of pizza");
return null;
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
DependentPizzaStore创建了各种Pizza产品,它们都是Pizza的具体类。Pizza的定义如下,
import java.util.ArrayList;
public abstract class Pizza {
String name;
String dough;
String sauce;
ArrayList toppings = new ArrayList();
void prepare() {
System.out.println("Preparing " + name);
System.out.println("Tossing dough...");
System.out.println("Adding sauce...");
System.out.println("Adding toppings: ");
for (int i = 0; i < toppings.size(); i++) {
System.out.println(" " + toppings.get(i));
}
}
void bake() {
System.out.println("Bake for 25 minutes at 350");
}
void cut() {
System.out.println("Cutting the pizza into diagonal slices");
}
void box() {
System.out.println("Place pizza in official PizzaStore box");
}
public String getName() {
return name;
}
public String toString() {
StringBuffer display = new StringBuffer();
display.append("---- " + name + " ----\n");
display.append(dough + "\n");
display.append(sauce + "\n");
for (int i = 0; i < toppings.size(); i++) {
display.append((String )toppings.get(i) + "\n");
}
return display.toString();
}
}
DependentPizzaStore的依赖关系图如下,可以发现,该类依赖于所有的具体实现,任何一种Pizza的实现改变了,都可能需要改动到DependentPizzaStore。
下图给出了使用工厂模式后的依赖关系图,有关工厂模式可参考文章《HeadFirst设计模式》读书笔记-第4章-工厂模式。从图可以看出,低层组件现在依赖于高层的抽象,高层组件现在也依赖于相同的抽象。所以该原则也叫依赖倒置原则(dependency inversion principle)。
下面给出了几个指导方针,能够帮助我们避免在OO设计时违反依赖倒置原则:
- 变量不可以引用具体类的对象
使用new关键字一定会引用到具体类的对象,可以改用工厂模式来避开这样的做法。
- 不要让类派生自具体类
因为派生自具体的类,就依赖于具体类了,可以派生自一个抽象(接口或者抽象类)。
- 不要覆盖基类中已经实现的方法
如果基类已实现的方法,应该由所有的子类共享。
和其它的许多原则一样,应该尽量达到这个原则,而不是随时都遵守这个原则。
原则7: 只和你的密友谈话
这个原则叫最少知识(least knowledge)原则,也叫得墨忒耳法则(law of demeter),这个原则告诉我们要减少对象之间的交互,不要让太多的类耦合在一起,免得修改系统中的一部分,会影响到其它部分。外观模式是说明这个原则的最好例子。
这个原则提供了一些方针,就任何对象而言,在该对象的方法内,我们应该只调用属于以下范围的方法:
-
该对象本身的方法
-
被当作方法的参数而传递进来的对象的方法
-
此方法所创建或者实例化的任何对象的方法
-
该对象成员变量所引用的对象的方法
试想,如果调用从另一个调用中返回的对象的方法,会增加我们直接认识对象的数目,如下面的代码片段,应该让station对象直接为我们做出请求,而不是通过thermometer。
不推荐的做法:
public float getTemp() {
// 该方法依赖两个类
Thermometer thermometer = station.getThermometer();
return thermometer.getTemperature();
}
原则推荐的做法:
public float getTemp() {
// 该方法依赖一个类
return station.getTemperature();
}
虽然这个原则会减少对象之间的依赖,研究显示这会减少软件的维护成本。但是采用这个原则会导致更多的“包装”类被制造出来,以处理和其他组件的沟通,这可能会导致复杂度和开发时间的增加,并降低运行时的性能。因此,所有的设计都不免需要折衷,在抽象和速度之间取舍,在空间和时间之间平衡。
原则8: 高层组件对待低层组件的方式是“别调用我们,我们调用你”
这个原则叫做“好莱坞原则”,该原则把规定了组件的调用方式:低层组件绝不可以直接调用高层组件,允许低层组件将自己hook到系统上,但是由高层组件会决定什么时候和怎样使用这些低层组件。
模板方法模式里面提供的hook()方法就是非常明显符合该原则。模板方法定义的算法,子类如果实现hook()方法的话,就可以在合适的时候被调用。
好莱坞原则与依赖倒置原则的区别:
依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。好莱坞原则是用在创建框架或者组件的一种技巧,好让低层组件能够被挂钩到系统中,而且又不会让高层组件依赖低层组件。
两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。好莱坞原则提供了一个技巧,创建一个有弹性的设计,允许低层结构能够相互操作,而又防止其他类太过依赖它们。
原则9: 一个类应该只有一个引起变化的原因
这个原则叫单一责任原则,如果一个类具有两个改变的原因,那么未来修改这个类的几率就提高了,这个原则是说,一个类只做一件事情。
记得在大学上软件工程课时,有一句话也表达了类似的观点:高内聚,低耦合。内聚(cohesion)是用来度量一个类或者模块紧密地达到单一目的或责任。当一个模块或者类被设计成只支持一组相关的功能时,我们说它具有高内聚,反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。
体现该原则的设计模式如迭代器模式。
原则10:
未完,待续