MVC vs Flux(实用库 - 状态库原理)

508 阅读9分钟

(2022-12-30 小白初识MVC、Flux、DDD、CQRS、CQS) 起初接触这MVC、Flux两个概念是读到一篇文章,有一个提问“React解决了什么问题”,文章给出的解答是React是一个用于视图层的类库,它搭配其他的一些库,可以构建出一个Flux架构,相对于传统的MVC架构,Flux有些优点。具体下面接着讨论。

MVC概念以及优缺。

image.png 可见,MVC是将整个系统分成三个部分,【View】、【Controller】、【Model】。 系统用户与View发生交互,View将用户的操作事件传递给Controller,Controller接收到事件,会通知Model更新(update)数据,Model更新数据后,会通知View数据发生了更新(notify),View会获取Model中的数据(query),View的就会发生相应变化。 这是一个非常简单的结构,在简单的系统当中,使用MVC的结构逻辑是可以的。但是:

  • 当系统复杂以后,Controller里面包含的控制逻辑将会非常的繁杂。(虽然原则上是skinny Controller,fat Model,但是Controller里面还是会或多或多少的涉及业务逻辑,当系统庞大时,Controller就会变得臃肿)
  • 当系统复杂以后,MVC架构中一个Model的变化可能在造成多个View改变(级联修改)。(一个Model可能会关联多个View,一个Controller也可能会去控制着多个Model修改。) MVC-complax.png 如上图所示。当View1的发生变化,它会促使Controller1去更新Model1从而更新数据并通知View1获取新数据-更新界面。但是此时Model1上面还关联有View2,Model1的更新会导致View2去query数据,然后触发event,致使Controller2发生update事件,并update Model2,进而影响到View3。 可以看见仅仅是View1的改变导致Model1发生数据更新,就会导致View2和View3都发生改变。如果系统非常庞大复杂,这个数据流的复杂度会非常的大。 (这里我不是很理解的是为什么Moel1的数据更新会去通知View2进行query操作,以及View2的query操作为什么会触发action event从而使Controller2去update Model2。。所以小白嘛,之后找个机会自己撸一撸MVC结构的代码)

In MVC,a Model can be read by multiple Views,and can be updated by multiple Controllers.In a large application, this result in highly complex interactions where a single update to a Model can cause Views to notify their Controller,which may trigger even more Model updates.

在MVC当中,一个Model可以被多个View读取,并且可以被多个Controller进行更新。在大型应用当中,单个Model会导致多个Views去通知Controllers,并且可能触发更多的Model更新,这样结果就会变得复杂。

当MVC的系统非常庞大时,这种情况将会变得非常复杂。(就像往一个湖里扔一块石头,涟漪会越传越多)。

Flux架构概念以及优缺点

Flux架构就是强制实现一种单向数据流,来避免 MVC架构应用在庞大系统中的问题。 Flux2.png View触发的事件不是直接传递给Controller,而是传递给Action creator去生成一个Action,Action生成以后传递给一个集中的Dispacher,Dispacher去分发Action给Store,Store接收到这个Action,去更新相应的数据,然后将数据传递给View,更新界面。

那么Flux架构为什么能够解决MVC架构的问题呢?

The main difference between MVC and Flux is the separation of queries and updates.In MVC,the Model is both updated by the Controller and queried by the View.In Flux,the data that a View gets from a Store is read-only.Store can only be updated through Actions,which would affect the Store themselves not the read-only data.

MVC和Flux最大的不同就是查询和更新的分离。在MVC中,Model同时可以被Controller更新并且被View所查询。在Flux里,View从Store获取的数据是只读的,而Store只能通过Action被更新,这就会影响Store本身而不是那些只读的数据。

这里要描述一下DDD(Domain Driven Design, 领域驱动设计),CQRS(Command-Query Responsibility Segregation, 命令-查询职责隔离),CQS(Command-Query Separation, 命令-查询分离)。

DDD(Domain-Driven Design | 领域驱动设计)

