Architect's Inner Work, Memo Mode Details Participating in Rich Text Editor Development

Posted by coollog on Sun, 22 Mar 2020 07:16:37 +0100

Memento Pattern, also known as Snapshot Pattern or Token Pattern, captures an internal state and preserves it outside the object without destroying the encapsulation.The object can then be restored to its original saved state.

In software systems, the memo mode provides us with a mechanism of "regret medicine", which allows us to roll back the system to a certain historical state at any time by storing snapshots of each historical state of the system.

1. Scenarios for the application of memo mode

We have the opportunity to use memo mode every day, such as Git, SVN, to provide a code version recall function.There is also an archiving function for the game, which allows players to continue from their previous positions the next time they continue playing by storing the current progress of the game in a local file system or database.

The memo mode applies to the following two scenarios:

  • Scenes that need to save a historical snapshot;
  • You want to save the state in addition to the object, and the state cannot be accessed by any other class object other than yourself to save the specific content.

There are three main roles in the memo mode:

  • Initiator role: responsible for creating a memo, recording the status you need to save, with the status rollback function;
  • Memento role: Used to store the internal state of the initiator and prevent access to objects other than the initiator;
  • Memo Administrator (Caretaker): responsible for storage, provides management memo, cannot operate on and access the contents of the memo.

1.1 Manage Ground Memo Mode with Stack

Rich text editors have been used to write articles or blogs on webpages, which come with features like draft boxes, undos, and so on.

The code below is used to do this.Suppose we need to publish an article. The editing process of this article takes a long time. It is undone and saved as drafts and modifications.Create the initiator role editor Editor class first:

public class Editor {

    private String title;

    private String content;

    private String imgs;

    public Editor(String title, String content, String imgs) {
        this.title = title;
        this.content = content;
        this.imgs = imgs;
    }

    public ArticleMemento save2Memento() {
        ArticleMemento articleMemento =
                new ArticleMemento(this.title, this.content, this.imgs);
        return articleMemento;
    }

    public void undoFromMemento(ArticleMemento articleMemento) {
        this.title = articleMemento.getTitle();
        this.content = articleMemento.getContent();
        this.imgs = articleMemento.getImgs();
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getImgs() {
        return imgs;
    }

    public void setImgs(String imgs) {
        this.imgs = imgs;
    }

    @Override
    public String toString() {
        return "Editor{" +
                "title='" + title + '\'' +
                ", content='" + content + '\'' +
                ", imgs='" + imgs + '\'' +
                '}';
    }
}

Then create the memo role ArticleMemento class:

public class ArticleMemento {

    private String title;

    private String content;

    private String imgs;

    public ArticleMemento(String title, String content, String imgs) {
        this.title = title;
        this.content = content;
        this.imgs = imgs;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getImgs() {
        return imgs;
    }

    public void setImgs(String imgs) {
        this.imgs = imgs;
    }

    @Override
    public String toString() {
        return "ArticleMemento{" +
                "title='" + title + '\'' +
                ", content='" + content + '\'' +
                ", imgs='" + imgs + '\'' +
                '}';
    }
}

Create a Memo Management Role DraftBox DraftBox class:

public class DraftBox {

    private final Stack<ArticleMemento> STACK = new Stack<>();

    public ArticleMemento getMemento() {
        ArticleMemento articleMemento = STACK.pop();
        return articleMemento;
    }

    public void addMemento(ArticleMemento articleMemento) {
        STACK.push(articleMemento);
    }

}

The Stack class of the draft box is a subclass of Vector that implements a standard LIFO stack.

2. The reflection of memo mode in source code

The application of memo mode in framework source code is relatively rare, mainly in combination with specific application scenarios.The webfolw source code StateManageableMessageContext interface in spring, let's look at its source code:

public interface StateManageableMessageContext extends MessageContext {

	/**
	 * Create a serializable memento, or token representing a snapshot of the internal state of this message context.
	 * @return the messages memento
	 */
	public Serializable createMessagesMemento();

	/**
	 * Set the state of this context from the memento provided. After this call, the messages in this context will match
	 * what is encapsulated inside the memento. Any previous state will be overridden.
	 * @param messagesMemento the messages memento
	 */
	public void restoreMessages(Serializable messagesMemento);

