laibao101

laibao101 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

laibao101 关注了专栏 · 10月2日

Jokcy的前端小站

看到,想到,做过的一些内容总结

关注 52

laibao101 赞了文章 · 8月13日

Typescript 设计模式之工厂方法

在现实生活中,工厂是负责生产产品的,比如牛奶、面包或礼物等,这些产品满足了我们日常的生理需求。此外,在日常生活中,我们也离不开大大小小的系统,这些系统是由不同的组件对象构成。

而作为一名 Web 软件开发工程师,在软件系统的设计与开发过程中,我们可以利用设计模式来提高代码的可重用性、可扩展性和可维护性。在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。

工厂模式可以分为三类:

  • 简单工厂模式(Simple Factory Pattern)
  • 工厂方法模式(Factory Method Pattern)
  • 抽象工厂模式(Abstract Factory Pattern)

本文阿宝哥将介绍简单工厂模式与工厂方法模式,而抽象工厂模式将在后续的文章中介绍,下面我们先来介绍简单工厂模式。

一、简单工厂模式

1.1 简单工厂模式简介

简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。

相信对于刚接触简单工厂模式的小伙伴来说,看到以上的描述可能会觉得有点抽象。这里为了让小伙伴更好地理解简单工厂模式,阿宝哥以用户买车为例,来介绍一下 BMW 工厂如何使用简单工厂模式来生产🚗。

在上图中,阿宝哥模拟了用户购车的流程,pingan 和 qhw 分别向 BMW 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。接下来,阿宝哥将介绍如何使用简单工厂来描述 BMW 工厂生产指定型号车子的过程。

1.2 简单工厂模式实战

  1. 定义 BMW 抽象类
abstract class BMW {
  abstract run(): void;
}
  1. 创建 BMW730 类(BMW 730 Model)
class BMW730 extends BMW {
  run(): void {
    console.log("BMW730 发动咯");
  }
}
  1. 创建 BMW840 类(BMW 840 Model)
class BMW840 extends BMW {
  run(): void {
    console.log("BMW840 发动咯");
  }
}
  1. 创建 BMWFactory 工厂类
class BMWFactory {
  public static produceBMW(model: "730" | "840"): BMW {
    if (model === "730") {
      return new BMW730();
    } else {
      return new BMW840();
    }
  }
}
  1. 生产并发动 BMW730 和 BMW840
const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");

bmw730.run();
bmw840.run();

以上代码运行后的输出结果为:

BMW730 发动咯 
BMW840 发动咯 

通过观察以上的输出结果,我们可以知道我们的 BMWFactory 已经可以正常工作了。在 BMWFactory 类中,阿宝哥定义了一个 produceBMW() 方法,该方法会根据传入的模型参数来创建不同型号的车子。

看完简单工厂模式实战的示例,你是不是觉得简单工厂模式还是挺好理解的。那么什么场景下使用简单工厂模式呢?要回答这个问题我们需要来了解一下简单工厂的优缺点。

1.3 简单工厂模式优缺点

1.3.1 优点
  • 将创建实例与使用实例的任务分开,使用者不必关心对象是如何创建的,实现了系统的解耦;
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。
1.3.2 缺点
  • 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,也有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

了解完简单工厂的优缺点,我们来看一下它的应用场景。

1.4 简单工厂模式应用场景

在满足以下条件下可以考虑使用简单工厂模式:

  • 工厂类负责创建的对象比较少:由于创建的对象比较少,不会造成工厂方法中业务逻辑过于复杂。
  • 客户端只需知道传入工厂类静态方法的参数,而不需要关心创建对象的细节。

介绍完简单工厂模式,接下来我们来介绍本文的主角 ”工厂方法模式“

二、工厂方法模式

2.1 工厂方法简介

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

在上图中,阿宝哥模拟了用户购车的流程,pingan 和 qhw 分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。接下来,阿宝哥来介绍如何使用工厂方法来描述 BMW 工厂生产指定型号车子的过程。

2.2 工厂方法实战

  1. 定义 BMW 抽象类
abstract class BMW {
  abstract run(): void;
}
  1. 创建 BMW730 类(BMW 730 Model)
class BMW730 extends BMW {
  run(): void {
    console.log("BMW730 发动咯");
  }
}
  1. 创建 BMW840 类(BMW 840 Model)
class BMW840 extends BMW {
  run(): void {
    console.log("BMW840 发动咯");
  }
}
  1. 定义 BMWFactory 接口
