27 | Practical combat (2): How to design a "drawing" program?

At the beginning of the last lecture, we entered practical mode. I reviewed it and found that although this program looks relatively simple, there are still many things that need to be explained but have not been explained clearly.

My personal expectations for this example are relatively high. Because I think the "draw" program is very suitable as a first lesson in architectural practice. The "Paint" program requires very high scalability. It is completely a mini Office program, which is very suitable for talking about the evolution of the architecture from shallow to deep.

So today I slightly adjusted my plan, postponed the lecture on server-side docking, and added a "Practical Combat (Part 2)" article. On the one hand, this "middle" chapter will make up for the lack of clear explanations in the previous "Practical Combat (Part 1)" chapter, and on the other hand, it will iterate on the requirements of the "drawing" program.

MVP printmaking program

Let’s go back to the “Practical Combat (Part 1)” chapter first. This version is basically an MVP version of the drawing program: it can only add new graphics, but cannot delete or modify it.

How? Let's look at the Model layer first. Its code is a dom.js file. In terms of data structure, it is a DOM tree with QPaintDoc as the root. This DOM tree has only three levels: Document -> Shape -> LineStyle. Specific details can be found in the table below:

This table lists the coupling relationships between Model, View, and Controllers: What does Model provide for them? It can be seen that the View layer currently has no other requirements for the Model layer except drawing (onpaint). Each Controller's demand for Model seems to have a lot of methods, but in fact it has only one purpose, which is to create graphics (addShape).

Let’s look at the View layer again. Its code is mainly an index.htm file and a view.js File. The View layer only depends on the Model layer and only one doc.onpaint function. So we focus on the functions of View itself.

The View layer has only one QPaintView class. We divide its functions into three categories: those related to the responsibilities of the Model layer, those related to the responsibilities of the View itself, and those that serve the Controller layer, as shown in the following table.

Finally, let's look at the Controller layer. There are many files in the Controller layer. Some Controllers are merged into one file because of similar implementation, as shown below.

Menu, PropSelectors, MousePosTracker:accel/menu.js

Create Path:creator/path.js

Create FreePath:creator/freepath.js

Create Line, Rect, Ellipse, Circle:creator/rect.js

Controller is located at the top of MVC. Our focus on it is no longer its specification itself, and no one calls its methods. So we focused on how each Controller uses Model and View.

We have made a table as follows. Note that the Controller's use of Events is listed separately from the View.

By comparing the above three tables, you can clearly see how Model, View, and Controllers are related.

Improved version of the drawing program

If you use the MVP version of the drawing program, you will find it difficult to use. After all, the graphics cannot be changed after they are created. So we plan to make a new version with some improvements in functionality.

Select a graphic to allow deletion, movement, or modification of its style.

Graphic style adds fillColor (fill color).

A more modern interaction paradigm: It is in the ShapeSelector state by default and automatically returns to this state after the shape is created.

After selecting a graphic, the current style on the interface is automatically updated to the style of the selected graphic.

How to change our program?

For a complete comparison of the differences, see:

https://github.com/qiniu/qpaint/compare/v26...v27

Below, we will explain in detail the thinking behind these modifications.

Let’s look at the Model layer first. The new specifications are shown in the table below.

dom.js

In order to facilitate everyone's understanding, we have made a ChangeNotes table of Model, as follows:

Most of them are the addition of new functions, not to mention. We focus on one point: QLineStyle was renamed to QShapeStyle, and its properties width and color were renamed to lineWidth and lineColor. These are incompatible modifications and are equivalent to a small refactoring.

The key to refactoring is to handle it in a timely manner and control the quality. Especially for weakly typed languages ​​like JavaScript, the mental burden of refactoring is greater. In order to ensure that the quality is still controllable, it is best to supplement it with enough unit tests.

This is also the reason why individuals prefer statically typed languages. If there is any omission in the refactoring, the compiler will tell you where the omission has been made. Of course, this does not mean that unit testing can be omitted. For every language, automated testing is always an important means of quality assurance.

The topic returns to graphic styles. When we first created new QLine, QRect, QEllipse, and QPath, the last parameter passed in was QLineStyle. This was a design mistake, which means that these subsequent constructs still need to add more parameters such as QFillStyle.

Change the last parameter to QShapeStyle and it's complete by design. Even if there are more evolutions in graphic styles later, they will be concentrated on the QShapeStyle class.

The current data structure of QShapeStyle is as follows:

class QShapeStyle {
  lineWidth: number
  lineColor: string
  fillColor: string
}

So, is this reasonable? What are the potential future evolutions?

The key to deducing the evolution of demand is how far you look. Currently, all types of GDI have rich support for LineStyle and FillStyle. So if it is a drawing program that actually needs to iterate, the above QShapeStyle will inevitably face a reconstruction. It becomes like this:

class QLineStyle {
  width: number
  color: string  
}

