您的位置:首页 > 编程语言 > Java开发

JAVA设计模式(19) —<行为型>备忘录模式(Memento)

2015-11-01 21:48 706 查看


1 定义:

备忘录模式(Memento)
Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.
(在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。)


1.1 通用类图:



个人认为更易理解的类图



在备忘录模式结构图中包含如下几个角色:

● 发起人(Originator):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。

●备忘录(Memento):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。

●[b]负责人(Caretaker):[/b]负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。

在备忘录模式中,最重要的就是备忘录Memento了。我们都是备忘录中存储的就是原发器的部分或者所有的状态信息,而这些状态信息是不能够被其他对象所访问了,也就是说我们是不可能在备忘录之外的对象来存储这些状态信息,如果暴漏了内部状态信息就违反了封装的原则,故备忘录是除了原发器外其他对象都是不可以访问的。

所以为了实现备忘录模式的封装,我们需要对备忘录的访问做些控制:

对原发器:可以访问备忘录里的所有信息。

对负责人:不可以访问备忘录里面的数据,但是他可以保存备忘录并且可以将备忘录传递给其他对象。

其他对象:不可访问也不可以保存,它只负责接收从负责人那里传递过来的备忘录同时恢复原发器的状态。

所以就备忘录模式而言理想的情况就是只允许生成该备忘录的那个原发器访问备忘录的内部状态。


1.2 通用代码:

[java] view
plaincopy

public class Originator {

// 内部状态

private String state = "";

public String getState() {

return state;

}

public void setState(String state) {

this.state = state;

}

// 创建一个备忘录

public Memento createMemento() {

return new Memento(this.state);

}

// 恢复一个备忘录

public void restoreMemento(Memento _memento) {

this.setState(_memento.getState());

}

}

class Memento {

// 发起人的内部状态

private String state = "";

// 构造函数传递参数

public Memento(String _state) {

this.state = _state;

}

public String getState() {

return state;

}

public void setState(String state) {

this.state = state;

}

}

public class Caretaker {

// 备忘录对象

private Memento memento;

public Memento getMemento() {

return memento;

}

public void setMemento(Memento memento) {

this.memento = memento;

}

}

public class Client {

public static void main(String[] args) {

// 定义出发起人

Originator originator = new Originator();

// 定义出备忘录管理员

Caretaker caretaker = new Caretaker();

// 创建一个备忘录

caretaker.setMemento(originator.createMemento());

// 恢复一个备忘录

originator.restoreMemento(caretaker.getMemento());

}

}


2 优点

1、 给用户提供了一种可以恢复状态的机制。可以是用户能够比较方便地回到某个历史的状态。

2、 实现了信息的封装。使得用户不需要关心状态的保存细节。


3 缺点

消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。


4 应用场景

4.1 需要保有存和恢复数据的相关状态场景;

4.2 提供一个可回滚的操作;

4.3 需要监控的副本场景中;(如要监控一个对象的属性,但监控又不应该作为系统的主业务来调用,它只是边缘应用,即使出现监控不准、错误报警也影响不大。)

4.4 数据库中的事务处理就使用备忘录模式。
如果有需要提供回滚操作的需求,使用备忘录模式非常适合,比如jdbc的事务操作,文本编辑器的Ctrl+Z恢复等。


5 注意事项


5.1 备忘录的生命周期:

创建就要使用,要主动管理它的生命。


5.2 备忘录的性能:

不要在频繁建立备份的场景中使用备忘录模式(比如一个for循环中),原因是:一是控制不了其数量,二是大对象的建立是要消耗资源的,系统的性能需要考虑。


5.3在设计备忘录类时需要考虑其封装性

除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法,如果不考虑封装性,允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。

在使用Java语言实现备忘录模式时,一般通过将Memento类与Originator类定义在同一个包(package)中来实现封装,在Java语言中可使用默认访问标识符来定义Memento类,即保证其包内可见。只有Originator类可以对Memento进行访问,而限制了其他类对Memento的访问。在 Memento中保存了Originator的state值,如果Originator中的state值改变之后需撤销,可以通过调用它的restoreMemento()方法进行恢复。

