软件架构设计模式
有三种主要的设计模式分别是mvvm和mvc以及mvp设计模式,主要通过分离关注点的方式来组织代码结构,优化我们的开发效率
Contraller/Presenter负责业务逻辑,Model负责管理数据。View负责显示。
假设我们在进行项目开发的时候,使用单页应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里,如果项目过大,那么代码就会非常冗长,难以维护。
mvc模式
- MVC通过分离Model、View和Contraller的方式来组织代码结构。其中View负责页面的显示逻辑,Model负责存储页面的业务数据以及对相应数据的操作。
- 并且View和Model应用了观察者模式,当Model层发生改变的时候,他会通知有关View层更新页面
- Contraller层是View层和Model层的纽带,主要负责用户与应用的响应操作,View和Model是相互解耦的,他们不直接交互,而是通过Controller来进行通信。当用户与页面产生交互的时候,Controller根据事件的类型和用户输入处理相关的业务逻辑,并更新Model中的数据。然后Model层再去通知View层更新。
- 在MVC中,View需要主动从Model中获取数据,并由Controller负责将数据传递给View进行展示。这使得开发者需要手动编写代码来同步更新数据和UI,相对来说,MVVM的数据绑定机制在这方面更加简化了开发流程。
在开发过程中十分繁琐,不灵活,都是单向的,一个小的事件操作,都必须要经过这样的流程,开发不便捷
这种模式在开发中更加灵活,但是这种灵活也伴随着问题:
-
数据流混乱:
-
View比较庞大,而Controller比较单薄:由于很多开发者都会在View中写一些逻辑代码,导致View中的内容越来越庞大,而Controller变得越来越单薄(单纯的数据监听)
mvp模式
- MVP通过分离Model、View和Presenter的方式来组织代码结构其中View负责页面的显示逻辑,Model负责存储页面的业务数据以及对相应数据的操作。
- 并且View和Model应用了观察者模式,当Model层发生改变的时候,他会通知有关View层更新页面
- MVP模式与MVC唯一不同的在于Presenter和Contraller
- 在MVC模式中我们使用观察者模式,来实现Model层数据发生变化时通知View层的更新,这样View层和Model层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,可能会对代码的复用性造成一些问题。
- MVP模式通过使用Presenter来实现对View层和Model层的解耦,MVC中的Contraller只知道Model的接口,因此它没有办法控制View层的更新,MVP模式中,View层的接口暴露给了Presenter,因此可以在Presenter中将Model的变化和View的变化绑定在一起,以此来实现View和Model的同步更新,实现了对View和Model的解耦
- Presnter还包含了其他的响应的逻辑
MVVM模式
-
MVVM通过分离Model、View和ViewModel的方式来组织代码结构其中
-
View:是用户界面的可视化部分,负责展示数据并与用户进行交互。View通常由XML、HTML、XAML等描述,这取决于具体的开发平台。在ArkUI中为@Components修饰组件渲染的UI
-
Model:表示应用程序的数据模型或业务逻辑,负责处理数据的存取、处理和操作。它通常包含数据结构、数据库操作、网络请求等。Model并不直接与UI层交互,它只暴露一些接口供ViewModel层调用,使得ViewModel可以获取所需的数据。
-
ViewModel:层类似媒婆,起到了牵线搭桥的作用,负责将数据从Model中取出并转换成View可用的形式。ViewModel不直接操作View,而是通过数据绑定机制将数据与View进行绑定,使得数据的变化可以自动反映在View上,实现了数据的双向绑定。在ArkUI中,ViewModel是存储在自定义组件的状态变量。
- 自定义组件通过执行其build()方法或者@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。
- View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了@Watch回调方法用于监听状态数据的改变。
- 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。
- ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。
- ViewModel通常也包含用户交互的逻辑,例如处理用户输入、按钮点击等
- 它的思想和MVP其实是相同的,不过ViewModel通过双向的数据绑定,将View和Model的同步更新给自动化了,当Model发生变化时,ViewModel就会自动更新,ViewModel变化了,View也会更新,这样就将Presenter中的工作给自动化了。
-
MVVM在保持View和Model松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性
ViewModel的数据源
ViewModel包含多个数据源。@State和@Provide装饰的变量以及LocalStorage和AppStorage都是顶层数据源,其余装饰器都是与数据源做同步的数据。装饰器的选择取决于状态需要在自定义组件之间的共享范围。共享范围从小到大的排序是:
- @State:组件级别的共享,通过命名参数机制传递,例如:CompA: ({ aProp: this.aProp }),表示传递层级(共享范围)是父子之间的传递。
- @Provide:组件级别的共享,可以通过key和@Consume绑定,因此不用参数传递,实现多层级的数据共享,共享范围大于@State。
- LocalStorage:页面级别的共享,可以通过@Entry在当前组件树上共享LocalStorage实例。
- AppStorage:应用全局的UI状态存储,和应用进程绑定,在整个应用内的状态数据的共享。
@State装饰的变量与一个或多个子组件共享状态数据
@State可以初始化多种状态变量,@Prop、@Link和@ObjectLink可以和其建立单向或双向同步。
- 使用Parent根节点中@State装饰的testNum作为ViewModel数据项。将testNum传递给其子组件LinkChild和Sibling。
// xxx.ets
@Entry
@Component
struct Parent {
@State @Watch("testNumChange1") testNum: number = 1;
testNumChange1(propName: string): void {
console.log(`Parent: testNumChange value ${
this.testNum}`)
}
build() {
Column() {
LinkChild({
testNum: $testNum })
Sibling({
testNum: $testNum })
}
}
}
- LinkChild和Sibling中用@Link和父组件的数据源建立双向同步。其中LinkChild中创建了LinkLinkChild和PropLinkChild。
@Component
struct Sibling {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`Sibling: testNumChange value ${
this.testNum}`);
}
build() {
Text(`Sibling: ${
this.testNum}`)
}
}
@Component
struct LinkChild {
@Link @Watch("testNumChange") testNum: number;
testNumChange(propName: string): void {
console.log(`LinkChild: testNumChange value ${
this.testNum}`);
}
build() {
Column() {
Button('incr testNum')
.onClick(() => {
console.log(`LinkChild: before value change value ${
this.testNum}`);
this.testNum = this.testNum + 1
console.log(`LinkChild: after value change value ${
this.testNum}`);
})
Text(`LinkChild: ${
this.testNum}`)
LinkLinkChild({
testNumGrand: $testNum })
PropLinkChild({
testNumGrand: this.testNum })
}
.height(200).width(200)
}
}
- LinkLinkChild和PropLinkChild声明如下,PropLinkChild中的@Prop和其父组件建立单向同步关系。
@Component
struct LinkLinkChild {
@Link @Watch("testNumChange") testNumGrand: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNumGrand value ${
this.testNumGrand}`);
}
build() {
Text(`LinkLinkChild: ${
this.testNumGrand}`)
}
}
@Component
struct PropLinkChild {
@Prop @Watch("testNumChange") testNumGrand: number = 0;
testNumChange(propName: string): void {
console.log(`PropLinkChild: testNumGrand value ${
this.testNumGrand}`);
}
build() {
Text(`PropLinkChild: ${
this.testNumGrand}`)
.height(70)
.backgroundColor(Color.Red)
.onClick(() => {
this.testNumGrand += 1;
})
}
}
当LinkChild中的@Link testNum更改时。
- 更改首先同步到其父组件Parent,然后更改从Parent同步到Sibling。
- LinkChild中的@Link testNum更改也同步给子组件LinkLinkChild和PropLinkChild。
@State装饰器与@Provide、LocalStorage、AppStorage的区别:
- @State如果想要将更改传递给孙子节点,需要先将更改传递给子组件,再从子节点传递给孙子节点。
- 共享只能通过构造函数的参数传递,即命名参数机制CompA: ({ aProp: this.aProp })。
完整的代码示例如下:
@Component
struct LinkLinkChild {
@Link @Watch("testNumChange") testNumGrand: number;
testNumChange(propName: string): void {
console.log(`LinkLinkChild: testNumGrand value ${
this.testNumGrand}`);
}
build() {
Text</