假设已知DDD基础知识,但是没有相关基础的话这篇文章也依然有价值。想了解更多关于DDD的知识,推荐InfoQ有关这个话题的免费电子书

CQRS(Command-Query Responsibility Segregation | 命令-查询职责隔离)

这里要讲到一个概念——"聚合体"(Aggregate),聚合体的概念就是数据集合。在一般的DDD中,它既有数据,也有更新和查询方法。执行更新方法和查询方法也有一个库维护。 Aggregate.png

##命令-查询分离(CQS)##

如果一个方法改变(mutates)了对象的状态(state),那它就是一个命令(command),那么它一定不能返回值;

如果一个方法返回值,那它就是一个查询(query),那么它一定不能改变(mutate)对象状态(state)。

CQRS就是CQS的一种进阶,将CQS(Command-Query Separation | 命令-查询分离)中Aggregate的命令和查询方法分离到不同的对象中。

CQRS simply takes CQS further by separating command and query into different objects.Aggregates would have no query methods,only command methods.Repositories would now only have a single query method(e.g. find),and a single persist method(e.g. save).

CQRS中有几个概念(这些是普通DDD里找不到的新对象):query-model, query-processor, command-model, domain-event, command, command-handler, event-subscriber。

1、query-model | 查询模型

query-model是一个纯粹的数据模型,也就是整个架构体系中的数据存放处(类似于MVC中的Model)。

2、query-processor | 查询处理器

检索query-model是通过执行query方法来实现的。query-processor就是处理View传递过来的query请求,query-processor知道根据query请求如何在query-model(或数据库表)中查找数据。

query-processor.png

3、command-model | 命令模型

command-model就是接受来自command-handler的command,然后执行command-model中相应的方法,command-model中的方法执行之后都要发布一个领域事件(domain-event),这个domain-event具有最近进入到command-model的command名字以及执行该command所需的充足的信息。

(command-model中只包含command methods,不包含query methods。) command-model.png

4、domain-event | 领域事件

command-model运行command method完成后发布的事件。domain-event里面包含事件的名字以及能让订阅者(event-subscriber)正确更新查询模型(query-model)的充足且有效信息(payload)

Note: domain-event are always in past tense since they decribe what has already occurred(e.g. 'ITEM_ADDED_TO_CART').

注意:领域事件总在过去时,因为他们描述着已经发生的事情。

5、event-subscriber | 事件订阅者

event-subscriber在query-processor里对command-model中发布的所有domain-events进行订阅监听。当command-model发布了一个domain-event时,相应event-subscriber就会执行,然后使query-model的对应数据发生更新(这一过程也是发生在query-processor)。 event-subscriber.png

6、command | 命令

Commands are submitted as the means of excuting behaviour on Command Models.A Command contains the name of the behaviour to execute and a payload necessary to carry it out.

命令模型所执行的行为就意味着所提交的命令。(说人话:commands就是用户与view发生交互传递过来的事件命令,需要借助command-handler将它映射为不同的command-method,以发布不同的domain-event)。

一个命令包含这个要执行的行为的名字和需要携带的负载(payload)。

Note: Commands are always in imperative tense since they describe behaviours that need to be executed(e.g. AddItemToCart).

注意:命令总是命令式的,因为他们描述需要被执行的行为。

7、command-handler | 命令处理器

command-handler就是接收来自用户的command,然后根据command去库中找出对应的command-model,并执行对应的方法(command-method)。 command-handler.png

为什么Flux可以解决MVC的问题

MVC架构,数据Model既被Controller所update,又被View所query。而在Flux架构中,数据存储在Store中,View读取到的是Store的数据,为read-only。而View传递过去的状态改变事件,只能改变Store的状态而不是Store中的数据。

Flux是如何实现CQRS的?

Flux-simple.png Flux角色对应于CQRS中的角色:

View: View;

Store: query-processor;

Action-creator: command-handler;

Dispacher: command-model;

Action:domain-event;