对于负责人类Caretaker,它用于保存备忘录对象,并提供getMemento()方法用于向客户端返回一个备忘录对象,原发器通过使用这个备忘录对象可以回到某个历史状态。


6 扩展


6.1 clone方式的备忘录:

通过复制的方式产生一个对象的内部状态,类图如下:



从类图上来看,发起人角色融合了发起人角色和备忘录角色,代码如下:

[java] view
plaincopy

public class Originator implements Cloneable {

// 内部状态

private String state = "";

// 此二方法原为Memento所有。

public String getState() {

return state;

}

public void setState(String state) {

this.state = state;

}

// 创建一个备忘录

public Originator createMemento() {

return this.clone();

}

// 恢复一个备忘录

public void restoreMemento(Originator _originator) {

this.setState(_originator.getState());

}

// 克隆当前对象

@Override

protected Originator clone() {

try {

return (Originator) super.clone();

} catch (CloneNotSupportedException e) {

e.printStackTrace();

}

return null;

}

}

public class Caretaker {

// 发起人对象

private Originator originator;

public Originator getOriginator() {

return originator;

}

public void setOriginator(Originator originator) {

this.originator = originator;

}

}

public class Client {

public static void main(String args[]) {

Originator o = new Originator();

Caretaker c = new Caretaker();

o.setState("version 1.0.0");

c.setOriginator(o.clone());

System.out.println("当前状态:" + o.getState());

o.setState("version 2.0.0");

System.out.println("修改后状态:" + o.getState());

o.restoreMemento(c.getOriginator());

System.out.println("恢复后状态:" + o.getState());

}

}</span>

测试:

当前状态:version 1.0.0

修改后状态:version 2.0.0

恢复后状态:version 1.0.0

仍可以进一步精简,去掉管理备忘录角色

[java] view
plaincopy

public class Originator implements Cloneable {

private Originator backup;

// 内部状态

private String state = "";

public String getState() {

return state;

}

public void setState(String state) {

this.state = state;

}

// 创建一个备忘录

public void createMemento() {

this.backup = this.clone();

}

// 恢复一个备忘录

public void restoreMemento() {

// 在进行恢复前应该进行断言,防止空指针

this.setState(this.backup.getState());

}

// 克隆当前对象

@Override

protected Originator clone() {

try {

return (Originator) super.clone();

} catch (CloneNotSupportedException e) {

e.printStackTrace();

}

return null;

}

}

public class Client {

public static void main(String[] args) {

// 定义发起人

Originator originator = new Originator();

// 建立初始状态

originator.setState("初始状态...");

System.out.println("初始状态是:" + originator.getState());

// 建立备份

originator.createMemento();

// 修改状态

originator.setState("修改后的状态...");

System.out.println("修改后状态是:" + originator.getState());

// 恢复原有状态

originator.restoreMemento();

System.out.println("恢复后状态是:" + originator.getState());

}

}

对比以上两例,可以发现:程序精简了很多,而且高层模块的依赖也减少了。现在再考虑一下原型模式深拷贝与浅拷贝的问题,在复杂的场景下它会让你的程序逻辑异常混乱,出现错误也很难跟踪。因此Clone方式的备忘录模式适用于较简单的场景。


6.2 多状态的备忘录模式:

实际开发中,一个对象可能有多个状态,一个JavaBean有多个属性非常常见,如果随上所述,就要写一堆的状态备份、还原语句?这里介绍一个工具类BeanUtils,可以把类的所有属性值转换到HashMap中,亦可以把HashMap中的值放入对象中。

此方案的类图如下:



源代码如下:

[java] view
plaincopy