	/**
	 * Configure the message source used to resolve messages added to this context. May be set at any time to change how
	 * coded messages are resolved.
	 * @param messageSource the message source
	 * @see MessageContext#addMessage(MessageResolver)
	 */
	public void setMessageSource(MessageSource messageSource);
}

createMessagesMemento() creates a message memo.Take a look at the implementation class:

public class DefaultMessageContext implements StateManageableMessageContext {

	private static final Log logger = LogFactory.getLog(DefaultMessageContext.class);

	private MessageSource messageSource;

	@SuppressWarnings("serial")
	private Map<Object, List<Message>> sourceMessages = new AbstractCachingMapDecorator<Object, List<Message>>(
			new LinkedHashMap<Object, List<Message>>()) {

		protected List<Message> create(Object source) {
			return new ArrayList<Message>();
		}
	};

	/**
	 * Creates a new default message context. Defaults to a message source that simply resolves default text and cannot
	 * resolve localized message codes.
	 */
	public DefaultMessageContext() {
		init(null);
	}

	/**
	 * Creates a new default message context.
	 * @param messageSource the message source to resolve messages added to this context
	 */
	public DefaultMessageContext(MessageSource messageSource) {
		init(messageSource);
	}

	public MessageSource getMessageSource() {
		return messageSource;
	}

	// implementing message context

	public Message[] getAllMessages() {
		List<Message> messages = new ArrayList<Message>();
		for (List<Message> list : sourceMessages.values()) {
			messages.addAll(list);
		}
		return messages.toArray(new Message[messages.size()]);
	}

	public Message[] getMessagesBySource(Object source) {
		List<Message> messages = sourceMessages.get(source);
		return messages.toArray(new Message[messages.size()]);
	}

	public Message[] getMessagesByCriteria(MessageCriteria criteria) {
		List<Message> messages = new ArrayList<Message>();
		for (List<Message> sourceMessages : this.sourceMessages.values()) {
			for (Message message : sourceMessages) {
				if (criteria.test(message)) {
					messages.add(message);
				}
			}
		}
		return messages.toArray(new Message[messages.size()]);
	}

	public boolean hasErrorMessages() {
		for (List<Message> sourceMessages : this.sourceMessages.values()) {
			for (Message message : sourceMessages) {
				if (message.getSeverity() == Severity.ERROR) {
					return true;
				}
			}
		}
		return false;
	}

	public void addMessage(MessageResolver messageResolver) {
		Locale currentLocale = LocaleContextHolder.getLocale();
		if (logger.isDebugEnabled()) {
			logger.debug("Resolving message using " + messageResolver);
		}
		Message message = messageResolver.resolveMessage(messageSource, currentLocale);
		List<Message> messages = sourceMessages.get(message.getSource());
		if (logger.isDebugEnabled()) {
			logger.debug("Adding resolved message " + message);
		}
		messages.add(message);
	}

	public void clearMessages() {
		sourceMessages.clear();
	}

	// implementing state manageable message context

	public Serializable createMessagesMemento() {
		return new LinkedHashMap<Object, List<Message>>(sourceMessages);
	}

	@SuppressWarnings("unchecked")
	public void restoreMessages(Serializable messagesMemento) {
		sourceMessages.putAll((Map<Object, List<Message>>) messagesMemento);
	}

	public void setMessageSource(MessageSource messageSource) {
		if (messageSource == null) {
			messageSource = new DefaultTextFallbackMessageSource();
		}
		this.messageSource = messageSource;
	}

	// internal helpers

	private void init(MessageSource messageSource) {
		setMessageSource(messageSource);
		// create the 'null' source message list eagerly to ensure global messages are indexed first
		this.sourceMessages.get(null);
	}

	public String toString() {
		return new ToStringCreator(this).append("sourceMessages", sourceMessages).toString();
	}

	private static class DefaultTextFallbackMessageSource extends AbstractMessageSource {
		protected MessageFormat resolveCode(String code, Locale locale) {
			return null;
		}
	}
}

The main logic is to leave a backup of the Message for recovery.

3. Advantages and disadvantages of memo mode

Advantage:

  • It simplifies the responsibility of the initiator, isolates the storage and acquisition of state, implements the encapsulation of information, and the client does not need to care about the details of state preservation.
  • Provides status rollback functionality.

Disadvantages: Consuming resources: If there are too many states to save, each save consumes a lot of memory.

Topics: Programming snapshot git svn Database