设计模式之观察者模式
首先假设目前有这样的一个应用场景,你正在设计一款角色对战游戏,每一个角色都有自己的一些属性,此处简化为:
- 血量
hp
- 魔法值
magicValue
- 攻击速度
speed
- 武器装备等级
attackLevel
现在的需求是开发人员需要在游戏界面的多处显示这些数据,比如角色头顶HeadPanel
,屏幕左上角统计控件SummaryPanel
以及右下角角色操作块OperationPanel
。其中:
HeadPanel
会显示角色的血量
以及魔法值
SummaryPanel
会显示角色的血量
,魔法值
,攻击速度
以及武器装备等级
OperationPanel
会显示角色的攻击速度
和装备等级
当角色的属性发生变化(比如被攻击掉血,或是捡到装备武器升级)的时候,这些属性的值也应该及时更新到这些控件中。
准备工作
根据上面的场景,我们可以先创建一些类出来。
public class Role {
private Integer id;
private String name;
private Integer hp;
private Integer magicValue;
private Integer speed;
private Integer attackLevel;
// 省略对应的setter和getter
}
class HeadPanel{
private Integer hp;
private Integer magicValue;
// 省略更多特有的属性
// 省略对应的setter和getter
public void update(hp,magicValue){
// ...
}
}
class SummaryPanel{
private Integer hp;
private Integer magicValue;
private Integer speed;
private Integer attackLevel;
// 省略更多特有的属性
// 省略对应的setter和getter
public void update(hp,magicValue,speed,attackLevel){
// ...
}
}
class OperationPanel{
private Integer speed;
private Integer attackLevel;
// 省略更多特有的属性
// 省略对应的setter和getter
public void update(speed,attackLevel){
// ...
}
}
根据场景需求分析来看,想要数据的任何更改都及时推送到指定的控件,最简单的做法是在Role中直接依赖这些空间类,然后当Role中的属性发生变化,即对应setter被调用时,直接在setter内部进行数值更改。
代码实现为:
public class Role {
private String id;
private String name;
private int hp;
private int attackLevel;
private int speed;
private int magicValue;
//=======================
private HeadPanel headPanel;
private OperationPanel operationPanel;
private SummaryPanel summaryPanel;
public HeadPanel getHeadPanel() {
return headPanel;
}
public void setHeadPanel(HeadPanel headPanel) {
this.headPanel = headPanel;
}
public OperationPanel getOperationPanel() {
return operationPanel;
}
public void setOperationPanel(OperationPanel operationPanel) {
this.operationPanel = operationPanel;
}
public SummaryPanel getSummaryPanel() {
return summaryPanel;
}
public void setSummaryPanel(SummaryPanel summaryPanel) {
this.summaryPanel = summaryPanel;
}
//==========================
public Role() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
headPanel.setHp(hp);
summaryPanel.setHp(hp);
}
public int getAttackLevel() {
return attackLevel;
}
public void setAttackLevel(int attackLevel) {
this.attackLevel = attackLevel;
headPanel.setAttackLevel(attackLevel);
summaryPanel.setAttackLevel(attackLevel);
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
operationPanel.setSpeed(speed);
summaryPanel.setSpeed(speed);
}
public int getMagicValue() {
return magicValue;
}
public void setMagicValue(int magicValue) {
this.magicValue = magicValue;
operationPanel.setMagicValue(magicValue);
summaryPanel.setMagicValue(magicValue);
}
}
这显然是可以实现现阶段的产品需求的。但是也充满了代码的坏味道:
-
在Role的setter方法里进行组件值的更新,违反了单一职责原则
-
如果角色的属性增加,比如增加角色等级
level
,此时必须要更改很多地方的参数,这是典型的坏味道 -
三个panel虽然属性不太一致,但是整体逻辑一致,代码重复率很高
-
如果现在增加了新的组件来监控角色panel属性,则必须更改Role已有的代码,将新的组件直接作为属性写到Role中,违反了开闭原则
在未来的时间内,角色类可能会增加或是删除一些属性,panel类也可能加入或删除对一些属性的监控,因此将监控数据使用参数的形式传入update
函数并不是一个好的方式,更好的方式是:直接将整个Role实例作为参数,panel类根据自己的需求来选择属性进行展示。在有些人看来,这可能会导致程序违反了迪米特法则(最少知道原则),但是它却让整个代码在update()参数传递时具有很高的可扩展性。这是可以接受的,执行效率也不会变低。
此时update
方法在三个panel之间完全一致,可以考虑为其抽象出一个借口Updateable
interface Updateable{
void update(Role role);
}
所有panel实现该接口,然后实现自己的update
方法。
此时可以考虑换一种数据结构来存储Role中panels,它们实现了共同的接口,则可以在Role中使用list来存储它们,这样最大的好处是:可以动态的添加或删除panel。
public class Role {
private String id;
private String name;
private int hp;
private int attackLevel;
private int speed;
private int magicValue;
//=======================
private List<Updateable> updateableList = new ArrayList<>();
public void addPanel(Updateable panel) {
updateableList.add(panel);
}
public void removePanel(Updateable panel) {
updateableList.remove(panel);
}
public void updatepanels() {
for (Updateable updateable : updateableList) {
updateable.update(this);
}
}
//==========================
public Role() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
updatepanels();
}
public int getAttackLevel() {
return attackLevel;
}
public void setAttackLevel(int attackLevel) {
this.attackLevel = attackLevel;
updatepanels();
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
updatepanels();
}
public int getMagicValue() {
return magicValue;
}
public void setMagicValue(int magicValue) {
this.magicValue = magicValue;
updatepanels();
}
}
现在其实已经算是采用了观察者模式。在本例中,所有的panel
有一个共同的特性是:观察Role
的数据,如果发生变化,则更新自己的界面。
程序实现的逻辑为:Role角色一旦发现自己变化了,就给所有的观察者发数据,发的数据就是自身实例。观察者接收到实例之后,从中挑选自己关心的数据在界面上进行更新。
在两者的关系中,Role
是被观察的主题Subject
,Panel
是Role
的观察者Observer
。这就构成了观察者模式里面的两个主题。
使用观察者模式重新构造上面的代码:
public class Role {
private String id;
private String name;
private int hp;
private int attackLevel;
private int speed;
private int magicValue;
private List<Observer> observerList = new ArrayList<>();
public void registerObserver(Observer panel) {
observerList.add(panel);
}
public void removeObserver(Observer panel) {
observerList.remove(panel);
}
public void updatePanels() {
for (Observer observer : observerList) {
observer.update(this);
}
}
public Role() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
updatePanels();
}
public int getAttackLevel() {
return attackLevel;
}
public void setAttackLevel(int attackLevel) {
this.attackLevel = attackLevel;
updatePanels();
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
updatePanels();
}
public int getMagicValue() {
return magicValue;
}
public void setMagicValue(int magicValue) {
this.magicValue = magicValue;
updatePanels();
}
}
此时,其实已经可以结束了。但是《Head First 设计模式》给了我们更好的一种方式,使代码的可扩展性进一步增强。代码为:
import java.util.ArrayList;
import java.util.List;
public class Role {
private String id;
private String name;
private int hp;
private int attackLevel;
private int speed;
private int magicValue;
private List<Observer> observerList = new ArrayList<>();
public void registerObserver(Observer panel) {
observerList.add(panel);
}
public void removeObserver(Observer panel) {
observerList.remove(panel);
}
public void updatePanels() {
for (Observer observer : observerList) {
observer.update();
}
}
public Role() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
updatePanels();
}
public int getAttackLevel() {
return attackLevel;
}
public void setAttackLevel(int attackLevel) {
this.attackLevel = attackLevel;
updatePanels();
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
updatePanels();
}
public int getMagicValue() {
return magicValue;
}
public void setMagicValue(int magicValue) {
this.magicValue = magicValue;
updatePanels();
}
}
interface Observer {
void update();
}
abstract class Panel implements Observer {
private Role role;
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
}
class HeadPanel extends Panel{
private int hp;
private int attackLevel;
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getAttackLevel() {
return attackLevel;
}
public void setAttackLevel(int attackLevel) {
this.attackLevel = attackLevel;
}
@Override
public void update() {
System.out.println("更新HeadPanel");
}
}
class OperationPanel extends Panel{
private int speed;
private int magicValue;
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
}
public int getMagicValue() {
return magicValue;
}
public void setMagicValue(int magicValue) {
this.magicValue = magicValue;
}
@Override
public void update() {
System.out.println("更新OperationPanel");
}
}
class SummaryPanel extends Panel {
private int hp;
private int attackLevel;
private int speed;
private int magicValue;
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getAttackLevel() {
return attackLevel;
}
public void setAttackLevel(int attackLevel) {
this.attackLevel = attackLevel;
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
}
public int getMagicValue() {
return magicValue;
}
public void setMagicValue(int magicValue) {
this.magicValue = magicValue;
}
@Override
public void update() {
System.out.println("更新SummaryPanel");
}
}
public class Main {
public static void main(String[] args) {
Role role = new Role();
Panel panel1 = new HeadPanel();
panel1.setRole(role);
Panel panel2 = new OperationPanel();
panel2.setRole(role);
Panel panel3 = new SummaryPanel();
panel3.setRole(role);
role.registerObserver(panel1);
role.registerObserver(panel2);
role.registerObserver(panel3);
role.setHp(23);
role.setMagicValue(12);
}
}
//输出为:
// 更新HeadPanel
// 更新OperationPanel
// 更新SummaryPanel
// 更新HeadPanel
// 更新OperationPanel
// 更新SummaryPanel