public class Originator {

// 内部状态

private String state1 = "";

private String state2 = "";

private String state3 = "";

public String getState1() {

return state1;

}

public void setState1(String state1) {

this.state1 = state1;

}

public String getState2() {

return state2;

}

public void setState2(String state2) {

this.state2 = state2;

}

public String getState3() {

return state3;

}

public void setState3(String state3) {

this.state3 = state3;

}

// 创建一个备忘录

public Memento createMemento() {

return new Memento(BeanUtils.backupProp(this));

}

// 恢复一个备忘录

public void restoreMemento(Memento _memento) {

BeanUtils.restoreProp(this, _memento.getStateMap());

}

// 增加一个toString方法

@Override

public String toString() {

return "state1=" + state1 + "\nstat2=" + state2 + "\nstate3=" + state3;

}

}

public class Memento {

// 接受HashMap作为状态

private HashMap<String, Object> stateMap;

// 接受一个对象,建立一个备份

public Memento(HashMap<String, Object> map) {

this.stateMap = map;

}

public HashMap<String, Object> getStateMap() {

return stateMap;

}

public void setStateMap(HashMap<String, Object> stateMap) {

this.stateMap = stateMap;

}

}

public class Caretaker {

// 备忘录对象

private Memento memento;

public Memento getMemento() {

return memento;

}

public void setMemento(Memento memento) {

this.memento = memento;

}

}

public class BeanUtils {

// 把bean的所有属性及数值放入到Hashmap中

public static HashMap<String, Object> backupProp(Object bean) {

HashMap<String, Object> result = new HashMap<String, Object>();

try {

// 获得Bean描述

BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());

// 获得属性描述

PropertyDescriptor[] descriptors = beanInfo

.getPropertyDescriptors();

// 遍历所有属性

for (PropertyDescriptor des : descriptors) {

// 属性名称

String fieldName = des.getName();

// 读取属性的方法

Method getter = des.getReadMethod();

// 读取属性值

Object fieldValue = getter.invoke(bean, new Object[] {});

if (!fieldName.equalsIgnoreCase("class")) {

result.put(fieldName, fieldValue);

}

}

} catch (Exception e) {

// 异常处理

}

return result;

}

// 把HashMap的值返回到bean中

public static void restoreProp(Object bean, HashMap<String, Object> propMap) {

try {

// 获得Bean描述

BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());

// 获得属性描述

PropertyDescriptor[] descriptors = beanInfo

.getPropertyDescriptors();

// 遍历所有属性

for (PropertyDescriptor des : descriptors) {

// 属性名称

String fieldName = des.getName();

// 如果有这个属性

if (propMap.containsKey(fieldName)) {

// 写属性的方法

Method setter = des.getWriteMethod();

setter.invoke(bean, new Object[] { propMap.get(fieldName) });

}

}

} catch (Exception e) {

// 异常处理

System.out.println("shit");

e.printStackTrace();

}

}

}

public class Client {

public static void main(String[] args) {

// 定义出发起人

Originator ori = new Originator();

// 定义出备忘录管理员

Caretaker caretaker = new Caretaker();

// 初始化

ori.setState1("中国");

ori.setState2("强盛");

ori.setState3("繁荣");

System.out.println("===初始化状态===\n" + ori);

// 创建一个备忘录

caretaker.setMemento(ori.createMemento());

// 修改状态值

ori.setState1("软件");

ori.setState2("架构");

ori.setState3("优秀");

System.out.println("\n===修改后状态===\n" + ori);

// 恢复一个备忘录

ori.restoreMemento(caretaker.getMemento());

System.out.println("\n===恢复后状态===\n" + ori);

}

}

通过这种方式,无论有多少状态都可以。


6.3 多备份的备忘录:

可在通用源码上添加如下即可:

[java] view
plaincopy

public class Caretaker {

// 容纳备忘录的容器

private HashMap<String, Memento> memMap = new HashMap<String, Memento>();

public Memento getMemento(String idx) {

return memMap.get(idx);

}

public void setMemento(String idx, Memento memento) {

this.memMap.put(idx, memento);

}

}

