用简单的例子说明提升可维护性的设计模式

  • Post author:
  • Post category:其他


此文写给想要理解设计模式,但认为《设计模式》中所举的例子过于复杂的读者。

前人的经验告诉我们:使用以下设计模式提升软件的可维护性:

创建型模式:将实例化过程抽象化

1.1 工厂方法

1.2 抽象工厂

1.3 建造者模式

结构型模式:通过组合类和对象来获得更大的结构

2.1 桥接模式

2.2 代理模式

2.3 组合模式

行为模式:涉及到算法和对象间职责的分配

3.1 观察者模式

3.2 访问者模式

3.3 备忘录模式

3.4 状态模式



1.1 工厂模式

工厂模式的原则就是:绝对不要让用户直接使用new操作来新建对象。

我们现在有产品接口、两个产品类:

public interface Fruit {
    void whatIm();
}

public class Apple implements Fruit {
    @Override
    public void whatIm() {
        //苹果
    }
}

public class Pear implements Fruit {
    @Override
    public void whatIm() {
        //
    }
}

先看看简单工厂:

public class FruitFactory {

    public Fruit createFruit(String type) {

        if (type.equals("apple")) {//生产苹果
            return new Apple();
        } else if (type.equals("pear")) {//生产梨
            return new Pear();
        }

        return null;
    }
}

它根据传入参数的不同生产不同的产品。但是如果有新增加的产品,就要修改工厂类了。有些人觉得这不妥,并且认为,每种产品需要有一个工厂。

于是我们需要把两种产品的new操作包装起来,写一个工厂接口,两个工厂对象:

public interface FruitFactory {
    Fruit createFruit();//生产水果
}

public class AppleFactory implements FruitFactory {
    @Override
    public Fruit createFruit() {
        return new Apple();
    }
}

public class PearFactory implements FruitFactory {
    @Override
    public Fruit createFruit() {
        return new Pear();
    }
}

这就是工厂方法了。

用户用如下方法使用:

AppleFactory appleFactory = new AppleFactory();
PearFactory pearFactory = new PearFactory();
Apple apple = (Apple) appleFactory.createFruit();//获得苹果
Pear pear = (Pear) pearFactory.createFruit();//获得梨



1.2 抽象工厂模式

现在我们有两种产品:CPU、屏幕

CPU有两种:850、650

屏幕有两种:大、小

所以我们一共有六个类:CPU接口、850CPU、650CPU、屏幕接口、大屏幕、小屏幕

现在写出抽象工厂接口、红米手机零件抽象工厂、小米手机零件抽象工厂

小米手机零件抽象工厂包含了850CPU工厂、大屏幕工厂的功能

红米手机零件抽象工厂包含了650CPU工厂、小屏幕工厂的功能

总结一下,抽象工厂就是把两种不同的工厂方法拼在一起。



1.3 建造者模式

我们的程序要模拟雷军的手机生产过程。

雷军的手机分为红米和小米,虽然里面的配件不一样,但是壳子都是一样的,所以不需要不同的类来表示。以下是表示Mi系列手机的类:

/**
 * Mi系列手机
 */
public class MiPhone {
  private String screen;
  private String cpu;
  
  /**
   * 安装屏幕
   * @param screen 将要安装的屏幕
   */
  public void setScreen(String screen) {
    this.screen = screen;
  }
  
  /**
   * 安装CPU
   * @param cpu 将要安装的CPU
   */
  public void setCpu(String cpu) {
    this.cpu = cpu;
  }
}

可以看到,可以使用set操作给手机装屏幕和CPU。但这个工作肯定不能交给用户做,所以我们需要用一个Builder类来完成手机制造和装配。

这个类相当于一台生产小米或者红米手机的机器。

先写一个抽象类来完成生产小米手机和红米手机都需要的步骤:制作外壳、交付手机

public abstract class MiPhoneBuilder {
  
  protected MiPhone phone;
  
  /**
   * 制作一个手机外壳
   */
  public void BuildAPhone() {
    this.phone = new MiPhone();
  }
  
  /**
   * 交付,返回装配好零件的手机
   * @return 制作好的手机
   */
  public MiPhone getPhone() {
    return this.phone;
  }
  
  /**
   * 给手机装配零件
   */
  public abstract void installScreen();
  public abstract void installCpu();
  
}

不同种手机需要装配不同的零件,这样的工作分别交给MiPhoneBuilder的不同的子类来做。装配零件的方法其实就是把产品类的Setter包装一下。

/**
 * 生产小米手机
 */
public class XiaomiBuilder extends MiPhoneBuilder {

  @Override
  public void installScreen() {
    this.phone.setScreen("5英寸大屏");
  }

  @Override
  public void installCpu() {
    this.phone.setCpu("高通骁龙830");
  }
  
}

/**
 * 生产红米手机
 */
public class RedmiBuilder extends MiPhoneBuilder {

  @Override
  public void installScreen() {
    this.phone.setScreen("4英寸小屏");
  }

  @Override
  public void installCpu() {
    this.phone.setCpu("高通骁龙650");
  }
  
}

