MVC架构设计,从 JavaScript看为什么选择TypeScript

132 阅读5分钟

MVC 是什么?

MVC,这个架构将程式分成三个部分,分别是Model (模型)View (视图) 以及Controller (控制器) ,每个部分所负责的事情都不同,主要是希望透过这种 职责分离 的方式来管理整个应用并尽可能将相同的程式码片段抽离成共用的单元。

image.png

假设整个应用程式是一间大餐厅,有数名员工,他们什么都做,从厨房大小事到点餐通通都要做,没有一个明确的职责分工,这样的制度很难管理员工,也很难厘清责任,比如说:今天生意非常好,员工们手忙脚乱,外场擦桌子后跑去支援厨房、煮菜煮到一半要去支援点餐等,此时有一位客人点了 A 套餐,送上的却是 C 套餐,在百忙之中还要去找出该位客人是哪张单?刚刚服务的员工是谁?这些都还不是最糟的,如果餐厅的菜单要进行变更,每个员工都必须了解新增的餐点如何制作、要如何介绍餐点给客人,听起来工程十分浩大,不仅用餐体验不佳,效率还很差。

同样的道理,如果我们把程式码的所有逻辑都写在一起,不依照职责来分离程式码,这样对程式码的管理与维护是十分困难的,中间出了差错还必须慢慢找出 Bug 在哪里,更怕碰上需求的变更,惨一点就是要大改,那是开发人员恶梦的开端

导入 MVC 架构的假设

我们再假设整个应用程式是一间大餐厅,有数名员工,但员工有各自的职责,简单划分成内场外场,内场负责备料、依需求做餐点,外场则采分区的方式替客户点餐、送单、上菜等,有了职责分工之后,大家都可以清楚知道自己该做好什么事情,员工管理变得容易且较好厘清责任,比如说:有一位客人位于 A 区,点 A 套餐送上的却是 C 套餐,此时可以很简单的从 A 区服务生找出问题点,究竟是点餐时有点错?还是内场做错?就算餐厅的菜单要进行变更,内场负责学习如何制作新的餐点、外场负责学习如何介绍餐点等,这样的制度让餐厅的用餐体验大幅提升,效率也变好了许多。 我们可以把角色对应到 MVC 架构图中:

image.png

  • 客人即 User,为需求提供者
  • 内场即 Model,负责从 Controller 接收需求,并按需求处理资料,将处理好的资料回应给 Controller
  • 餐点即 View,与 Controller 互动,需求被回应后展现对应的资讯
  • 外场即 Controller,负责接收需求,与 Model 互动后回应需求到 View

可以看出各个角色之间都有自己的责任与范围,就算遇到需求变更,也只需要调整该变更的责任分区即可,不需要牵一发而动全身。

小结

  • Model 负责处理资料
  • Controller 负责连接 View 与 Model 之间的互动
  • View 则是让 User 可以操作系统以及让 User 可以看到对应的资讯

TypeScript 解决几个 JavaScript 的痛点?

型别系统

JavaScript 是一个弱型别语言,而 TypeScript 可以说是强型别的 JavaScript

const sum = (...args) => args.reduce((a, c) => a + c);
//都适用这个函数
console.log(sum(1, 2, 3));
console.log(sum(1, 2, '3'));

此时很难判断这个函式究竟是用来加总数字?还是用来字串相加?这两种结果截然不同,却都适用于此函式,试想今天是一个大型专案,一个函式多种用法是否很容易产生问题呢?更具体一点的举例:小明都用 sum(...args) 来处理加总数字、小华都用 sum(...args) 处理字串相加、阿呆都看心情用,这时候可能会有 理解上的误差,当问题产生的时候,就会很难找出错误。

那么 TypeScript 如何解决这个问题呢?就是替变数加上型别定义,此时,这个函式只接受数字型别的参数,可以解决理解上的误差:

const sum = (...args: number[]) => args.reduce((a, c) => a + c);

console.log(sum(1, 2, 3, 4, 5));
// 会显示错误并编译不过
console.log(sum(1, 2, 3, 4, '5'));

无法辨识

试想今天有一个函式是要处理个人资料用的,所以把个资传入函式进行处理:

/**
 * @param {{ name: string, email: string, phone: string }} info - profile.
 */
const responseBasicInfo = info => {
// info 里面有什么属性???
  const { name, email, phone } = info;
  console.log(name, email, phone);
};

const profile = {
  name: 'HAO',
  email: 'test@test.com',
  phone: '0987654321'
};

responseBasicInfo(profile);

如上方范例所示,我们无法在第一时间知道这个参数里面到底有什么属性,造成 无法辨识 的问题,这时候只好去看资料库回传的资料格式长什么样子然后写 JSDoc...

如果用 TypeScript 的话,可以事先定义好资料的 model,并定义参数的型别为该 model:

class ProfileModel {
  public name: string;
  public email: string;
  public phone: string;

  constructor(data) {
    this.name = data.name;
    this.email = data.email;
    this.phone = data.phone;
  }
}

const responseBasicInfo = (info: ProfileModel) => {
  const { name, email, phone } = info;
  console.log(name, email, phone);
};

const profile: ProfileModel = {
  name: 'HAO',
  email: 'test@test.com',
  phone: '0987654321'
};

responseBasicInfo(profile);

公有与私有

在过去,JavaScript 的世界里是没有公有与私有概念的,需要 用闭包手段来实作出私有概念:

class Pig {
  constructor(weight) {
    let _weight = weight;
    this.eat = () => {
      _weight++;
      console.log('Oink!');
    }
  }
}
此时害羞的猪体重只有自己知道:

const shyPig = new Pig(130);
console.log(shyPig._weight); // undefined

TypeScript 能解!

TypeScript 可以像 Java 等语言一样直接宣告为publicprivate以及protected,直观且存取范围分明:

class Pig {
  private weight: number;

  constructor(weight) {
    this.weight = weight;
  }

  public eat() {
    this.weight++;
    console.log('Oink!');
  }

}

const shyPig = new Pig(130);
console.log(shyPig.weight);

小结

在开发大型系统的时候,不论是理解上的落差、无法辨识型别或是存取权,都很容易导致系统产生问题,因此,我会采用 TypeScript 而不是 JavaScript 。下一篇将会带着大家进入 Express 的世界,敬请期待!