DEV Community

FEConf
FEConf

Posted on

The Architecture of Web-Based Graphic Editors and 7 Design Patterns (Part 2)

*This article is a summary of the talk The Architecture of Web-Based Graphic Editors and 7 Design Patterns presented at FEConf 2023. The content of the presentation will be published in a two-part series. Part 1 covered the basic architecture of a web-based graphic editor and the design patterns embedded within it. Part 2 will take a deeper dive into design patterns by actually implementing a graphic editor and addressing its problems. All images in this article are from the presentation slides of the same name and are not individually cited. The presentation slides can be downloaded from the FEConf 2023 website.

'The Architecture of Web-Based Graphic Editors and 7 Design Patterns' / Heungwoon Shim, Frontend Engineer at Naver, presented at FEConf 2023

Implementing a Graphic Editor and Applying Design Patterns

In this section, we'll explore how to apply five design patterns by implementing a sample graphic editor. I recommend focusing on the overall flow and concepts rather than the specific implementation details.

1. Creating Controllers (Parts) from a Saved Model

Let's assume the sample graphic editor we're building supports a selection tool, a rectangle tool, and a pen tool. The domain model for this editor can be represented by the following JSON, consisting of rectangles, circles, and paths.

Image description

The domain model of the graphic editor

Now, let's look at our first goal: "Creating controllers from the model." As I mentioned earlier, the internal structure is implemented as shown in the diagram below. The highlighted parts are the structures involved in creation. The graphic editor reads the model and displays it in the graphic viewer. So when the model is passed to the graphic editor, it appears in the graphic viewer.

Image description

Creating controllers (parts) from the model

This process can be expressed in pseudocode as follows. The code on the left shows the graphic editor telling the graphic viewer to create parts, passing the model as a parameter. The graphic viewer then calls createParts, receives the model as a parameter, iterates through the model array, and creates a specific part for each item based on its type. After creating the parts, it renders them and displays them on the screen using addPart.

Image description

Pseudocode for creating a controller from a model

But doesn't the part-creation logic seem a bit off? The highlighted code below needs to be modified every time a new shape is added. We previously said that we can use design patterns to isolate the parts that change. Here, we'll use a Simple Factory to isolate this changing part.

Image description

Inefficient code for creating parts

The Factory Trio

Before we look at the solution, let's briefly review factories. There are three types of factories:

  1. Simple Factory: A pattern that centralizes object creation in one class.
  2. Factory Method Pattern: A pattern where a superclass provides an interface for creating objects, but lets subclasses decide which class to instantiate.
  3. Abstract Factory Pattern: A pattern that implements an interface and creates a family of products using composition.

Since a Simple Factory is sufficient to solve this example, we'll use that.

Solution - Isolating the Changing Part

We move the if-else conditional statement from the highlighted code into a PartFactory. Because the PartFactory is now separate, the GraphicViewer that calls it no longer needs to know about the specific part types. Therefore, the createParts function can simply iterate through the model and call the PartFactory to create the parts. This means that if new types are added or changed, the GraphicViewer doesn't need to be modified; only the PartFactory needs to be updated.

Image description

Isolating the changing part

This method offers two main advantages:

  1. Open-Closed Principle (OCP)
  2. Dependency Inversion Principle (DIP)

By using a Simple Factory, we adhere to the OCP, being open for extension but closed for modification. We also adhere to the DIP by delegating creation responsibility to the PartFactory and depending on the Part abstraction.

2. Creating Views (Figures) from a Controller

Now let's look at creating views. The model we looked at earlier was a one-dimensional model. However, we don't just edit one-dimensional models; we also need to handle nested structures, layers, and groups. How can we represent these nested structures? A nested model would look like the diagram below.

Image description

Nested model

There's a pattern that makes it easy to handle nested models: the Composite pattern. The Composite pattern allows clients to treat individual objects and composite objects uniformly.

Image description

Composite pattern

The concept of the Composite pattern originates from the idea of a part-whole hierarchy. It treats a tree of nodes as if it were a single object. In other words, the gray group area in the diagram below is treated the same as the light blue node object.

Image description

Part-whole hierarchy

This is why this pattern is often used when implementing graphical user interfaces, and you can see that it's very similar to React's rendering process.

Using this pattern, you can treat individual views and nested views as the same type for rendering. In the code on the left, the addChild function takes a part as a parameter, which can be either a single object or a composite object. Either type can be added as a child. Next, in the render() code, the part's render method is called when the model is first loaded or changed. It renders itself and then iterates through its children to render them.

However, there's one problem here. For components that cannot have children, like a text part, you need code to throw an error when addChild is called, as shown in the code on the right.

Image description

Composite pattern

To summarize, this pattern allows you to recursively apply operations like rendering to an entire structure with simple code.

Features of the Composite Pattern

The previous example violated the SRP (Single Responsibility Principle). That's why the Composite pattern is known for trading SRP for transparency. Here, transparency means treating individual and composite objects as the same type. This reduces type safety. As seen with the text part in the previous example, logically inconsistent operations can occur, and you must prevent them. Therefore, this pattern is said to require a balance between transparency and safety during design.

Image description

Features of the Composite pattern

3. Modifying the Model Using Tools

Next, let's look at modifying the model using tools. The structures related to the editing process include the Event Dispatcher, Command Stack, Root Part, Tool, Request, and EditPolicy, as shown in the diagram below.

Image description

Structure related to editing

As mentioned before, an editing task is "the process of converting an event into a state change." There is a special component in this process: the "Tool". If we look at this process in detail with the structure introduced earlier, it looks like the diagram below.

Image description

Internal structure of the event flow

