策略模式、观察者模式、状态模式原理及实例

本文深入探讨了策略模式、观察者模式和状态模式。策略模式通过封装不同算法族,使它们可以互换,实现了算法的独立性和客户类的解耦。观察者模式则用于对象间的一对多依赖,当被依赖对象状态改变时,所有依赖者都会得到通知并自动更新。状态模式允许对象在内部状态改变时改变其行为。文中通过实例演示了每个模式的使用,并分析了关键点和注意事项。

策略模式

基本介绍

  1. 策略模式(Strategy Pattern)中,定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户

  2. 这算法体现了几个设计原则。

    第一 把变化的代码从不变的代码中分离出来

    第二 针对接口编程而不是具体类(定义了策略接口

    第三 多用组合/聚合,少用继承(客户通过组合方式使用策略)。

策略模式就是通过组合的方式将类的一些方法组合起来,每个类都可以持有不同的策略。通过持有不同的策略来达到同样方法不同执行方式的效果。

context就是用来存放策略。

实例演示

现假设要实现一个RPG游戏,该游戏有以下人物:皇帝(King), 皇后(Queen), 骑士(Knight),士兵(Pawn)。

人物可以使用魔法(MagicBehavior)和武器攻击(WeaponBehavior)两种:

这里假设魔法有火球(Fireball Magic, 国王使用), 雷电(Thunder Magic,皇后使用), 回复(Recovery Magic,骑士使用), 冲锋(Assult Magic, 士兵使用)。

武器攻击时,皇帝使用剑(Sword), 皇后使用杖(Stick), 骑士使用枪(Spear), 士兵使用斧头(Axe)。

1)UML类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O5MfF6OU-1643881227964)(F:\StudyNotepad\img\image-20211126191854021.png)]

2)编写策略接口

这里根据题目有对于每一个角色(context)都有两种策略(strategy),分别是魔法和物理攻击。

public interface WeaponBehavior {
    public void attackByWeapon();
}
public interface MagicBehavior{
    public void attackByMagic();
}
3)编写不同的策略

对于每种策略都有不同的实现。

这里以魔法中的火球和物理中的剑为例。

public class FireballMagic implements MagicBehavior{
    @Override
    public void attackByMagic() {
        System.out.println("使用火球魔法");
    }
}
public class Sword implements WeaponBehavior{
    @Override
    public void attackByWeapon() {
        System.out.println("使用武器-剑攻击");
    }
}
4)编写策略持有类

对于本题中每一个角色就是context。本题中的所有角色都是通过聚合的方式来获取策略。

这里以King为例。

public class King implements Role{
    private MagicBehavior magicBehavior;
    private WeaponBehavior weaponBehavior;

    public King(MagicBehavior magicBehavior, WeaponBehavior weaponBehavior) {
        this.magicBehavior = magicBehavior;
        this.weaponBehavior = weaponBehavior;
    }

    @Override
    public void useMagic() {
        magicBehavior.attackByMagic();
    }

    @Override
    public void useWeapon() {
        weaponBehavior.attackByWeapon();
    }
}
5)测试
public class Client {
    public static void main(String[] args) {
        King king = new King(new FireballMagic(), new Sword());
        king.useMagic();
        king.useWeapon();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NIqKIcM7-1643881227975)(F:\StudyNotepad\img\image-20211126192348674.png)]

策略模式在JDK - Arrays 应用的源码分析

在Arrays中的sort方法就使用了策略模式,通过传入不同策略(排序规则),来执行sort方法。

有两种传入策略的方式,一种是先写好策略(创建Comparator类中的compare方法,然后将实例化的类传入到sort方法参数中,就可以接收排序策略。)

另一种是直接在Arrays.sort()方法中第二个策略参数上直接用lambda(因为这个策略本身就是一个函数式接口,支持lambda表达式)

下面演示

/*
 * 第一种方式,写好策略然后传递
 * */
public class Client {
    public static void main(String[] args) {
        // 注意策略中用的integer那么数组也必须要用
        Integer[] arr = {5,2,1,3,7,0};
        
        Comparator<Integer> comparator  = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o1 > o2)
                    return 1;
                else
                    return -1;
            }
        };

        Arrays.sort(arr,comparator);

        System.out.println(Arrays.toString(arr));
    }
}
/*
 * 第二种方式,通过lambda
 * */