public class Client {

public static void main(String[] args) {

// 定义出发起人

Originator originator = new Originator();

// 定义出备忘录管理员

Caretaker caretaker = new Caretaker();

// 创建两个备忘录

caretaker.setMemento("001", originator.createMemento());

caretaker.setMemento("002", originator.createMemento());

// 恢复一个指定标记的备忘录

originator.restoreMemento(caretaker.getMemento("001"));

}

}

注意:要严格限定备忘录的创建,建议增加MAP的上限,否则系统很容易产生内存溢出情况。


6.4 更好的封装:

双接口(一个类可以实现多个接口,在系统设计时,如果考虑对象的安全问题,可以提供两个接口,一个是业务的正常接口,实现必要的业务逻辑,叫做宽接口;另外一个接口是一个空接口,什么方法都没有,其目的是提供给子系统外的模块访问,因此较安全。)

类图如下:



源代码如下:

[java] view
plaincopy

public class Originator {

// 内部状态

private String state = "";

public String getState() {

return state;

}

public void setState(String state) {

this.state = state;

}

// 创建一个备忘录

public IMemento createMemento() {

return new Memento(this.state);

}

// 恢复一个备忘录

public void restoreMemento(IMemento _memento) {

this.setState(((Memento) _memento).getState());

}

// 内置类

private class Memento implements IMemento {

// 发起人的内部状态

private String state = "";

// 构造函数传递参数

private Memento(String _state) {

this.state = _state;

}

private String getState() {

return state;

}

private void setState(String state) {

this.state = state;

}

}

}

public interface IMemento {

}

public class Caretaker {

// 备忘录对象

private IMemento memento;

public IMemento getMemento() {

return memento;

}

public void setMemento(IMemento memento) {

this.memento = memento;

}

}

public class Client {

public static void main(String[] args) {

// 定义出发起人

Originator originator = new Originator();

// 定义出备忘录管理员

Caretaker caretaker = new Caretaker();

// 创建一个备忘录

caretaker.setMemento(originator.createMemento());

// 恢复一个备忘录

originator.restoreMemento(caretaker.getMemento());

}

}

内置类Memento全部是private访问权限,也就是说除了发起人外,别人休想访问到,如果要产生关联关系,就通过空接口进行。


7 范例

7.1游戏boss血量回滚

实现场景:我们就以游戏挑战BOSS为实现场景,在挑战BOSS之前,角色的血量、蓝量都是满值,然后存档,在大战BOSS时,由于操作失误导致血量和蓝量大量损耗,所以只好恢复到刚刚开始的存档点,继续进行大战BOSS了。这里使用备忘录模式来实现。UML结构图如下:



首先是游戏角色类:Role.java

[java] view
plaincopyprint?

private int bloodFlow;

private int magicPoint;

public Role(int bloodFlow,int magicPoint){

this.bloodFlow = bloodFlow;

this.magicPoint = magicPoint;

}

public int getBloodFlow() {

return bloodFlow;

}

public void setBloodFlow(int bloodFlow) {

this.bloodFlow = bloodFlow;

}

public int getMagicPoint() {

return magicPoint;

}

public void setMagicPoint(int magicPoint) {

this.magicPoint = magicPoint;

}

/**

* @desc 展示角色当前状态

* @return void

*/

public void display(){

System.out.println("用户当前状态:");

System.out.println("血量:" + getBloodFlow() + ";蓝量:" + getMagicPoint());

}

/**

* @desc 保持存档、当前状态

* @return

* @return Memento

*/

public Memento saveMemento(){

return new Memento(getBloodFlow(), getMagicPoint());

}

/**

* @desc 恢复存档

* @param memento

* @return void

*/

public void restoreMemento(Memento memento){

this.bloodFlow = memento.getBloodFlow();

this.magicPoint = memento.getMagicPoint();

}

}

备忘录:Memento.java

[java] view
plaincopyprint?

