面向对象的设计原则

本文罗列了面向对象(OO)程序设计的九个原则(principle)。我们在讲设计模式的时候,要先讲OO原则,千万不要轻视这些OO原则,因为每个设计模式背后都包含了几个OO原则的概念。很多时候,在设计时有两难的情况下,我们必须回归到OO原则,以方便取舍,可以这么说,OO原则是我们的目标,而设计模式则是我们的做法。

原则1: 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起

该原则我暂且称之为封装变化原则。在软件开发领域有一个不变的真理是,不管当初软件设计得多好,一段时间后,总是需要成长与改变。该原则的另外一个思考方式是,把会变化的部分取出并封装起来,以便以后可以轻易地改动或者扩充该部分,而不影响不需要变化的其他部分。

原则2: 针对接口编程,而不是针对实现编程

针对接口编程,实际上是针对超类型(supertype)编程。针对超类型编程,关键就在多态,利用多态,程序可以针对超类型编程,执行时会根据实际对象的类型执行真正的行为,不会被绑死在超类型的行为上。“针对超类型编程”这句话,可以更加明确地说成“变量的声明类型应该是超类型,通常是一个抽象类或者一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以赋值给这个变量。”

举个多态的例子,假设有一个抽象类Animal,有两个具体的实现(Dog与Cat)继承Animal,类图如下:

![这里写图片描述](https://img-blog.csdn.net/20180415233252230?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JleF9uaWU=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

针对实现编程:

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。

![这里写图片描述](https://img-blog.csdnimg.cn/img_convert/f1e5bbbce1f4957936c0f4440e2d3a0a.png)

下图给出了使用工厂模式后的依赖关系图,有关工厂模式可参考文章《HeadFirst设计模式》读书笔记-第4章-工厂模式。从图可以看出,低层组件现在依赖于高层的抽象,高层组件现在也依赖于相同的抽象。所以该原则也叫依赖倒置原则(dependency inversion principle)

![这里写图片描述](https://img-blog.csdnimg.cn/img_convert/f447fb5ed29d9c3aca32f5d5ee22f905.png)

下面给出了几个指导方针,能够帮助我们避免在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:

未完,待续

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值