然后,写一个BuildManager类,它相当于手机的生产商。将Builder与它组合,并用新方法将MiPhoneBuilder类的生产外壳和组装配件的方法连接起来,再包装一下getPhone方法,这样就能根据传入参数(相当于老板的命令)的不同来决定生产哪种手机了。

/**
 * 手机生产商
 */
public class BuildManager {

  MiPhoneBuilder builder;
  
  /**
   * 构造方法:确定该生产商的生产机器能够生产哪种手机
   */
  public BuildManager(MiPhoneBuilder builder) {
    this.builder = builder;
  }
  
  /**
   * 生产一台手机
   */
  public void buildNewPhone() {
    builder.BuildAPhone();
    builder.installCpu();
    builder.installScreen();
  }
  
  /**
   * 返回生产的手机
   * @return
   */
  public MiPhone getPhone() {
    return builder.getPhone();
  }
  
}

用户需要这样使用:

  public static void main(String[] args) {
    BuildManager manager = new BuildManager(new RedmiBuilder());
    manager.buildNewPhone();
    MiPhone phone = manager.getPhone();
  }



2.1 桥接模式

我们的程序需要模拟Mi手机的装配。Mi手机的外壳都是相同的,但具有不同的CPU。

/**
 * 抽象类:Mi手机
 */
public abstract class MiPhone {
  
  protected Cpu cpu;
  protected String name;
  
  /**
   * 构造方法:安装CPU
   * @param cpu
   */
  public MiPhone(Cpu cpu) {
    this.cpu = cpu;
  }
  
  public String getName() {
    return this.name;
  }
  
}

注意到MiPhone类组合了一个Cpu类的对象。

这个抽象类有两个具体子类:XiaomiPhone和RedmiPhone

Cpu是一个接口,有两种实现:Cpu830和Cpu650

用户使用时,可以将XiaomiPhone, RedmiPhone和两种Cpu任意组合,比如:

  public static void main(String[] args) {
    MiPhone xiaomiIV = new XiaomiPhone(new Cpu830());
    MiPhone redmiVI = new RedmiPhone(new Cpu650());
    MiPhone fakeXiaomi = new XiaomiPhone(new Cpu650());
    MiPhone fakeRedmi = new RedmiPhone(new Cpu830());
  }



2.2 代理模式

它更像一个提升性能的设计模式,使用了Lazy思想。

因为比较简单所以就直接用语言叙述了。

我们已有的东西是一个类和它的接口。这个类的构造方法里有一些需要耗费大量时间代价的东西,将会极大地影响性能。于是我们对这个接口新写一个实现类,这个新类中有一个对原类的引用。这个新类里面几乎什么都没有,所以创建它的时间代价很小。当用户不需要使用原类的方法时,就用这个新类“占位”就行(比如说塞到列表或集合里);当用户需要使用原类的方法时,这个新类创建一个原类的对象,然后将操作委派给原类的对象。



2.3 组合模式

在某个类的实例中,包含一个同类对象的集合,组成树形结构。



3.1 观察者模式

我们有一个万众瞩目的对象,我们称它为被观察者

它有很多观察它的人,我们称它们为观察者

使用方法:



3.2 访问者模式

https://www.jianshu.com/p/0fdf3470ff68

总结一下:某一个ADT拥有一个成员变量:列表,其中的元素包含某类的两个子类的对象。这个ADT有一个操作Action,它遍历列表元素,对每一个对象都干一些事情。但是我们希望这个操作Action能够干各种各样的事情,于是我们给这个ADT组合一个Visitor对象(Visitor是接口名),在Visitor的不同实现类中实现我们希望Action能干的各种各样的事情,然后将Action的操作委派给Visitor。调用Action时,传入对应的Visitor,就能进行对应的操作了。

这样的好处就是,新增操作时不用修改原来的代码,只需要新建一个Visitor实现类。



3.3 备忘录模式

本例中,我们的程序要模拟用着前去挑战恶龙的场景。

我们已经拥有一个勇者ADT:

public class Hero {
  
  int hp;
  int mp;
  String weapon;
  
  public Hero() {
    hp = 100;
    mp = 78;
    weapon = "天使的祝福之刃";
  }
  
  /**
   * 受伤:hp减少
   */
  public void getHurt() {
    hp -= 15;
  }
  
  /**
   * 死亡:hp归零
   */
  public void getKilled() {
    hp = 0;
  }
  
  /**
   * 使用技能:mp减少
   */
  public void useSkill() {
    mp -= 15;
  }
  
  /**
   * 意外:武器被偷走了
   */
  public void weaponStolen() {
    weapon = null;
  }
  
}

可以看到有很多操作可以改变勇者的状态。勇者一旦挑战失败,游戏就结束了。我们当然不想要从头开始游戏,所以在挑战恶龙之前我们需要存档,即保存勇者的当前状态。

1. 设计Memento类

Memento用来保存勇者的某个状态的信息。勇者的状态由hp, mp, weapon的取值决定,于是我们设计的Memento也需要有这三个成员变量。

/**
 * 勇者的备忘录
 */