interface BMWFactory {
  produceBMW(): BMW;
}
  1. 创建 BMW730Factory 类
class BMW730Factory implements BMWFactory {
  produceBMW(): BMW {
    return new BMW730();
  }
}
  1. 创建 BMW840Factory 类
class BMW840Factory implements BMWFactory {
  produceBMW(): BMW {
    return new BMW840();
  }
}
  1. 生产并发动 BMW730 和 BMW840
const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();

const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();

bmw730.run();
bmw840.run();

通过观察以上的输出结果,我们可以知道我们的 BMW730Factory 和 BMW840Factory 工厂已经可以正常工作了。相比前面的简单工厂模式,工厂方法模式通过创建不同的工厂来生产不同的产品。下面我们来看一下工厂方法有哪些优缺点。

2.3 工厂方法优缺点

2.3.1 优点
  • 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,更加符合 “开闭原则”。而简单工厂模式需要修改工厂类的判断逻辑。
  • 符合单一职责的原则,即每个具体工厂类只负责创建对应的产品。而简单工厂模式中的工厂类存在一定的逻辑判断。
  • 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
2.3.2 缺点
  • 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  • 一个具体工厂只能创建一种具体产品。

最后我们来简单介绍一下工厂方法的应用场景。

2.4 工厂方法应用场景

  • 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
  • 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

三、参考资源

四、推荐阅读

查看原文

赞 18 收藏 9 评论 0

laibao101 关注了用户 · 2018-09-26

幽_月 @you_yue

关注 58

laibao101 收藏了文章 · 2018-03-12

Koa原理学习路径与设计哲学

Koa原理学习路径与设计哲学

本文基于Koa@2.5.0

Koa简介(废话篇)

Koa是基于Node.jsHTTP框架,由Express原班人马打造。是下一代的HTTP框架,更简洁,更高效。

我们来看一下下载量(2018.3.4)

Koa:471,451 downloads in the last month
Express:18,471,701 downloads in the last month

说好的Koa是下一代框架呢,为什么下载量差别有这么大呢,Express一定会说:你大爷还是你大爷!

确实,好多知名项目还是依赖Express的,比如webpack的dev-server就是使用的Express,所以还是看场景啦,如果你喜欢DIY,喜欢绝对的控制一个框架,那么这个框架就应该什么功能都不提供,只提供一个基础的运行环境,所有的功能由开发者自己实现。

正是由于Koa的高性能和简洁,好多知名项目都在基于Koa,比如阿里的eggjs,360奇舞团的thinkjs

所以,虽然从使用范围上来讲,Express对于Koa你大爷还是你大爷!,但是如果Express很好,为什么还要再造一个Koa呢?接下来我们来了解下Koa到底带给我们了什么,Koa到底做了什么。

如何着手分析Koa

先来看两段demo。

下面是Node官方给的一个HTTP的示例。

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

下面是最简单的一个Koa的官方实例。

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Koa是一个基于Node的框架,那么底层一定也是用了一些Node的API。

jQuery很好用,但是jQuery也是基于DOM,逃不过也会用element.appendChild这样的基础API。Koa也是一样,也是用一些Node的基础API,封装成了更好用的HTTP框架。

那么我们是不是应该看看Koahttp.createServer的代码在哪里,然后顺藤摸瓜,了解整个流程。

Koa核心流程分析

Koa的源码有四个文件

  • application.js // 核心逻辑
  • context.js // 上下文,每次请求都会生成一个
  • request.js // 对原生HTTP的req对象进行包装
  • response.js // 对原生HTTP的res对象进行包装

我们主要关心application.js中的内容,直接搜索http.createServer,会搜到

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

刚好和Koa中的这行代码app.listen(3000);关联起来了。

找到源头,现在我们就可以梳理清楚主流程,大家对着源码看我写的这个流程

fn:listen
∨
fn:callback
∨
[fn:compose] // 组合中间件 会生成后面的 fnMiddleware
∨
fn:handleRequest // (@closure in callback)
∨
[fn(req, res):createContext] // 创建上下文 就是中间件中用的ctx
∨
fn(ctx, fnMiddleware):handleRequest // (@koa instance)
∨
code:fnMiddleware(ctx).then(handleResponse).catch(onerror);
∨
fn:handleResponse
∨
fn:respond
∨
code:res.end(body);

