[关闭]
@DevWiki 2015-07-02T21:15:20.000000Z 字数 4989 阅读 2361

Java设计模式原则---封装变化和面向接口编程

设计模式


更多内容详见:Java设计模式学习笔记

最近看了Head First 设计模式一书,开篇的故事讲述了设计模式的原则:封装变化面向接口编程.

基本需求

故事从编写一个模拟鸭子的游戏开始,游戏要求:

游戏里有许多鸭子,一边游泳戏水,一边呱呱叫...

该游戏内部使用面向对象设计,有一个鸭子的超类Duck:

  1. public abstract class Duck{
  2. public void swim(){
  3. //游泳的方法
  4. }
  5. public void quack(){
  6. //呱呱叫的方法
  7. }
  8. public abstract void display(){
  9. //子类要实现的显示的方法
  10. }
  11. }

因为所有的鸭子都会游泳和叫,所以在超类中实现了swim()和qucak()方法,而具体显示出什么样和具体的鸭子有关,所以display()方法为抽象方法.

现在有种鸭子是红头鸭RedHeadDuck和绿头鸭MallardDuck.
红头RedHeadDuck代码:

  1. public class RedHeadDuck extends Duck {
  2. public void display() {
  3. System.out.println("我是红头鸭...");
  4. }
  5. }

绿头鸭MallardDuck代码:

  1. public class MallardDuck extends Duck {
  2. public void display() {
  3. System.out.println("我是绿头鸭...");
  4. }
  5. }

需求变化

现在需求发生了变化,想要鸭子能飞行...那不是很简单嘛,给Duck类加个飞行的方法不就可以了,如下:

  1. public abstract class Duck{
  2. public void swim(){
  3. //游泳的方法
  4. }
  5. public void quack(){
  6. //呱呱叫的方法
  7. }
  8. public void fly(){
  9. //飞行的方法
  10. }
  11. public abstract void display(){
  12. //子类要实现的显示的方法
  13. }
  14. }

这样一来,确实绿头鸭和红头鸭都会飞行了.

出现问题

由于公司业务需要,增加橡皮鸭这一角色RubberDuck,如下:

  1. public class RubberDuck extends Duck {
  2. public void display() {
  3. System.out.println("我是橡皮鸭...");
  4. }
  5. }

等等,上面的橡皮鸭貌似不对啊,橡皮鸭不会飞啊!而且橡皮鸭是吱吱叫不是呱呱叫.这该怎么办呢?

这还不简单,直接覆盖方法不就行了.

  1. public class RubberDuck extends Duck {
  2. public void qucak(){
  3. //吱吱叫...
  4. }
  5. public void fly(){
  6. //什么也不做...
  7. }
  8. public void display() {
  9. System.out.println("我是橡皮鸭...");
  10. }
  11. }

这样貌似是解决了,但是问题又来来,如果后来需要增加诱饵鸭DecoyDuck,诱饵鸭不会叫不会飞.怎么办?难道还要继续覆盖方法么?

解决问题

既然无法确定以后的鸭子是什么类型,干脆抽取公共的部分,不同的写成接口.
比如会飞的实现Flyable接口,会叫的实现Qucakable接口.

  1. //会飞的接口
  2. public interface Flyable{
  3. void fly();
  4. }
  5. //会叫的接口
  6. public interface Quackable{
  7. void quack();
  8. }
  9. //新的Duck类
  10. public abstract class Duck{
  11. public void swim(){
  12. //游泳的方法
  13. }
  14. public abstract class display(){
  15. //显示的方法
  16. }
  17. }
  18. //新的绿头鸭
  19. public class MallardDuck extend Duck implements Flyable, Qucakable {
  20. public void fly(){
  21. //我会飞...
  22. }
  23. public void quack(){
  24. //我会呱呱叫...
  25. }
  26. public void display(){
  27. //我是绿头鸭
  28. }
  29. }
  30. //新的红头鸭类
  31. public class RedHeadDuck extend Duck implements Flyable, Qucakable {
  32. public void fly(){
  33. //我会飞...
  34. }
  35. public void quack(){
  36. //我会呱呱叫...
  37. }
  38. public void display(){
  39. //我是红头鸭
  40. }
  41. }
  42. //橡皮鸭
  43. public class RubberDuck extend Duck implements Quackable {
  44. public void quack(){
  45. //我会吱吱叫...
  46. }
  47. public void display(){
  48. //我是橡皮鸭
  49. }
  50. }
  51. //诱饵鸭
  52. public class DecoyDuck extends Duck {
  53. public void display(){
  54. //我是诱饵鸭
  55. }
  56. }

这样一来,问题就解决了.

新问题

上面的问题是解决了,好像代码有重复:
绿头鸭和红头鸭的会飞的方法和会呱呱叫的方法是重复的.

如果以后有更多类型的方法,重复的代码会更多,而且会埋下一个隐患:

如果以后飞行的动作有所改变,难道一个一个类的去修改?
如果需求还有变化,不是更难维护吗?

解决问题

有没有好的方法解决这个问题呢?答案是肯定的.我们需要将代码中的变化的部分与不变的部分拆分出来.这就是封装变化的原则

封装变化

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

下面就建立两组类,变化的和不会变化的.

上面的案例中什么是变化的呢?
飞行叫声是变化的.那么就将飞行和叫声与Duck类分开.

如何设计鸭子的飞行行为和叫声行为呢?
我们希望一切有弹性,因为你无法确定以后的飞行行为会有什么变化,也无法确定以后的绿头鸭会有什么行为.