class Memento {

private int bloodFlow;

private int magicPoint;

public int getBloodFlow() {

return bloodFlow;

}

public void setBloodFlow(int bloodFlow) {

this.bloodFlow = bloodFlow;

}

public int getMagicPoint() {

return magicPoint;

}

public void setMagicPoint(int magicPoint) {

this.magicPoint = magicPoint;

}

public Memento(int bloodFlow,int magicPoint){

this.bloodFlow = bloodFlow;

this.magicPoint = magicPoint;

}

}

负责人:Caretaker.java

[java] view
plaincopyprint?

public class Caretaker {

Memento memento;

public Memento getMemento() {

return memento;

}

public void setMemento(Memento memento) {

this.memento = memento;

}

}

客户端:Client.java

[java] view
plaincopyprint?

public class Client {

public static void main(String[] args) {

//打BOSS之前:血、蓝全部满值

Role role = new Role(100, 100);

System.out.println("----------大战BOSS之前----------");

role.display();

//保持进度

Caretaker caretaker = new Caretaker();

caretaker.memento = role.saveMemento();

//大战BOSS,快come Over了

role.setBloodFlow(20);

role.setMagicPoint(20);

System.out.println("----------大战BOSS----------");

role.display();

//恢复存档

role.restoreMemento(caretaker.getMemento());

System.out.println("----------恢复----------");

role.display();

}

}

运行结果



7.2可悔棋的中国象棋


Sunny软件公司欲开发一款可以运行在Android平台的触摸式中国象棋软件,由于考虑到有些用户是“菜鸟”,经常不小心走错棋;还有些用户因为不习惯使用手指在手机屏幕上拖动棋子,常常出现操作失误,因此该中国象棋软件要提供“悔棋”功能,用户走错棋或操作失误后可恢复到前一个步骤。如图21-1所示:



图21-1 Android版中国象棋软件界面示意图

如何实现“悔棋”功能是Sunny软件公司开发人员需要面对的一个重要问题,“悔棋”就是让系统恢复到某个历史状态,在很多软件中通常称之为“撤销”。下面我们来简单分析一下撤销功能的实现原理:

在实现撤销时,首先必须保存软件系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态。如图21-2所示:



图21-2撤销功能示意图

备忘录模式正为解决此类撤销问题而诞生,它为我们的软件提供了“后悔药”,通过使用备忘录模式可以使系统恢复到某一特定的历史状态。

完整解决方案

为了实现撤销功能,Sunny公司开发人员决定使用备忘录模式来设计中国象棋软件,其基本结构如图21-4所示:



在图21-4中,Chessman充当原发器,ChessmanMemento充当备忘录,MementoCaretaker充当负责人,在MementoCaretaker中定义了一个ChessmanMemento类型的对象,用于存储备忘录。完整代码如下所示:

[java] view
plaincopy

//象棋棋子类:原发器

class Chessman {

private String label;

private int x;

private int y;

public Chessman(String label,int x,int y) {

this.label = label;

this.x = x;

this.y = y;

}

public void setLabel(String label) {

this.label = label;

}

public void setX(int x) {

this.x = x;

}

public void setY(int y) {

this.y = y;

}

public String getLabel() {

return (this.label);

}

public int getX() {

return (this.x);

}

public int getY() {

return (this.y);

}

//保存状态

public ChessmanMemento save() {

return new ChessmanMemento(this.label,this.x,this.y);

}

//恢复状态

public void restore(ChessmanMemento memento) {

this.label = memento.getLabel();

this.x = memento.getX();

this.y = memento.getY();

}

}

//象棋棋子备忘录类:备忘录

class ChessmanMemento {

private String label;

private int x;

private int y;

public ChessmanMemento(String label,int x,int y) {

this.label = label;

this.x = x;

this.y = y;

}

public void setLabel(String label) {

this.label = label;

}

public void setX(int x) {

this.x = x;

}

public void setY(int y) {

this.y = y;

}

public String getLabel() {

return (this.label);

}

public int getX() {

return (this.x);

}

public int getY() {

return (this.y);

}

}

