六、SAM:函数式反应式模式
因为,你知道,弹性——如果你从淘金热的角度来考虑,那么你现在会非常沮丧,因为最后一块金子会消失。但好的一面是,有了创新,就不会有最后的掘金。每一个新事物都会产生两个新问题和两个新机会。—杰夫·贝索斯
In this chapter we are going to explore the SAM Pattern, less used inside the community but still part of the functional/reactive family. SAM stands for State-Action- Model , three actors that we have already encountered in other frameworks, but with the SAM pattern we are going to discover a different use for them. Let’s start with saying that SAM is following the unidirectional flow pattern, also is heavily based on functional concepts; in fact functions are first-class citizens for the correct implementation of the final architecture. Because SAM is a pattern, it’s totally framework agnostic, and it can be used with MobX, Cycle.js, Angular, or any other framework we are using in a specific project, also in our Vanilla JavaScript projects if we want. During this chapter, we explore a “vanilla” implementation of SAM in order to understand how this pattern uses the reactive paradigm for managing the data flow between objects.
SAM 简介
SAM was created with clear goals in mind, and we could summarize them in this way:
- 组合:在 SAM 中,我们大量使用组合和纯函数;这允许我们减少对象之间的耦合,并且容易地在隔离模式的不同部分中进行测试。
- 单向数据流:正如我们在 Cycle.js 中看到的,SAM 也通过高阶函数利用单向数据流的概念,在我们应用程序的不同部分之间创建一个反应循环。
- 框架无关性:SAM 是框架无关性的,因此不仅可以在客户机上使用,如果需要的话也可以在服务器上使用。
- 被动视图:使用 SAM 允许我们拥有被动视图,它们与行为完全分离,允许我们单独测试特定视图,并在运行时或传输时潜在地更改它们,保持相同的行为。
- 分形架构:在我们的旅程中,我们已经遇到了包含分形架构范例的 Cycle.js。SAM 也在做同样的事情:提供使用封装良好的组件的灵活性,这些组件可以按照相同的架构原则组合在一起。
Figure 6-1 provides a schematic to understand this pattern. Figure 6-1SAM pattern schematic As mentioned before, SAM is composed by the state, action, and model , and the flow for a SAM project is the following one:
- 一个动作由用户交互触发,主要职责是将用户意图转化为要在模型中验证的数据。
- 模型需要评估动作接收到的数据,它可以接受也可以不接受。如果数据不符合预期,模型可以决定触发错误或无声地失败。模型还负责维护应用程序状态和触发状态表示。以这种模式实现模型的最佳方式是使用单个状态树。
- 顾名思义,状态是一个从模型中计算状态表示并通知视图需要呈现新内容的函数。状态还负责调用下一个动作谓词,这个名为 nap 的函数将调用任何需要调用的自动动作。
The reactivity in this pattern is composed by the loop of the main actors, which are changing the state representation after every action invoked. SAM pattern could be summarized with a mathematical formula : View = State( viewmodel(Model.present(Action(data))), nextActionPredicate(Model) ) SAM takes inspiration from React components implementation where React introduced a strong decoupling between the data to represent and the view itself; therefore any virtual DOM library fits very well in the SAM pattern. In fact, the state representation is just providing an output that will be computed by a virtual DOM library like React, Vue.js, or Snabbdom. Obviously, we can also think to use template libraries but the Virtual DOM ones fit better for the pattern purposes. The state representation is not meant to keep the state but to merge the data with the virtual dom or template, retrieving the state from the model and combining it with a specific interface. The model, as mentioned above, has to evaluate values provided by an action; it exposes only the present function, and it can decide to accept or refuse the data received, triggering an error or silently stopping the loop for a specific scenario. The actions are very similar to what an intent is doing in Cycle.js: they are preparing the data to be proposed to the model. The actions can be invoked by user interactions on the UI or via the nap method, and this method is called after the state representation for automatically changing the model and triggering another render of the view.
SAM 模式数据流
If we were to summarize with code the SAM pattern data flow , we could do it with a skeleton like this: const model = { status: STATUS.INIT, present: data => { if(model.status === STATUS.INIT){ model.status = STATUS.STATE1; } if(Array.isArray(data)){ model.status = STATUS.INIT; } state.render(model) } }; const actions = { state1: (value, present) => { present({data: value}); } } const nap = model => { if (model.status === STATUS.INIT) { actions.state1(value, model.present); } } const view = { init: model => { return
SAM 的基本实现
This time we want to build an interface similar to Figure 6-2. Figure 6-2Our new interface This interface has some peculiarities, so the first thing to do would be to load the countries data provided by an open API. Then we will need to generate the list on the left of the map above where every time a user is clicking on a country name, we want to display few information on the right side of our UI, like the country name, the flag, the population size, the capital, and an interactive map showing the country coordinates. Considering we start the application without any user interaction we are going immediately to set a default country, so we can fill up the space and provide some information on how to use our application to the final user. In this case the next-action-predicate will help us to achieve this task. We are going now to create the application based on the skeleton we have explored before; remember that the SAM pattern embraces the simplicity of a clean and robust architecture with the power of two programming paradigms like functional and reactive ones.
回顾示例
Let’s start to analyze our example from the beginning. The first thing to do is to wait until the first action for consuming a remote endpoint, retrieving the specific data requested for displaying the selected country details : document.addEventListener("DOMContentLoaded", function(event) { actions.getCountries(REGION, model.present) }); We can immediately see that we are passing to the getCountries action a default country and the present method of the model that will be invoked once the promise inside the action will be fulfilled: getCountries: (region, present) => { fetch(URL + region) .then(response => response.json()) .then(data => { const countries = normaliseData(data); present(countries); }) } Once we receive the response and we normalize the data filtering with only what the view needs, we call the present method injected as argument, and this method is responsible for accepting or not the data prepared by the action. Therefore we can say that these actions have the responsibility of preparing data that the model consumes and uses for then rendering a new state. Let’s see what our model looks like: const model = { status: STATUS.INIT, selectedCountryID: "", countries: [], present: data => { if(data.selectedID !== "" || model.status === STATUS.INIT){ model.status = STATUS.SELECT_COUNTRY; model.selectedCountryID = data.selectedID; } if(Array.isArray(data)){ model.status = STATUS.INIT; model.countries = data; } state.render(model) } }; The model is an object with a status property where we store the current application state, a few parameters used for rendering the new state like countries and selectedCountryID, and finally the method present that is invoked every time by actions only. Inside the present method we can see the checks for each single possible value we are going to receive from an action. This could become a long list of checks, so in large applications we would need to wrap these statements in external files for making the code more readable and easy to manage. Once the checks are passed the present function invokes the render method of the state object: const state = { init: model => (model.status === STATUS.INIT && model.countries.length > 0 && model.selectedCountryID == ""), selectCountry: model => (model.status === STATUS.SELECT_COUNTRY && model.selectedCountryID !== ""), render: model => { stateRepresentation(model); nap(model); } } In the state object, we have some methods used for understanding in which state the application is, like the init and selectCountry methods, and also the render method that is split in preparing the state representation and invoking the nap function. The state representation uses the application state for merging the data stored in the model with the UI to display: const stateRepresentation = model => { let representation =
- 一旦用户与 countries 列表中的元素进行交互,就会触发一个动作,向模型的 present 方法提供国家 ID。
- 该模型检查从操作接收的数据类型,并将操作提供的 ID 存储在 selectedCountryID 中。
- 然后,模型触发状态表示,我们根据模型数据组装新视图。
- 视图被推送到 display 方法,该方法会将新的视图状态附加到 DOM。
- 最后,我们调用 nap 函数,检查在特定的应用程序状态下,我们是否需要触发任何自动操作。
SAM is an interesting pattern because it combines S.O.L.I.D. principles with different programming paradigms. When we want to implement it in a real project we need to be aware of the freedom provided by this pattern, considering at the moment there aren’t many frameworks that are enforcing SAM rules. At the same time, SAM provides us a great structure, a strong encapsulation, and applies the single responsibility principle very well for each main part of the pattern. This allows us to test quickly and in total isolation our projects, and considering it’s heavily based on pure functions, we are not going to have any side effects from them. Therefore the outcome predictability of this pattern is very high. As I said at the beginning of the chapter, we can implement SAM in our existing architectures or we could use an architecture that bases its foundations into SAM providing a solid structure for handling elegantly the data flow of our applications.
总结
In this chapter, we looked at another approach to reactive architectures. As we have learned, there are many interpretations and all of them are providing pros and cons to take in consideration. SAM in particular has solid foundations with great principles to be inspired by, and the simplicity of its implementation grants us the ability to apply the SAM pattern in any project, existing or greenfield, without the need to use specific frameworks for using it. Now it’s time to look at the future of reactive paradigm and in particular how we will envision this paradigm in the front-end panorama for the next years. But this time I won’t be alone, so let us move on to the last chapter of this book.
版权属于:月萌API www.moonapi.com,转载请注明出处