从上面可以看到最开始是listen方法,到最后HTTP的res.end方法。

listen可以理解为初始化的方法,每一个请求到来的时候,都会经过从callbackrespond的生命周期。

在每个请求的生命周期中,做了两件比较核心的事情:

  1. 将多个中间件组合
  2. 创建ctx对象

多个中间件组合后,会先后处理ctx对象,ctx对象中既包含的req,也包含了res,也就是每个中间件的对象都可以处理请求和响应。

这样,一次HTTP请求,接连经过各个中间件的处理,再到返回给客户端,就完成了一次完美的请求。

Koa中的ctx

app.use(async ctx => {
  ctx.body = 'Hello World';
});

上面的代码是一个最简单的中间件,每个中间件的第一个参数都是ctx,下面我们说一下这个ctx是什么。

创建ctx的代码:

  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }

直接上代码,Koa每次请求都会创建这样一个ctx对象,以提供给每个中间件使用。

参数的req, res是Node原生的对象。

下面解释下这三个的含义:

  • context:Koa封装的带有一些和请求与相应相关的方法和属性
  • request:Koa封装的req对象,比如提了供原生没有的host属性。
  • response:Koa封装的res对象,对返回的bodyhook了getter和setter。

其中有几行一堆 xx = xx = xx,这样的代码。

是为了让ctx、request、response,能够互相引用。

举个例子,在中间件里会有这样的等式

ctx.request.ctx === ctx
ctx.response.ctx === ctx

ctx.request.app === ctx.app
ctx.response.app === ctx.app

ctx.req === ctx.response.req
// ...

为什么会有这么奇怪的写法?其实只是为了互相调用方便而已,其实最常用的就是ctx。

打开context.js,会发现里面写了一堆的delegate

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

是为了把大多数的requestresponse中的属性也挂在ctx下,我们为了拿到请求的路径需要ctx.request.path,但是由于代理过path这个属性,ctx.path也是可以的,即ctx.path === ctx.request.path

ctx模块大概就是这样,没有讲的特别细,这块是重点不是难点,大家有兴趣自己看看源码很方便。

一个小tip: 有时候我也会把context.js中最下面的那些delegate当成文档使用,会比直接看文档快一点。

Koa中间件机制

中间件函数的参数解释

  • ctx:上面讲过的在请求进来的时候会创建一个给中间件处理请求和响应的对象,比如读取请求头和设置响应头。
  • next:暂时可以理解为是下一个中间件,实际上是被包装过的下一个中间件。

一个小栗子

我们来看这样的代码:

// 第一个中间件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next();
  console.log('m1.2', ctx.path);
});

// 第二个中间件
app.use(async(ctx, next) => {
  console.log('m2.1', ctx.path);
  ctx.body = 'Koa m2';
  ctx.set('m2', 'm2');
  next();
  debugger
  console.log('m2.2', ctx.path);
});

// 第三个中间件
app.use(async(ctx, next) => {
  console.log('m3.1', ctx.path);
  ctx.body = 'Koa m3';
  ctx.set('m3', 'm3');
  next();
  console.log('m3.2', ctx.path);
});

会输出什么呢?来看下面的输出:

m1.1 /
m2.1 /
m3.1 /
m3.2 /
m2.2 /
m1.2 /

来解释一下上面输出的现象,由于将next理解为是下一个中间件,在第一个中间件执行next的时候,第一个中间件就将执行权限给了第二个中间件,所以m1.1后输出的是m2.1,在之后是m3.1

那么为什么m3.1后面输出的是m3.2呢?第三个中间件之后已经没有中间件了,那么第三个中间件里的next又是什么?

我先偷偷告诉你,最后一个中间件的next是一个立刻resolve的Promise,即return Promise.resolve(),一会再告诉你这是为什么。

所以第三个中间件(即最后一个中间件)可以理解成是这样子的:

app.use(async (ctx, next) => {
    console.log('m3.1', ctx.path);
    ctx.body = 'Koa m3';
    ctx.set('m3', 'm3');
    new Promise.resolve(); // 原来是next
    console.log('m3.2', ctx.path);
});

从代码上看,m3.1后面就会输出m3.2

那为什么m3.2之后又会输出m2.2呢?,我们看下面的代码。

let f1 = () => {
  console.log(1.1);
  f2();
  console.log(1.2);
}

let f2 = () => {
  console.log(2.1);
  f3();
  console.log(2.2);
}

