备忘录模式实战
- 前面文章依次介绍了Java设计模式中的备忘录模式以及其在Android源码中的实现,相信很多人和我一样,知其然但不知其所以然。俗话说时间是检验真理的唯一标准。现在就跟我来进行实战分析吧!
本次采用一个简单的记事本案例,通过记事本的撤销,重做,保存等逻辑,使用备忘录模式对其代码重构。先看一下人人都会写的部分吧:
<LinearLayoutandroid:layout_width="368dp" android:layout_height="495dp" android:orientation="vertical" tools:layout_editor_absoluteX="8dp" tools:layout_editor_absoluteY="8dp"> <EditText android:id="@+id/edit_text" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="left" /> <RelativeLayout android:layout_margin="12dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp"> <TextView android:id="@+id/tv_save" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_margin="12dp" android:text="保存" /> <TextView android:id="@+id/tv_pre" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="12dp" android:layout_toLeftOf="@id/tv_save" android:text="撤销" /> <TextView android:id="@+id/tv_next" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="12dp" android:layout_toRightOf="@id/tv_save" android:text="重做" /> </RelativeLayout> </LinearLayout>复制代码
xml布局代码,主要有三个逻辑(撤销、保存、重做)以及一个edittext空间进行数据的编辑。
Activity中主要进行控件初始化,点击事件的监听,代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private TextView tvPre;//撤销 private TextView tvSave;//保存 private TextView tvNext;//重做 private EditText etText;//编辑器 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); setOnClick(); } private void initViews() { tvPre = (TextView) findViewById(R.id.tv_pre); tvSave = (TextView) findViewById(R.id.tv_save); tvNext = (TextView) findViewById(R.id.tv_next); etText = (EditText) findViewById(R.id.edit_text); } private void setOnClick() { tvPre.setOnClickListener(this); tvSave.setOnClickListener(this); tvNext.setOnClickListener(this); }复制代码
撤销、保存、重做逻辑的具体实现(CareTaker作用):
@Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.tv_pre: //获取前一个存档信息(撤销) restoreeditText(getPreMemoto()); makeToast("撤销:"); break; case R.id.tv_save: //保存记录信息 saveMemoto(createMemotoForEditText()); makeToast("保存:"); break; case R.id.tv_next: //获取下一条记录(重做) restoreeditText(getNextMemoto()); makeToast("重做:"); break; } } private void makeToast(String msg) { Toast.makeText(this, msg + etText.getText() + "光标位置" + etText.getSelectionStart(), Toast.LENGTH_SHORT).show(); System.out.println(msg + etText.getText() + "光标位置" + etText.getSelectionStart()); }复制代码
备忘录的数据操作部分,可以保存30次操作记录,主要包括初始化、获取前一个存放信息、获取后一个存档信息以及恢复数据等逻辑。相当于Originator作用。
//最大存储数量 private static final int MAX = 30; //存储30条记录 List<Memoto> mMemoto = new ArrayList<Memoto>(MAX); int mIndex = 0; /** * 保存备忘录 */ public void saveMemoto(Memoto memoto) { if (mMemoto.size() > MAX) { mMemoto.remove(0); } mMemoto.add(memoto); mIndex = mMemoto.size() - 1; } /** * 获取前一个存档,相当于撤销 * * @return */ public Memoto getPreMemoto() { mIndex = mIndex > 0 ? --mIndex : mIndex; return mMemoto.get(mIndex); } /** * 获取下一个存档,相当于重做 * * @return */ public Memoto getNextMemoto() { mIndex = mIndex < mMemoto.size() - 1 ? ++mIndex : mIndex; return mMemoto.get(mIndex); } /** * 为编辑器创建Memoto对象 * * @return */ private Memoto createMemotoForEditText() { Memoto memoto = new Memoto(); memoto.cursor = etText.getSelectionStart(); memoto.text = etText.getText().toString(); return memoto; } /** * 恢复编辑器状态 * * @param memoto */ private void restoreeditText(Memoto memoto) { etText.setText(memoto.text); etText.setSelection(memoto.cursor); }复制代码
Memot备忘录数据,其中记录数据的字符以及游标信息
/** * Created by allies on 17-9-26. * 存储edittext的光标和内容 */ public class Memoto { //字符数据 public String text; //游标信息 public int cursor; }复制代码
*由上面可以看出,记事本的基本数据编辑、保存,以及特有的撤销操作和重做逻辑都已经实现。相信很多人上面的代码都可以不假思索的信手拈来,但是既然我们学习了备忘录模式,就要对其代码进行重构,学以致用方能行以千里。
以上代码的缺点:
Activity内逻辑众多,既要负责View的操作逻辑,还要管理记事本的记录、修改编辑器的状态,也就是Originator与CareTaker功能耦合在一起,造成类型膨胀,后续难以维护。我们需要优化的是将Activity中的保存数据的逻辑、职责分离出去,使每个类的职责都分工明确,降低代码之间的耦合度。
- 优化步骤:
将Activity中的CareTaker部分抽取出来,明确Originator与CreaTaker的功能。新建一个NoteCareTaker类,负责管理Memoto对象,包括撤销、保存、重做部分的逻辑(注意不对元数据进行修改操作)。
/** * Created by allies on 17-9-26. * 备忘录CareTaker部分逻辑 */ public class NoteCareTaker { //最大存储数量 private static final int MAX = 30; //存储30条记录 List<Memoto> mMemoto = new ArrayList<Memoto>(MAX); int mIndex = 0; /** * 保存备忘录 */ public void saveMemoto(Memoto memoto) { if (mMemoto.size() > MAX) { mMemoto.remove(0); } mMemoto.add(memoto); mIndex = mMemoto.size() - 1; } /** * 获取前一个存档,相当于撤销 * * @return */ public Memoto getPreMemoto() { mIndex = mIndex > 0 ? --mIndex : mIndex; return mMemoto.get(mIndex); } /** * 获取下一个存档,相当于重做 * * @return */ public Memoto getNextMemoto() { mIndex = mIndex < mMemoto.size() - 1 ? ++mIndex : mIndex; return mMemoto.get(mIndex); } }复制代码
NoteCareTaker中会维护一个备忘录列表,使用mIndex标识编辑器当前的记录点,可以通过相应的方法获取数据的前一个、后一个记录信息以及保存记录信息。
下面就是对Originator作用的代码重构。我们先来分析一下Originator的作用是什么?在第一篇中我们明确指出Originator作用是负责创建一个备忘录,可以记录、恢复自身的内部状态。这里我们可以看出,其可以直接操作元数据信息,也就是和数据信息耦合度最高的部分,其自身需要直接访问Memoto信息,以便通过这些信息存储和恢复数据等操作。此外最重要的是可以创建备忘录,也就是createMemoto的操作在这里执行。
public class NoteEditText extends EditText { public NoteEditText(Context context) { this(context,null); } public NoteEditText(Context context, AttributeSet attrs) { this(context, attrs,0); } public NoteEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 创建备忘录对象,存储元数据信息 * @return */ public Memoto createMemoto(){ Memoto memoto = new Memoto(); memoto.text = getText().toString(); memoto.cursor = getSelectionStart(); return memoto; } /** * 从备忘录恢复数据 * @param memoto */ public void restoreMemoto(Memoto memoto){ setText(memoto.text); setSelection(memoto.cursor); } }复制代码
从上面分析我们知道和数据接触最深的就是EditText了,我们这里直接覆写一个NoteEditTExt继承EditText,在其内部组织负责备忘录的创建以及恢复操作。这里也就突出了Originator的创建以及恢复状态的作用。
之后便是Activity中的具体操作逻辑,其直接通过自定义的NoteEditText空间创建Memoto备忘录,然后其撤销、保存、重做等逻辑不在Activity内部实现,而是转交给了CareTaker来实现。代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private TextView tvPre;//撤销 private TextView tvSave;//保存 private TextView tvNext;//重做 private NoteEditText etText;//自定义的编辑器 //新建Caretaker对象,负责备忘录的撤销,恢复以及保存等操作 NoteCareTaker mCareTaker = new NoteCareTaker(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); setOnClick(); } private void initViews() { tvPre = (TextView) findViewById(R.id.tv_pre); tvSave = (TextView) findViewById(R.id.tv_save); tvNext = (TextView) findViewById(R.id.tv_next); etText = (NoteEditText) findViewById(R.id.edit_text); } private void setOnClick() { tvPre.setOnClickListener(this); tvSave.setOnClickListener(this); tvNext.setOnClickListener(this); } @Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.tv_pre: etText.restoreMemoto(mCareTaker.getPreMemoto()); makeToast("撤销:"); break; case R.id.tv_save: //1.首先创建备忘录对象,创建时候就已经记录了元数据信息 Memoto mMemoto = etText.createMemoto(); //通过caretaker,保存备忘录信息 mCareTaker.saveMemoto(mMemoto); makeToast("保存:"); break; case R.id.tv_next: //重做 etText.restoreMemoto(mCareTaker.getNextMemoto()); makeToast("重做:"); break; } } private void makeToast(String msg) { Toast.makeText(this, msg + etText.getText() + "光标位置" + etText.getSelectionStart(), Toast.LENGTH_SHORT).show(); System.out.println(msg + etText.getText() + "光标位置" + etText.getSelectionStart()); } }复制代码
从上面可以看出:Activity中的逻辑简单化了不止一点两点,各个类的职责更加单一、明确。界面相关的类型和业务处理逻辑的类型隔离开来,避免类型膨胀,逻辑混乱等问题。在优化代码前一定要着重思考各个类的主要职责,明确他们之间的关系和功能,使得各个类各司其职,不可越过雷池半步。往往在这个时候,就要多看看这个设计模式中,各个角色所担任的职责,找准切入点在进一步优化重构代码。
总结
- 优点
- 给用户提供一种可以恢复状态的机制,能够使用户方便的恢复到某个历史状态
- 实现数据信息的封装,使得用户不用关心其状态的保存细节
- 缺点
消耗资源,如果类的成员变多,势必会占用较大的资源,而且每一次保存都会消耗一定的内存(降低耦合导致类增多,各个类分工明确单一的后果)
参考:Android源码设计模式实战与分析