CQRS-Flux.png 想想MVC的问题是当系统复杂以后,某个Model的更新,被多个View监听,而导致多个Controller去更新其它的Model,致使整个系统数据流复杂无比,甚至可能导致数据更新渲染死循环。 在Flux,某个View的事件不会直接去促使Controller更新Model,而是通过Command-handler。

Flux的缺点

Flux虽然解决了MVC的痛点,但是Flux架构在传统的DDD上多设置了很多对象,这是值得的吗?

当系统简单,系统中每个View与Controller、Model正确的对应,不会引起级联修改,那么MVC的架构是更优选择。

代码示例

一个普通的DDD购物车

class ShoppingCart {
    constructor (id: id, cartItems=[] ) {
        .id = id;
        .cartItems = cartItems;
    }
    // command method
    addItem () {}
    removeItem () {}
    // query method
    total () {}
}
class CartItem {}
class ShoppingCartRepository {
    all () {}
    findItemById () {}
    create () {}
    update () {}
    delete () {}
}

可见普通的DDD的Aggregate对象里面既有该改变对象的状态命令方法(addItem,removeItem),也有仅返回值的查询方法(cartItems, total);

CQRS改造后的购物车

CQRS有几个重要职责的角色,command-model, domain-event, event-subscriber, query-processor, query-model, command-handler, command。

// command-model, only contains command methods, no query methods, publish Domain Events
class ShoppingCart {
    constructor () {}
    addItem (cartItem) {
        DomainEventPublisher.publish('ITEM_ADDED', {
            cartId: .id,
            sku: cartItem.sku,
            price: cartItem.price,
            quantity: cartItem.quantity
        });
    }
    removeItem (cartItem) {
        DomainEventPublisher.publish('ITEM_REMOVED', {
            cartId: .id,
            sku: cartItem.sku,
            price: cartItem.price,
            quantity: cartItem.quantity
        });
    }
}
// query-processor, contains query-model(which is an Object),query methods,update methods
class ShoppingCartStore {
    constructor () {
        // holds query-model in memory
        .total = {}
        
        // subscribe to events that allows this store to update its query models
        DomainEventPublisher.subscribeTo('ITEM_ADDED', handleItemAdded);
        DomainEventPublisher.subscribeT('ITEM_REMOVED', handleItemRemoved);
    }
    // update methods
    handleItemAdded ({cartId: cartId, cartItem: cartItem}) {
        total = .totals[cartId] || ;
        newTotal = total + cartItem.price * cartItem.quantity;
        .totals[cartId] = newTotal;
    }
    handleItemRemoved ({cartId: cartId, cartItem: cartItem}) {
        total = .totals[cartId] || ;
        newTotal = total - cartItem.price * cartItem.quantity;
        .totals[cartId] = newTotal;
    }
    // query method
    total (cartId) {
        return {
            cartId: cartId,
            total: .totals[id]
        };
    }
}

// command-handler, maps commands to command methods on ShoppingCart
class ShoppingCartCommandHandler extends CommandHandler {
    constructor (repo) {
        .repo = repo;
        
        // Assumes commands implement subscribe that appends the handler to themselves. 
        AddItemToCart.subscribe(.addItemToCart); 
        RemoveItemFromCart.subscribe(.removeItemFromCart);
    }
    
    addItemToCart(payload) {
        cart = .repo.find(payload.cartId);
        cart.addItem(payload.cartItem); // this publish a Domain Event
    }
    removeFromCart(payload) {
        cart = .repo.find(payload.cartId);
        cart.removeItem(payload.cartItem); // this publish a Domain Event
    }   
}

写在文末

本文是一个小白初识 MVC与Flux架构,仅仅是认识其概念以及外形,对本质的探究还是差了一点,其实还有很多问题没有解决(如:MVC问题出的实际场景,Flux为什么能够解决MVC的问题,用code说话)。本文只是为后期更进一步发展打下基础。两个架构的本质还需要更进一步的深入。

对于小白,短期的学习方向会更偏向于基础知识夯实、框架知识丰富、工程化能力加强、项目/业务加强、运行环境、数据结构/算法、计算机基础等方向的深化。