The Event Dispatcher is responsible for converting low-level browser events into high-level editor events. The Event Dispatcher receives an event through a masking layer, processes it, and passes it to the canvas.

Features of a Tool

The tool described earlier has a distinct feature: even with the same event, the result must differ based on which tool is selected. For example, if the rectangle tool is selected, dragging should draw a rectangle. If the pen tool is selected, it should draw with a pen. In other words, the graphic editor can have only one tool active at a time.

Implementing a Tool

The code on the left is for the Event Dispatcher. The event listening part is represented in pseudocode. For example, it receives a mousemove event and calls transmitMouseMove. transmitMouseMove then passes the event to the viewer. In the viewer code on the right, it receives the event and draws the appropriate element—a rectangle, circle, or pen stroke—on the screen based on the currently active tool.

Image description

Tool implementation

There's a problem with this code as well. The if-else statement on the right has to grow every time a new tool is added. This means the GraphicViewer code is not closed for modification. Also, similar code is repeated.

Solution - The State Pattern

The way to solve this problem is with the State pattern. In the code on the right, the if-else statement is removed, and the event is passed directly to the tool in the receiveEvent method. The Tool becomes an object that represents the current state, with an abstract base class delegating state-specific handling to concrete subclasses. In other words, representing the current state as a single class is the essence of the State pattern. This allows you to isolate the behavior for each state into its own class, making code management easier and eliminating repetitive conditional logic.

With the State pattern, the GraphicViewer is now closed for modification but open for extension, allowing us to adhere to the OCP.

Image description

State pattern

To summarize, the rectangle, circle, and pen tools from the previous example are now implemented as concrete classes that handle their specific states. This way, you only need to inherit from a base Tool class to implement the functionality for a new tool.

4. Completing the MVC Structure

Now let's revisit the event flow so far and complete the MVC structure. We just learned about tools, but is it desirable for the tool to modify the model directly, as in the previous example? In an MVC structure, who should know how to modify the model?

For example, let's say we have an eraser tool that erases a bitmap. Since this eraser is only for bitmaps, it should be able to erase the bitmap image on the left but not the vector image on the right.

In this situation, if the eraser tool modifies the model directly, it needs to know about both erasable and non-erasable parts. Then, every time a new part is added, it needs to know whether that element is erasable or not. This means the code must be constantly modified as new elements are added, violating the Open-Closed Principle for the eraser tool.

To solve this, the part itself should know how to interpret the event. When the eraser requests to delete an element, the part receives this event and handles it.

Image description

Event handling for the eraser tool

Let's think about this more fundamentally. If we use MVC, who modifies the model? The Controller. In our example, the Part can be considered the controller. Therefore, the Part must know how to interpret events and be able to modify the model. And the tool doesn't pass the event directly to the Part; instead, it processes the event into a Request object and asks the Part to modify the model. This allows various editing requests to be encapsulated in a Request object. Here, the Request can be considered a high-level event.

So, should we put the model modification logic directly inside the Part? That's also problematic. Because if the editing logic is similar, duplicate logic could occur across different Parts. This would once again violate the Open-Closed Principle for the Part.

Image description

The problem with parts knowing the modification logic

Solution - The EditPolicy Pattern

A smart way to solve this problem is with the EditPolicy pattern. An EditPolicy is a micro-controller created by separating the model modification logic from the Part, making this logic reusable.

Image description

EditPolicy

The code below represents the drag behavior of a tool. When a mousedown event occurs, dragging starts, and when a mouseup event occurs, dragEnd is called. In dragEnd, the changeModel function is called to change the model. And the changeModel method of the ResizeTool performs a specific task, as shown below.

Image description

EditPolicy Example - Resize Tool

Now let's separate the Policy from the ResizeTool. We move the model modification code from ResizeTool to ResizePolicy. This allows the changeModel of ResizeTool to be abstracted. And we add an installPolicy function to the Part, allowing it to manage multiple policies. In other words, the policies are stored in an array, allowing the Part to apply various controller logics.

Image description

Extracting the EditPolicy

This means the ResizeTool only needs to send the request, and the changeModel logic that was in ResizeTool is moved to its superclass, Tool.

Image description

Delegating to the superclass

The Strategy Pattern

The EditPolicy we just looked at is an implementation of the Strategy pattern. This pattern works by separating the changing algorithm (the model modification logic) from the context (the Tool and Part) and delegating the task to a Policy object. The Part can choose a Policy to change its behavior dynamically, even at runtime. It also makes algorithms easy to reuse.

Image description

Features of the Strategy pattern

5. Managing Work History

This section is related to undo and redo, which are used to manage the work history. In the event flow we've built so far, an event passes through the Event Dispatcher, Graphic Viewer, Tool, and Part, eventually causing the EditPolicy to modify the model.

Image description

Command creation by EditPolicy

If we improve the EditPolicy process one more time, instead of the EditPolicy modifying the model directly, it creates an object called a Command. This command is pushed onto the Command Stack, which in turn executes it to change the model. Inside the Command Stack, Command objects that encapsulate model modifications are stacked one by one, enabling operations like undo and redo.

Image description

Command Stack

A summary of the final flow is shown in the diagram below. The tool receives an event, requests the Part's EditPolicy to create a command, and this command then modifies the model. When the model is modified, the Part requests a refresh, and the view is updated.

Image description

The final MVC flow

Conclusion

So far, we've explored seven design patterns through the architecture of a web-based graphic editor. We also explored how to apply design patterns by improving sample implementation code.

What I've shared today is just one example of how design patterns can be applied. I hope this experience will be valuable as you build your own graphic editors and helpful for applying design patterns in your work. I encourage you to discover better patterns through your own experiences, share them with others, and collaborate on their application. Thank you.

Top comments (0)