public class HeroMemento {

  final int hp;
  final int mp;
  final String weapon;
  
  /**
   * 构造方法:根据勇者的状态信息新建一个备忘录
   */
  public HeroMemento(int hp, int mp, String weapon) {
    this.hp = hp;
    this.mp = mp;
    this.weapon = weapon;
  }
  
  /**
   * 从备忘录读取存档时的hp
   */
  public int getHp() {
    return this.hp;
  }
  
  /**
   * 从备忘录读取存档时的mp
   */
  public int getMp() {
    return this.mp;
  }
  
  /**
   * 从备忘录读取存档时的weapon
   */
  public String getWeapon() {
    return this.weapon;
  }
  
}

2. 新建MementoManager类来管理备忘录

这个有一个备忘录的列表,来管理各个存档点

/**
 * 存档管理器
 */
public class MementoManager {
  
  List<HeroMemento> saveData;
  // AF
  //   保存各个存档点
  
  /**
   * 构造方法
   */
  public MementoManager() {
    saveData = new ArrayList<>();
  }
  
  /**
   * 新增一条存档
   * @param memento
   */
  public void add(HeroMemento memento) {
    saveData.add(memento);
  }
  
  /**
   * 读取特定编号的存档
   */
  public HeroMemento get(int index) {
    return saveData.get(index);
  }
  
}

3. 将一个MementoManager对象组合到Hero中,然后给Hero添加save, recover方法

public class Hero {
  
  int hp;
  int mp;
  String weapon;
  MementoManager saveData = new MementoManager();

  /**
   * 保存当前状态到存档列表末位
   */
  public void save() {
    saveData.add(new HeroMemento(this.hp, this.mp, this.weapon));
  }
  
  /**
   * 恢复到指定存档的状态
   * @param index 指定存档的编号
   */
  public void restore(int index) {
    HeroMemento memento = this.saveData.get(index);
    this.hp = memento.getHp();
    this.mp = memento.getMp();
    this.weapon = memento.getWeapon();
  }

这样就ok了。

在模拟程序中,这样使用:

  public static void main(String[] args) {
    Hero hero = new Hero();
    hero.save();
    hero.challengeDragon();
    if (hero.die())
      hero.restore(0);
  }



3.4 状态模式

某个ADT具有多个操作和多个状态。如果该ADT在不同“状态”时,各个操作的内容都不同,这个时候就可以使用状态模式。

编写状态接口、不同的状态的实现类,然后将ADT在这些状态下的操作写在各个状态实现类中。

举个栗子:现在我们需要编写程序来模拟大猪头在高兴的时候、不高兴的时候会做的事情。

1. 设计大猪头ADT

/**
 * 大猪头
 */
public class BigPig {
  
  // 两个操作:吃、睡
  public void eat();
  public void sleep();
  
}

因为在不同心情状态下,吃、睡的内容不一样,所以我们将操作交给状态类来完成。

2. 编写状态接口

/**
 * 心情状态接口
 * 因为要设计的大猪头ADT只会做两个操作,所以我们每个心情只需要实现
 * 吃、睡
 * 两个操作
 */
public interface MoodState {
  
  /**
   * 吃
   */
  public void eat();
  
  /**
   * 睡
   */
  public void sleep();
  
}

3. 实现两种心情

/**
 * 高兴的心情状态
 */
public class Happy implements MoodState {

  @Override
  public void eat() {
    System.out.println("健康饮食");
  }

  @Override
  public void sleep() {
    System.out.println("早睡早起");
  }

}

/**
 * 不高兴的心情状态
 */
public class Sad implements MoodState {

  @Override
  public void eat() {
    System.out.println("暴饮暴食");
  }

  @Override
  public void sleep() {
    System.out.println("睡懒觉");
  }

}

4. 将ADT的操作委派给MoodState

/**
 * 大猪头
 */
public class BigPig {
  
  MoodState state;
  
  /**
   * 构造方法:设置初始心情状态
   * @param state 初始心情状态
   */
  public BigPig(MoodState state) {
    this.state = state;
  }
  
  /**
   * Setter: 改变心情状态
   * @param state 要变成的心情状态
   */
  public void setState(MoodState state) {
    this.state = state;
  }
  
  // 两个操作:吃、睡
  public void eat() {
    state.eat();
  }
  
  public void sleep() {
    state.sleep();
  }
  
}

最后,在模拟程序中这样使用它:

  public static void main(String[] args) {
    BigPig bigPig = new BigPig(new Happy()); // 初始状态:好心情
    bigPig.eat();
    bigPig.sleep();
    
    bigPig.setState(new Sad()); // 改变状态:坏心情
    bigPig.eat();
    bigPig.sleep();
  }


举例就到此结束了。因为尽量选取了方便理解的例子,所以可能过于简单,请多多包涵。

/** * 不高兴的心情状态 */public class Sad implements MoodState {


@Override  public void eat() {    System.out.println(“暴饮暴食”);  }

@Override  public void sleep() {    System.out.println(“睡懒觉”);  }

}

转载于:https://www.cnblogs.com/snt165/p/11074899.html