public class Client {
    public static void main(String[] args) {
        // 注意策略中用的integer那么数组也必须要用
        Integer[] arr = {5,2,1,3,7,0};

        Arrays.sort(arr,(o1,o2) -> {
            if (o1>o2)
                return 1;
            else
                return -1;
        });

        System.out.println(Arrays.toString(arr));
    }
}

注意事项和细节

策略模式的关键

分析项目中变化部分与不变部分。

策略模式的核心思想

多用组合/聚合,少用继承;用行为类组合,而不是行为的继承,更有弹性。

体现了“对修改关闭,对扩展开放”原则

客户端增加行为不用修改原有代码,只要添加一种策略(或者行为)即可,避免了使用多重转移语句(if…else if…else)

提供了可以替换继承关系的办法

策略模式将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展

注意

每添加一个策略就要增加一个类,当策略过多是会导致类数目庞大

观察者模式

当观察者在被管理的类中进行注册以后,当管理中心的值发生了改变,可以第一时间通过到观察者。

接收到最新的消息。

原理

对象之间多对一依赖的一种设计方案,被依赖的对象为Subject,依赖的对象为Observer,Subject通知Observer变化。

面板中基本的方法
  1. registerObserver 注册
  2. removeObserver 移除
  3. notifyObservers 通知所有的注册的用户,根据不同需求,可以是更新数据,让用户来取,也可能是实施推送,看具体需求定。
实例演示

请使用观察者模式,实现更新UI的框架代码,即:主角属性可能通过各种方式更新,然后分发给对应的面板进行同步更新。

1)UML类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qL4vncob-1643881227978)(F:\StudyNotepad\img\image-20211127165059829.png)]

2)创建人物接口

人物就在观察者,等待接收最新的数据。

这里定义人物最基本的方法,接收最新数据和显示人物属性。(接收的数据就是人物属性)

public interface Role {
    void upDate(int level, float power, float exp, float diamond, float gold);
    void display();
}
3)创建属性管理面板接口

这个管理面板负责人物属性的设定,这是一个游戏公司,每个用户的属性肯定是由公司来设定,不会交给用户来自己设定。

因此面板的作用就是管理用户的增加和删除、通知用户接收最新的信息

public interface OSPanel {
    void registeredRole(Role role);
    void removeRole(Role role);
    void notifyRole();
}
4)创建具体的面板类

每一个类别的角色都可以设定一个人物管理面板,通过这个面板可以来设定一些人物的值。

这里设定的时候是将所有这个面板管理的集合中的人物进行统一设置,当然也是可以分别设置的,只要添加相应的方法,然后接收相应的参数就可以了。

public class RolePanel implements OSPanel{
    List<Role> roleList;

    public RolePanel() {
        roleList = new ArrayList<>();
    }

    @Override
    public void registeredRole(Role role) {
        roleList.add(role);
    }

    @Override
    public void removeRole(Role role) {
        if (roleList.contains(role)){
            roleList.remove(role);
            System.out.println("人物删除成功~");
        } else {
            System.out.println("未查询到人物,无法删除");
        }
    }

    @Override
    public void notifyRole() {
        for (Role role : roleList) {
            role.upDate(5,20.0f,1000.0f,777.7f,9999.99f);
        }
    }
}
5)具体人物

这里的任务有很多属性,一些get和set方法这里就没有列举出来。

默认认为,只要接收到更新的消息,那么就应当显示一次当前的人物属性。

public class RoleWXM implements Role{
    private String name = "id 001";
    private String img = "默认头像";
    private int level = 1;
    private float power = 1.0f; // 战斗力
    private float exp = 0.0f; // 经验值
    private float diamond = 0.0f; // 钻石数
    private float gold = 0.0f;

    // 默认初始化有自己的名字和头像
    public RoleWXM(String name, String img) {
        this.name = name;
        this.img = img;
    }

    @Override
    public void upDate(int level, float power, float exp, float diamond, float gold) {
        this.level = level;
        this.power = power;
        this.exp = exp;
        this.diamond = diamond;
        this.gold = gold;
        display();
    }