这就涉及到第二个原则:面向接口编程

面向接口编程

针对接口编程,而不是针对实现编程

那么现在的需求有两个行为:飞和叫.
接口就为飞行行为接口和叫的行为接口:

  1. //飞行行为接口
  2. public interface FlyBehaviour{
  3. void fly();
  4. }
  5. //叫的行为接口
  6. public interface QuackBehaviour{
  7. void quack();
  8. }

现在飞行有种不同的行为:飞和不会飞.

  1. //普通的飞
  2. public class FlyWithWings implements FlyBehaviour {
  3. public void fly(){
  4. System.out.println("我会飞...");
  5. }
  6. }
  7. //不会飞
  8. public class FlyNoWay implements FlyBehaviour {
  9. public void fly(){
  10. //我不会飞...
  11. }
  12. }

现在叫也有三种行为:呱呱叫和吱吱叫和不会叫

  1. //呱呱叫
  2. public class Quack implements QuackBehaviour {
  3. public void quack(){
  4. System.out.println("我会呱呱叫...");
  5. }
  6. }
  7. //吱吱叫
  8. public class Squack implements QuackBehaviour {
  9. public void quack(){
  10. System.out.println("我会吱吱叫...");
  11. }
  12. }
  13. //不会叫
  14. public class MuteQuack implements QuackBehaviour {
  15. public void quack(){
  16. //我不会叫...
  17. }
  18. }

这样写的好处就在于,使用飞行行为时只需指定会飞行,不需绑定具体飞行的动作,弹性空间较大.而且此处的面向接口编程,并不是狭义上指Java中的接口,而是指超类型,可以是接口也可以是抽象类.

那么如何将行为和Duck类组合到一起呢?

将行为转为属性

即将飞行和叫的行转为鸭子的一个变量

  1. public abstract class Duck {
  2. //鸭子不处理飞的行为,将飞的行为委托给FlyBehaviour接口
  3. FlyBehaviour flyBehaviour;
  4. //鸭子不处理叫的行为,将飞的行为委托给QucakBehaviour接口
  5. QucakBehaviour quackBehaviour;
  6. public void performFly(){
  7. flyBehaviour.fly();
  8. }
  9. public void performQuack(){
  10. quackBehaviour.quack();
  11. }
  12. public void swim(){
  13. System.out.println("我会游泳...");
  14. }
  15. public abstract void display();
  16. }

再来看看绿头鸭,

  1. public class MallardDuck extends Duck {
  2. public MallardDuck(){
  3. flyBehaviour = new FlyWithWings();
  4. quackBehaviour = new Quack();
  5. }
  6. public class void display(){
  7. System.out.println("我是绿头鸭...");
  8. }
  9. }

现在测试一下:

  1. public class Client{
  2. public static void main(String[] args){
  3. Duck duck = new MallardDuck();
  4. duck.display();
  5. duck.performFly();
  6. duck.performQuack();
  7. duck.swim();
  8. }
  9. }

执行后结果如下:

  1. 我是绿头鸭...
  2. 我会飞...
  3. 我会呱呱叫...
  4. 我会游泳...

如何实现动态改变鸭子的行为呢?修改Duck类如下:

  1. public abstract class Duck {
  2. //鸭子不处理飞的行为,将飞的行为委托给FlyBehaviour接口
  3. FlyBehaviour flyBehaviour;
  4. //鸭子不处理叫的行为,将飞的行为委托给QucakBehaviour接口
  5. QucakBehaviour quackBehaviour;
  6. public void setFlyBehaviour(FlyBehaviour flyBehaviour){
  7. this.flyBehaviour = flyBehaviour;
  8. }
  9. public void setQucakBehaviour(QucakBehaviour quackBehaviour){
  10. this.quackBehaviour = quackBehaviour;
  11. }
  12. public void performFly(){
  13. flyBehaviour.fly();
  14. }
  15. public void performQuack(){
  16. quackBehaviour.quack();
  17. }
  18. public void swim(){
  19. System.out.println("我会游泳...");
  20. }
  21. public abstract void display();
  22. }

现在构建一个模型鸭ModelDuck

  1. public class ModelDuck extends Duck{
  2. public ModelDuck(){
  3. flyBehaviour = new FlyNoWay(); //一开始不会飞
  4. quackBehaviour = new Quack();
  5. }
  6. public void display(){
  7. System.out.println("我是模型鸭...");
  8. }
  9. }

新建一个新的飞行行为:FlyRocketPowered

  1. public class FlyRocketPowered implements FlyBehaviour{
  2. public void fly(){
  3. System.out.println("我能像火箭一样飞...");
  4. }
  5. }

现在测试一下动态改变飞行行为:

  1. public class Client{
  2. public static void main(String[] args){
  3. Duck duck = new ModelDuck();
  4. duck.display();
  5. duck.performFly();
  6. duck.setFlyBehaviour(new FlyRocketPowered());
  7. duck.performFly();
  8. }
  9. }

测试结果:

  1. 我是模型鸭...
  2. 我会向火箭一样飞...

这样就实现了行为与类分开,及变化的部分与不变化的部分分开了.

小总结

变化的部分
飞行的行为和叫的行为

不变的部分
鸭子会有用,拥有飞行和叫的行为.

总结

封装变化和面向接口编程能让代码有很大的弹性,在代码不变或者很小的改变的情况下满足需求的变化,也易于维护.

更多内容请移步我的博客:DevWiki的博客

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注