class QFillStyle {
  color: string  
}

class QShapeStyle {
  line: any
  fill: any
}

Why is line in QShapeStyle not QLineStyle, and fill not QFillStyle, but any type? Because they are just simple versions of line styles and fill styles.

For example, in the GDI system, FillStyle can often be a picture tile or multiple color gradient fills, which cannot be represented by QFillStyle. So a better name for QFillStyle here might be QSimpleFillStyle.

After talking about the Model layer, let’s look at the View layer.

view.js

The View layer has not changed much. In order to give you a more intuitive feeling, a ChangeNotes table is also listed here, as follows:

Among them, properties were renamed to style, get lineStyle() was deleted, and properties were unified into style. This is related to the small reconstruction of the Model layer I mentioned above, and is not caused by the functions of this new version.

So the real changes to the View layer are two:

Selection is introduced, currently only one shape can be selected; the onSelectionChanged event will be emitted when the selection changes;

Introduced the onControllerReset event, which is emitted when the Controller completes or abandons the creation of the graph.

Introducing selection is more conventional. When the View becomes complex, there will usually be a selection. The only thing that needs to be considered is what changes will occur to the selection. For Office programs, it is unreasonable if the selection is only allowed to be a single shape, but we will skip it here and not expand it.

Let’s focus on the onControllerReset event.

The onControllerReset event is emitted by the Controller that created the graphic (such as QPathCreator, QRectCreator, etc.) and received by the Menu Controller.

This raises a question: How many more similar situations will there be? Will there be more events that need to be passed between Controllers in the future and require View to relay them?

This issue involves the design of the View layer event mechanism. Related to this issue are:

Whether to support arbitrary events;

Does the listening event support unicast or multicast?

From the most general perspective, it definitely supports arbitrary events and multicast. For example, we define a QEventManager class with the following specifications.

class QEventManager {
  fire(eventName: string, params: ...any): void
  addListener(eventName: string, handler: Handler): void
  removeListener(eventName: string, handler: Handler): void
}

However, View's event mechanism setting requires a balance between versatility and architectural controllability. Once the View aggregates this QEventManager, it is universal, but what kind of events will fly between Controllers is more difficult to control from the mechanism.

Code is documentation. If something can be constrained in code, it is best not to constrain it in documentation.

Therefore, even if we implement the QEventManager class at the bottom level, I personally do not tend to expose it directly in the interface of View. Instead, I define more specific fireControllerReset and onControllerReset/offControllerReset methods to make the dependencies of the architecture intuitive.

The specific code looks like this:

class QPaintView {
  constructor() {
    this._eventManager = new QEventManager()
  }
  onControllerReset(handler) {
    this._eventManager.addListener("onControllerReset", handler)
  }
  offControllerReset(handler) {
    this._eventManager.removeListener("onControllerReset", handler)
  }
  fireControllerReset() {
    this._eventManager.fire("onControllerReset")
  }
}

After talking about the View layer, let’s talk about the Controller layer. We also list how each Controller uses Model and View, as follows.

Menu, PropSelectors, MousePosTracker:accel/menu.js

ShapeSelector:accel/select.js

Create Path:creator/path.js

Create FreePath:creator/freepath.js

Create Line, Rect, Ellipse, Circle:creator/rect.js

There's a lot of content. In order to see the difference more clearly, we made a ChangeNotes table as follows:

First of all, the changes to Menu, QPathCreator, QFreePathCreator, and QRectCreator are mainly due to the introduction of a new interaction paradigm, for which we introduced the onControllerReset event. Another change is that QLineStyle becomes QShapeStyle, which has been discussed in detail before and will not be mentioned.

So there are actually two main changes to the Controller layer.

One, PropSelectors. This Controller is much more complex than the previous version: before it was just to modify the properties (now style) of the View so that it could be referenced when creating graphics. Now when it is changed, it will also affect the selection (selected graphics), changing its style; moreover, when the selection changes, the interface will be automatically updated to reflect the style of the selected graphics.

Second, QShapSelector. This is a newly added Controller that supports selecting graphics, deleting, and moving selected graphics.

Through this iteration of requirements, we can see that the current division of labor among Model, View, and Controller can make the decomposition of requirements very orthogonal.

Model only needs to consider the evolution of data structures caused by requirements and abstract a business interface that is natural enough. The View layer is very stable and mainly serves as a bridge between various roles. In the Controller layer, each Controller performs its own duties and will not be interfered by each other's needs.

Conclusion

Today we combined the "Paint" program to reorganize the MVC architecture. And we went one step further, by conducting a demand evolution on the drawing program to observe the sensitivity of each role in the MVC architecture to demand changes. It needs to be emphasized again that although we are based on Web development, the drawing program we currently provide is essentially a stand-alone version.

Guess you like

Origin blog.csdn.net/qq_37756660/article/details/134967934