    @Override
    public void display() {
        System.out.println("人物属性为:");
        System.out.println();
        System.out.println("等  级:  "+level);
        System.out.println("战斗力:  "+power);
        System.out.println("经验数:  "+exp);
        System.out.println("钻石数:  "+diamond);
        System.out.println("金币数:  "+gold);
    }
}
6)测试
public class Client {
    public static void main(String[] args) {
        // 创建人物管理面板
        RolePanel rolePanel = new RolePanel();
        // 创建人物
        RoleWXM roleWXM = new RoleWXM("王小明", "帅气头像");
        // 将人物添加到管理面板,以便接收最新消息
        rolePanel.registeredRole(roleWXM);
        // 通知管理人物接收最新消息,这里在通知的时候就进行了属性更新
        rolePanel.notifyRole();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0LspaUlC-1643881227981)(F:\StudyNotepad\img\image-20211127170231862.png)]

总结

  1. 观察者模式设计后,会以集合的方式来管理用户(Observer),包括注册,移除和通知。
  2. 这样,我们增加观察者(这里可以理解成一个新的公告板),就不需要去修改核心类RolePanel代码,遵守了ocp原则。

状态模式

基本介绍

  1. 状态模式(State Pattern):它主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。状态和行为是一一对应的,状态之间可以相互转换
  2. 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类

状态模式,不管任何状态都可以执行的一些公共方法抽取出来(定义状态接口),但是对于不同状态下执行这些方法有会有不同的情况。

比如:身体健康的时候你跑步没有问题,但是如果你受伤了在去跑步,执行结果就是跑步失败。

实例演示

状态模式实现抽奖。

1)UML类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X0ChwK6x-1643881343878)(F:\StudyNotepad\img\image-20211201171420713.png)]

2)创建状态接口

对于抽奖来说每一个状态下都可以扣除积分、抽奖结果、领取奖品,但是对于不同的状态下,执行这些方法应该会有不同的情况。

比如:初始状态就只能扣除积分,抽奖状态下只能进行抽奖,对于奖品的领取会根据抽奖的情况(这里的实现是根据抽奖的结果调取奖品的领取)

/*
* - 所有状态的接口
* - 对于每一个状态,都有相同的方法,但是对于每一种状态执行
*   这些方法时,会因为状态的情况,导致执行的结果是不同的。
* - 对于这个抽奖,各个状态下对于参加活动和获取奖品等动作,会因为
*   状态的限制而导致执行的结果不同。
* */
public interface State {
    void reduceMoney();
    boolean raffle();
    void dispense();

}
3)具体状态

这里以初始化状态演示

public class InitState implements State{
    RaffleActivity raffleActivity;

    public InitState(RaffleActivity raffleActivity) {
        this.raffleActivity = raffleActivity;
    }

    @Override
    public void reduceMoney() {
        if (raffleActivity.getMoney() < 50){
            System.out.println("积分不够,无法抽奖,请充值。");
        } else {
            raffleActivity.setMoney(raffleActivity.getMoney() - 50);
            System.out.println("正在抽奖~~请稍后...");
            raffleActivity.setState(new RaffleState(raffleActivity));
        }
    }

    @Override
    public boolean raffle() {
        System.out.println("请先参与抽奖...");
        return false;
    }

    @Override
    public void dispense() {
        System.out.println("请先抽奖~~");
    }
}
4)抽奖活动

抽奖活动初始化需要设定商品的数量和持有的积分。

还有一些get/set方法没有展示。

public class RaffleActivity {
    private int count;
    private int money;
    private State state;

    public RaffleActivity(int count, int money) {
        this.count = count;
        this.money = money;
        this.state = new InitState(this);
    }

    public void Go(){
        this.state.reduceMoney();
        this.state.raffle();
        this.state.dispense();
    }
}
5)测试
public class Client {
    public static void main(String[] args) {
        // 初始化抽奖活动
        RaffleActivity raffleActivity = new RaffleActivity(5, 200);
        // 进行抽奖,这里设定的是百分之90抽中
        raffleActivity.Go();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8liwMQXx-1643881343881)(F:\StudyNotepad\img\image-20211201172036260.png)]

注意事项和细节

  1. 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中
  2. 方便维护。将容易产生问题的if-else语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产出很多if-else语句,而且容易出错
  3. 符合“开闭原则”。容易增删状态
  4. 会产生很多类。每个状态都要一个对应的类,当状态过多时会产生很多类,加大维护难度
  5. 应用场景:当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值