首先我们来设定一下场景,Link上班的公司,设计了一款游戏,游戏中会出现各种人,人们会说话,这是基本的Person类,采用OO的技术,设计成了一个superclass,并让各种不同的人类继承这个超类。
初始版0.0
通常的做法是设计成如下的样子:
具体的程序的定义的类的代码如下,其中的show方法,是因为每个人的外观不同,所以基类的show方法是抽象的方法;所有的人都会说话(speak )。具体的代码定义如下:
Person.java
package wyhllk.blog.csdn.com;
public abstract class Person {
public void speak() {
System.out.println("speak English");
}
public void otherProperties() {
System.out.println("otherProperties");
}
abstract void show();
}
American.java
package wyhllk.blog.csdn.com;
public class American extends Person {
@Override
void show() {
System.out.println("American show()");
}
}
Britisher.java
package wyhllk.blog.csdn.com;
public class Britisher extends Person {
@Override
void show() {
System.out.println("Britisher show()");
}
}
package wyhllk.blog.csdn.com;
public class Chinese extends Person {
@Override
public void speak() {
System.out.println("speak Chinese");
}
@Override
void show() {
System.out.println("Chinese show()");
}
}
Robot.java
package wyhllk.blog.csdn.com;
public class Robot extends Person {
@Override
void show() {
System.out.println("Robot show()");
}
}
对代码所做的局部修改,影响层面可不只是局部!
进阶版1.0
Link想到的办法是在原来的Robot中重写eat()办法,变成什么也不做,把原来的基类中的eat()方法覆盖掉,就像Chinese中覆盖speak办法一样。
可是这样做会有一个问题:假如以后要加入一个木头人(Wooden ),不会说(speak),也不会吃(eat),那该如何呢?
进阶版2.0
Link认识到继承可能解决不了这个问题,产品的需求可能会经常的改变,每当有新人类出现的时候,他就需要被迫的检查并可能需要覆盖speak和eat()……这将是一个无尽的噩梦。所以,Link需要一个更加清晰的方法,让“某些”(而不是全部)人类或可speak或eat
Link的办法是,把speak和eat从基类中抽取出来,放到一个Speakable接口和Eatable接口中,这样一来,只有会说的人和会吃的人才需要实现这个接口,这样就变成了下图所示的情况:
但是这样又存在问题:
- 如果原来已经实现了很多的子类,那么每个类都需要修改eat的行为
- 这样会造成很多的代码重复,无法的复用
进阶版3.0
通过上面的几种尝试,Link知道了,使用继承并不能够很好的解决问题,因为人的行为在子类里面不断的改变,并且让所有的子类都有这些行为是不恰当的。Speakable接口和Eatable接口,一开始的时候似乎还是不错的,解决了问题(只有会吃的人才继承Eatable接口),但是Java的接口不具有实现代码,所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,你必须得往下追踪并在每个定义此行为的类中修改它,一不小心,可能会造成新的错误。
设计原则1:
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
换句话说:
如果每次新的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其他稳定的代码有所区分。
那么现在按照这个原则设计
抽离变化和不变化的部分
我们知道Person类内的speak和eat会随着人的不同而改变,为了分开变化和不会变化的部分,我们建立两组类,一个是speak相关的,一个是eat相关的,每组类将实现各自的动作。比方说,我们实现一个类实现“speak English”,一个类实现“speak Chinese”,一个类实现“quite”。
设计Person的行为
如何设计那组实现speak和eat的行为的类呢?
目标:一切都有弹性,指定行为到人的实例。比方说,我们想要产生一个新的American实例,并指定特定的“类型”的eat给他。也就是说人的行为可以动态的改变。在换句话说,我们应该在Person类中包含设定行为的方法,这样就可以在“运行时”动态“改变”American的eat的行为。
设计原则2:
针对接口编程,而不是针对实现编程
我们利用接口代表每个行为,比方说,EatBehavior与SpeakBehavior,而行为的每个实现都将实现其中的一个接口。
所以这次Person类不会负责实现Eatable和Speakable接口,反而是由我们制造一组其他类专门实现EatBehavior与SpeakBehavior,这就成为“行为”类。由行为类而不是Person类来实现行为接口。
之前的做法是:行为来自Person超类的具体实现,或是继承某个接口并由子类实现而来。这两种做法都是依赖于“实现”,我们被绑的死死的,没有办法改变行为(除非写更多的代码)。
现在我们的设计中,Person的子类使用接口(EatBehavior与SpeakBehavior)所表现的行为,所以实际的“实现”不会被绑死在Person的子类中。换句话说,特定的具体实现行为编写在实现了EatBehavior与SpeakBehavior类中了。
实现Person的行为
设计的类图如下:这样设计可以让speak和eat的动作被其他的对象复用,因为这些行为已经和Person无关了。而我们新增的一些行为不会影响到既有的行为类,也不会影响“使用”到eat行为的人类。
整合这些行为
关键在于,Person现在会将speak和eat的动作“委托”别人处理,而不是使用Person类(或子类)内的speak和eat方法。
- 首先,在Person类中“加入两个实例变量”,分别为“speakBehavior”和“eatBehavior”,声明为接口类型(而不是具体类实现类型),每个人类对象都会动态地设置这些变量一种以在运行时引用正确的行为类型(例如:EatWithMouse、SpeakChinese等)。
我们也必须将Person类与其子类中的speak()和eat删除,因为这些行为已经搬到了SpeakBehavior和EatBehavior类中了我们用两个相似的方法performEat()和performSpeak()取代Person类中的eat()和speak()。
- 现在,实现performEat():
package wyhllk.blog.csdn.com;
public abstract class Person {
EatBehavior eatBehavior;
public void performEat(){
eatBehavior.eat();
}
public void speak() {
System.out.println("speak English");
}
//还有其他的
}
- 现在,整合所有的代码如下:
package wyhllk.blog.csdn.com;
public abstract class Person {
EatBehavior eatBehavior;
SpeakBehavior speakBehavior;
public void performEat() {
eatBehavior.eat();
}
public void performSpeak() {
speakBehavior.speak();
}
public void otherProperties() {
System.out.println("otherProperties");
}
abstract void show();
}
EatBehavior.javapackage wyhllk.blog.csdn.com;
public interface EatBehavior {
void eat();
}
EatWithMouse.java
package wyhllk.blog.csdn.com;
public class EatWithMouse implements EatBehavior {
@Override
public void eat() {
System.out.println("I can eat!");
}
}
EatNoWay.javapackage wyhllk.blog.csdn.com;
public class EatNoWay implements EatBehavior {
@Override
public void eat() {
System.out.println("I can't eat");
}
}
SpeakBehavior.java
package wyhllk.blog.csdn.com;
public interface SpeakBehavior {
void speak();
}
SpeakEnglish.javapackage wyhllk.blog.csdn.com;
public class SpeakEnglish implements SpeakBehavior {
@Override
public void speak() {
System.out.println("speak English");
}
}
SpeakChinese.javapackage wyhllk.blog.csdn.com;
public class SpeakChinese implements SpeakBehavior {
@Override
public void speak() {
System.out.println("speak Chinese");
}
}
SpeakQuite.javapackage wyhllk.blog.csdn.com;
public class SpeakQuite implements SpeakBehavior {
@Override
public void speak() {
System.out.println("<<quite>>");
}
}
编写的新的人类,外星人
Alien.java
package wyhllk.blog.csdn.com;
public class Alien extends Person {
public Alien() {
eatBehavior = new EatWithMouse();
speakBehavior = new SpeakEnglish();
}
@Override
void show() {
System.out.println("I am Alien");
}
}
最后编写一个测试类package wyhllk.blog.csdn.com;
public class MiniPersonSimulator {
public static void main(String[] args) {
Person person = new Alien();
person.performEat();
person.performSpeak();
}
}
运行的结果如下:终极版
上面虽然实现了功能,但是并没有实现运行时的动态的设定行为。
- 要实现这个功能,只需要在Person类中加入两个setter方法即可:
public void setEatBehavior(EatBehavior eatBehavior) {
this.eatBehavior = eatBehavior;
}
public void setSpeakBehavior(SpeakBehavior speakBehavior) {
this.speakBehavior = speakBehavior;
}
这样就可以“随时”调用这个方法改变人类的行为了。
- 可以制造一个模型人
package wyhllk.blog.csdn.com;
public class ModelPerson extends Person {
public ModelPerson() {
eatBehavior = new EatNoWay();
speakBehavior = new SpeakEnglish();
}
@Override
void show() {
System.out.println("I am ModelPerson");
}
}
MiniPersonSimulator.java
package wyhllk.blog.csdn.com;
public class MiniPersonSimulator {
public static void main(String[] args) {
Person model = new ModelPerson();
model.performSpeak();
model.setSpeakBehavior(new SpeakChinese());
model.performSpeak();
}
}
测试的结果:
总结
通过上面的一系列的演变过程,我们得到了一个全新的设计之后的类结构,Link所期望的一切都有:人类继承Person,吃的行为实现EatBehavior接口,说的行为实现SpeakBehavior接口。
下面注意来点儿专业术语了。不再把人类的行为说成是“一组行为”,我们开始把行为想成是“一族算法”。想想看,在整个设计当中,算法代表人类能做的事(不同的吃法和说法)。
设计原则3:
多用组合,少用继承
通过上面的过程,我们抽象出一般的模型:
策略模式
定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
参考文献:
《Head First设计模式》
《设计模式-可复用面向对象软件的基础》