//象棋棋子备忘录管理类:负责人

class MementoCaretaker {

private ChessmanMemento memento;

public ChessmanMemento getMemento() {

return memento;

}

public void setMemento(ChessmanMemento memento) {

this.memento = memento;

}

}

编写如下客户端测试代码:

[java] view
plaincopy

class Client {

public static void main(String args[]) {

MementoCaretaker mc = new MementoCaretaker();

Chessman chess = new Chessman("车",1,1);

display(chess);

mc.setMemento(chess.save()); //保存状态

chess.setY(4);

display(chess);

mc.setMemento(chess.save()); //保存状态

display(chess);

chess.setX(5);

display(chess);

System.out.println("******悔棋******");

chess.restore(mc.getMemento()); //恢复状态

display(chess);

}

public static void display(Chessman chess) {

System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");

}

}

编译并运行程序,输出结果如下:
棋子车当前位置为:第1行第1列。

棋子车当前位置为:第1行第4列。

棋子车当前位置为:第1行第4列。

棋子车当前位置为:第5行第4列。

******悔棋******

棋子车当前位置为:第1行第4列。

实现多次撤销

Sunny软件公司开发人员通过使用备忘录模式实现了中国象棋棋子的撤销操作,但是使用上述代码只能实现一次撤销,因为在负责人类中只定义一个备忘录对象来保存状态,后面保存的状态会将前一次保存的状态覆盖,但有时候用户需要撤销多步操作。如何实现多次撤销呢?本节将提供一种多次撤销的解决方案,那就是在负责人类中定义一个集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史状态,而且还可以对备忘录集合进行正向遍历,实现重做(Redo)操作,即取消撤销,让对象状态得到恢复。

改进之后的中国象棋棋子撤销功能结构图如图21-5所示:



在图21-5中,我们对负责人类MementoCaretaker进行了修改,在其中定义了一个ArrayList类型的集合对象来存储多个备忘录,其代码如下所示:

[java] view
plaincopy

import java.util.*;

class MementoCaretaker {

//定义一个集合来存储多个备忘录

private ArrayList mementolist = new ArrayList();

public ChessmanMemento getMemento(int i) {

return (ChessmanMemento)mementolist.get(i);

}

public void setMemento(ChessmanMemento memento) {

mementolist.add(memento);

}

}

编写如下客户端测试代码:

[java] view
plaincopy

class Client {

private static int index = -1; //定义一个索引来记录当前状态所在位置

private static MementoCaretaker mc = new MementoCaretaker();

public static void main(String args[]) {

Chessman chess = new Chessman("车",1,1);

play(chess);

chess.setY(4);

play(chess);

chess.setX(5);

play(chess);

undo(chess,index);

undo(chess,index);

redo(chess,index);

redo(chess,index);

}

//下棋

public static void play(Chessman chess) {

mc.setMemento(chess.save()); //保存备忘录

index ++;

System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");

}

//悔棋

public static void undo(Chessman chess,int i) {

System.out.println("******悔棋******");

index --;

chess.restore(mc.getMemento(i-1)); //撤销到上一个备忘录

System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");

}

//撤销悔棋

public static void redo(Chessman chess,int i) {

System.out.println("******撤销悔棋******");

index ++;

chess.restore(mc.getMemento(i+1)); //恢复到下一个备忘录

System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");

}

}

编译并运行程序,输出结果如下:
棋子车当前位置为:第1行第1列。

棋子车当前位置为:第1行第4列。

棋子车当前位置为:第5行第4列。

******悔棋******

棋子车当前位置为:第1行第4列。

******悔棋******

棋子车当前位置为:第1行第1列。

******撤销悔棋******

棋子车当前位置为:第1行第4列。

******撤销悔棋******

棋子车当前位置为:第5行第4列。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: