实战之备忘录模式

备忘录模式实战

  • 前面文章依次介绍了Java设计模式中的备忘录模式以及其在Android源码中的实现,相信很多人和我一样,知其然但不知其所以然。俗话说时间是检验真理的唯一标准。现在就跟我来进行实战分析吧!
  • 本次采用一个简单的记事本案例,通过记事本的撤销,重做,保存等逻辑,使用备忘录模式对其代码重构。先看一下人人都会写的部分吧:
    <LinearLayout

    android: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中的保存数据的逻辑、职责分离出去,使每个类的职责都分工明确,降低代码之间的耦合度。

  • 优化步骤:
  1. 将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标识编辑器当前的记录点,可以通过相应的方法获取数据的前一个、后一个记录信息以及保存记录信息。

  2. 下面就是对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的创建以及恢复状态的作用。

  3. 之后便是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中的逻辑简单化了不止一点两点,各个类的职责更加单一、明确。界面相关的类型和业务处理逻辑的类型隔离开来,避免类型膨胀,逻辑混乱等问题。在优化代码前一定要着重思考各个类的主要职责,明确他们之间的关系和功能,使得各个类各司其职,不可越过雷池半步。往往在这个时候,就要多看看这个设计模式中,各个角色所担任的职责,找准切入点在进一步优化重构代码。

总结

  • 优点
  1. 给用户提供一种可以恢复状态的机制,能够使用户方便的恢复到某个历史状态
  2. 实现数据信息的封装,使得用户不用关心其状态的保存细节
  • 缺点
    消耗资源,如果类的成员变多,势必会占用较大的资源,而且每一次保存都会消耗一定的内存(降低耦合导致类增多,各个类分工明确单一的后果)
坚持原创技术分享,您的支持将鼓励我继续创作!