let f3 = () => {
  console.log(3.1);
  Promise.resolve();
  console.log(3.2);
}

f1();

/*
  outpout
  1.1
  2.1
  3.1
  3.2
  2.2
  1.2
*/

这段代码就是纯函数调用而已,从这段代码是不是发现,和上面一毛一样,对一毛一样,如果将next理解成是下一个中间件的意思,就是这样。

中间件组合的过程分析

用户使用中间件就是用app.use这个API,我们看看做了什么:

  // 精简后去掉非核心逻辑的代码
  use(fn) {
    this.middleware.push(fn);
    return this;
  }

可以看到,当我们应用中间件的时候,只是把中间件放到一个数组中,然后返回this,返回this是为了能够实现链式调用。

那么Koa对这个数组做了什么呢?看一下核心代码

const fn = compose(this.middleware); // @callback line1
// fn 即 fnMiddleware 
return fnMiddleware(ctx).then(handleResponse).catch(onerror); // @handleRequest line_last

可以看到用compose处理了middleware数组,得到函数fnMiddleware,然后在handleRequest返回的时候运行fnMiddleware,可以看到fnMiddleware是一个Promiseresolve的时候就会处理完请求,能猜到compose将多个中间件组合成了一个返回Promise的函数,这就是奇妙之处,接下来我们看看吧。

精简后的compose源码

// 精简后去掉非核心逻辑的代码
00    function compose (middleware) {
01      return function (context, next) { // fnMiddleware
02        return dispatch(0)
03        function dispatch (i) {
04          let fn = middleware[i] // app.use的middleware
05          if (!fn) return Promise.resolve()
06          return fn(context, function next () {
07            return dispatch(i + 1)
08          })
09        }
10      }
11    }

精简后代码只有十几行,但是我认为这是Koa最难理解、最核心、最优雅、最奇妙的地方。

看着各种function,各种return有点晕是吧,不慌,不慌啊,一行一行来。

compose返回了一个匿名函数,这个匿名函数就是fnMiddleware

刚才我们是有三个中间件,你们准备好啦,请求已经过来啦!

当请求过来的时候,fnMiddleware就运行了,即运行了componse返回的匿名函数,同时就会运行返回的dispatch(0),那我们看看dispatch(0)做了什么,仔细一看其实就是

// dispatch(0)的时候,fn即middleware[0]
return middleware[0](context, function next (){
  return dispatch(1);
})

// 上面的context和next即中间件的两个参数
// 第一个中间件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next(); // 这个next就是dispatch(1)
  console.log('m1.2', ctx.path);
});

同理,在第二个中间件里面的next,就是dispatch(2),也就是用上面的方法被包裹一层的第三个中间件。

  • 现在来看第三个中间件里面的next是什么?

可以看到精简过的compose05行有个判断,如果fn不存在,会返回Promise.resolve(),第三个中间件的nextdispatch(3),而一共就有三个中间件,所以middleware[3]是undefined,触发了分支判断条件,就返回了Promise.resolve()

再来复盘一下:

  1. 请求到来的事情,运行fnMiddleware(),即会运行dispatch(0)调起第一个中间件。
  2. 第一个中间件的nextdispatch(1),运行next的时候就调起第二个中间件
  3. 第二个中间件的nextdispatch(2),运行next的时候就调起第三个中间件
  4. 第三个中间件的nextdispatch(3),运行next的时候就调起Promise.resolve()。可以把Promise.resolve()理解成一个空的什么都没有干的中间件。

到此,大概知道了多个中间件是如何被compose成一个大中间件的了吧。

中间件的类型

koa2中,支持三种类型的中间件:

  • common function:普通的函数,需要返回一个promise
  • generator function:需要被co包裹一下,就会返回一个promise
  • async function:直接使用,会直接返回promise

可以看到,无论哪种类型的中间件,只要返回一个promise就好了,因为这行关键代码return fnMiddleware(ctx).then(handleResponse).catch(onerror);,可以看到KoafnMiddleware的返回值认为是promise。如果传入的中间件运行后没有返回promise,那么会导致报错。

结语

Koa的原理就解析到这里啦,欢迎交流讨论。
为了更好地让大家学习Koa,我写了一个mini版本的Koa,大家可以看一下 https://github.com/geeknull/t...

查看原文

认证与成就

  • 获得 0 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-09-22
个人主页被 45 人浏览