开闭原则

开闭原则(Open Closed Principle)简称OCP

解释

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

总结

当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

评论和共享

迪米特法则

迪米特法则(Law of Demeter)简称LoD

解释

一个对象应该对其他对象有最少的了解。

总结

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

评论和共享

依赖倒置原则

依赖倒置原则(Dependence Inversion Principle)简称DIP

## 解释
程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

  • 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
  • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

例如:
母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Book{  
public String getContent(){
return "很久很久以前有一个阿拉伯的故事……";
}
}

class Mother{
public void narrate(Book book){
System.out.println("妈妈开始讲故事");
System.out.println(book.getContent());
}
}

public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
}
}

假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:

1
2
3
4
5
class Newspaper{  
public String getContent(){
return "林书豪38+7领导尼克斯击败湖人……";
}
}

这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。
我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

1
2
3
interface IReader{  
public String getContent();
}

Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Newspaper implements IReader {  
public String getContent(){
return "林书豪17+9助尼克斯击败老鹰……";
}
}
class Book implements IReader{
public String getContent(){
return "很久很久以前有一个阿拉伯的故事……";
}
}

class Mother{
public void narrate(IReader reader){
System.out.println("妈妈开始讲故事");
System.out.println(reader.getContent());
}
}

public class Client {
public static void main(String[] args) {
Mother mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
}
}

总结

依赖倒置原则的核心思想是面向接口编程

评论和共享

接口隔离原则

接口隔离原则(Interface Segregation Principle)简称ISP

解释

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
interface I {  
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}

class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
public void depend3(I i){
i.method3();
}
}

class B implements I{
public void method1() {
System.out.println("类B实现接口I的方法1");
}
public void method2() {
System.out.println("类B实现接口I的方法2");
}
public void method3() {
System.out.println("类B实现接口I的方法3");
}
//对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method4() {}
public void method5() {}
}

class C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}

class D implements I{
public void method1() {
System.out.println("类D实现接口I的方法1");
}
//对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method2() {}
public void method3() {}

public void method4() {
System.out.println("类D实现接口I的方法4");
}
public void method5() {
System.out.println("类D实现接口I的方法5");
}
}

public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());

C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
}

可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
interface I1 {  
public void method1();
}

interface I2 {
public void method2();
public void method3();
}

interface I3 {
public void method4();
public void method5();
}

class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}

class B implements I1, I2{
public void method1() {
System.out.println("类B实现接口I1的方法1");
}
public void method2() {
System.out.println("类B实现接口I2的方法2");
}
public void method3() {
System.out.println("类B实现接口I2的方法3");
}
}

class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}

class D implements I1, I3{
public void method1() {
System.out.println("类D实现接口I1的方法1");
}
public void method4() {
System.out.println("类D实现接口I3的方法4");
}
public void method5() {
System.out.println("类D实现接口I3的方法5");
}
}

总结

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

评论和共享

里氏替换原则

里氏替换原则(Liskov Substitution Principle)简称 SRP

解释

所有引用基类的地方必须能透明地使用其子类的对象。

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

总结

子类可以扩展父类的功能,但不能改变父类原有的功能。

评论和共享

单一职责原则

单一职责原则( Single Responsibility Principle )简称 SRP

解释

规定一个类应该只有一个发生变化的原因

比如这个类:

1
2
3
4
5
6
7
8
public interface IPhone {
//拨通电话
public void dial(String phoneNumber);
//通话
public void chat(Object o);
//挂电话
public void hangup();
}

上面这段代码(IPhone),拥有两个职责,dialhangup 是负责协议管理,chat 负责数据传输。其中只要有一个需要改变就会导致实现类发生改变,所以不符合 单一职责原则

应该将这个接口分成两个,让实现类实现这两个接口。

总结

单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类的设计是否优良,但是“职责”和“变化原因”确实不可度量的,因项目而异,因环境而异

在实际使用中应该灵活使用

评论和共享

Hexo 支持 Emoji

Hexo 默认是采用 hexo-renderer-marked ,这个渲染器不支持插件扩展,当然就不行了,还有一个支持插件扩展的是 hexo-renderer-markdown-it ,所以我们可以使用这个渲染引擎来支持 emoji表情,具体实现过程如下:

更换渲染器进入blog跟目录,执行如下命令

1
2
npm un hexo-renderer-marked --save
npm i hexo-renderer-markdown-it --save

安装emoji插件,执行如下命令

1
npm install markdown-it-emoji --save

编辑 _config.yml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
markdown:
render:
html: true
xhtmlOut: false
breaks: true
linkify: true
typographer: true
quotes: '“”‘’'
plugins:
- markdown-it-abbr
- markdown-it-footnote
- markdown-it-ins
- markdown-it-sub
- markdown-it-sup
- markdown-it-emoji # add emoji
anchors:
level: 2
collisionSuffix: 'v'
permalink: true
permalinkClass: header-anchor
permalinkSymbol: ¶

添加emoji表情
先安装emoji

1
2
npm install emoji --save
npm install twemoji --save

编辑node_modules/markdown-it-emoji/index.js文件,最终文件像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use strict';
var emojies_defs = require('./lib/data/full.json');
var emojies_shortcuts = require('./lib/data/shortcuts');
var emoji_html = require('./lib/render');
var emoji_replace = require('./lib/replace');
var normalize_opts = require('./lib/normalize_opts');
var twemoji = require('twemoji') //添加twemoji
module.exports = function emoji_plugin(md, options) {
var defaults = {
defs: emojies_defs,
shortcuts: emojies_shortcuts,
enabled: []
};

var opts = normalize_opts(md.utils.assign({}, defaults, options || {}));

md.renderer.rules.emoji = emoji_html;
//使用 twemoji 渲染
md.renderer.rules.emoji = function(token, idx) {
return twemoji.parse(token[idx].content);
};
md.core.ruler.push('emoji', emoji_replace(md, opts.defs, opts.shortcuts, opts.scanRE, opts.replaceRE));
};

在主题CSS中添加你的CSS代码就行了

1
2
3
.emoji{
width: 20px;
}

评论和共享

An emoji guide for your commit messages: gitmoji

Code Emoji 描述
:art: :art: 改进代码的结构/格式
:zap: :zap: 提高性能
:fire: :fire: 删除代码或文件
:bug: :bug: 修复bug
:ambulance: :ambulance: 关键修补程序
:sparkles: :sparkles: 引入新功能
:memo: :memo: 写文档
:rocket: :rocket: 部署项目
:lipstick: :lipstick: 更新UI和样式文件
:tada: :tada: 初始提交
:white_check_mark: :white_check_mark: 添加测试
:lock: :lock: 解决安全问题
:apple: :apple: 修改 macOS 下的一些问题
:penguin: :penguin: 修改 Linux 下的一些问题
:checkered_flag: :checkered_flag: 修改 Windows 下的一些问题
:robot: :robot: 修改 Android 下的一些问题
:green_apple: :green_apple: 修改 IOS 下的一些问题
:bookmark: :bookmark: 发布版本标签
:rotating_light: :rotating_light: 移除 linter 警告
:construction: :construction: 工作正在进行中
:green_heart: :green_heart: 修复CI构建
:arrow_down: :arrow_down: 降级依赖
:arrow_up: :arrow_up: 更新依赖
:construction_worker: :construction_worker: 添加CI构建系统
:chart_with_upwards_trend: :chart_with_upwards_trend: 添加分析或跟踪代码
:hammer: :hammer: 重构代码
:heavy_minus_sign: :heavy_minus_sign: 删除依赖关系
:whale: :whale: 关于Docker的工作
:heavy_plus_sign: :heavy_plus_sign: 添加依赖关系
:wrench: :wrench: 更改配置文件
:globe_with_meridians: :globe_with_meridians: 国际化和本地化
:pencil2: :pencil2: 修正打字错误
:hankey: :hankey: 编写不好的代码,需要改进
:rewind: :rewind: 还原更改
:twisted_rightwards_arrows: :twisted_rightwards_arrows: 合并分支
:package: :package: 更新已编译的文件或包
:alien: :alien: 由于外部API更改而更新代码
:truck: :truck: 移动或重命名文件
:page_facing_up: :page_facing_up: 添加或更新许可证
:boom: :boom: 引入爆炸改变
:bento: :bento: 添加或更新资源
:ok_hand: :ok_hand: 由于代码审查更改而更新代码
:wheelchair: :wheelchair: 改善无障碍
:bulb: :bulb: 文档化源代码
:beers: :beers: 沉迷写代码
:speech_balloon: :speech_balloon: 更新文本和常量
:card_file_box: :card_file_box: 执行数据库相关更改

评论和共享

这种写法并不能解决问题

1
Access-Control-Allow-Origin: https://www.google.com,https://www.baidu.com

应该写成下面这种

1
2
3
4
5
6
7
8
9
app.all('*', function(req, res, next) {
if( req.headers.origin == 'https://www.google.com' || req.headers.origin == 'https://www.baidu.com' ){
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header('Access-Control-Allow-Methods', 'POST, GET');
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
res.header('Access-Control-Allow-Headers', 'Content-Type');
}
next();
});

如果想不限制域名

1
2
3
4
5
6
7
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header('Access-Control-Allow-Methods', 'POST, GET');
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
});

评论和共享

Fetch API

概况

JavaScript 通过 XMLHttpRequest(XHR) 来执行异步请求,这个方式已经存在了很长一段时间。但它不是最佳API。它在设计上不符合职责分离原则,将输入、输出和用事件来跟踪的状态混杂在一个对象里。而且,基于事件的模型与最近 JavaScript 流行的 Promise 以及基于生成器的异步编程模型不太搭。新的 Fetch API 打算修正上面提到的那些缺陷。

而与jQuery相比, fetch 方法与 jQuery.ajax() 的主要区别在于:

  • fetch() 方法返回的 Promise 对象并不会在HTTP状态码为404或者500的时候自动抛出异常,而需要用户进行手动处理
  • 默认情况下,fetch 并不会发送任何的本地的 cookie 到服务端,注意,如果服务端依靠 Session 进行用户控制的话要默认开启 Cookie

Installation & Polyfill

window.fetch 是基于 XMLHttpRequest 的浏览器的统一的封装,针对老的浏览器可以使用 Github 的这个polypillfetch 基于 ES6Promise ,在旧的浏览器中首先需要引入 Promisepolypill ,可以用 es6-promise:

1
npm install es6-promise

使用 isomorphic-fetch

1
npm install --save isomorphic-fetch es6-promise

使用的时候也非常方便:

1
2
3
4
5
6
7
8
9
10
11
12
require('es6-promise').polyfill();
require('isomorphic-fetch');
fetch('//offline-news-api.herokuapp.com/stories')
.then(function(response) {
if (response.status >= 400) {
throw new Error("Bad response from server");
}
return response.json();
})
.then(function(stories) {
console.log(stories);
});

Usage

HTML

1
2
3
4
5
6
fetch('/users.html')
.then(function(response) {
return response.text()
}).then(function(body) {
document.body.innerHTML = body
})

JSON

1
2
3
4
5
6
7
8
fetch('/users.json')
.then(function(response) {
return response.json()
}).then(function(json) {
console.log('parsed json', json)
}).catch(function(ex) {
console.log('parsing failed', ex)
})

Response metadata

1
2
3
4
5
6
fetch('/users.json').then(function(response) {
console.log(response.headers.get('Content-Type'))
console.log(response.headers.get('Date'))
console.log(response.status)
console.log(response.statusText)
})

Post form

1
2
3
4
5
6
var form = document.querySelector('form')

fetch('/users', {
method: 'POST',
body: new FormData(form)
})

Post JSON

1
2
3
4
5
6
7
8
9
10
fetch('/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Hubot',
login: 'hubot',
})
})

File upload

1
2
3
4
5
6
7
8
9
10
var input = document.querySelector('input[type="file"]')

var data = new FormData()
data.append('file', input.files[0])
data.append('user', 'hubot')

fetch('/avatars', {
method: 'POST',
body: data
})

Request:请求构造

Request对象代表了一次fetch请求中的请求体部分,你可以自定义Request对象:

  • method - 使用的HTTP动词,GET, POST, PUT, DELETE, HEAD
  • url - 请求地址,URL of the request
  • headers - 关联的Header对象
  • referrer - referrer
  • mode - 请求的模式,主要用于跨域设置,cors, no-cors, same-origin
  • credentials - 是否发送Cookie omit, same-origin
  • redirect - 收到重定向请求之后的操作,follow, error, manual
  • integrity - 完整性校验
  • cache - 缓存模式(default, reload, no-cache)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var request = new Request('/users.json', {
    method: 'POST',
    mode: 'cors',
    redirect: 'follow',
    headers: new Headers({
    'Content-Type': 'text/plain'
    })
    });

    //use it
    fetch(request).then(function() { /* handle response */ });
1
2
3
4
5
6
7
8
fetch('/users.json', {
method: 'POST',
mode: 'cors',
redirect: 'follow',
headers: new Headers({
'Content-Type': 'text/plain'
})
}).then(function() { /* handle response */ });

Cookies

如果需要设置fetch自动地发送本地的Cookie,需要将credentials设置为same-origin:

1
2
3
fetch('/users', {
credentials: 'same-origin'
})

该选项会以类似于XMLHttpRequest的方式来处理Cookie,否则,可能因为没有发送Cookie而导致基于Session的认证出错。可以将credentials的值设置为include来在CORS情况下发送请求。

1
2
3
fetch('https://example.com:1234/users', {
credentials: 'include'
})

Headers:自定义请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Create an empty Headers instance
var headers = new Headers();

// Add a few headers
headers.append('Content-Type', 'text/plain');
headers.append('X-My-Custom-Header', 'CustomValue');

// Check, get, and set header values
headers.has('Content-Type'); // true
headers.get('Content-Type'); // "text/plain"
headers.set('Content-Type', 'application/json');

// Delete a header
headers.delete('X-My-Custom-Header');

// Add initial values
var headers = new Headers({
'Content-Type': 'text/plain',
'X-My-Custom-Header': 'CustomValue'
});

使用webpack前后端跨域发送cookie的问题

最简单的方法是服务端将响就头设置成 Access-Control-Allow-Origin:域名,如果客户端发送请求时,不需要携带 cookie 等信息,可以设置成 Access-Control-Allow-Origin:* ,表示任何域都可以向服务端发送请求,客户端不需要任何配置,就可以进行跨域调试了。

但一般网站,都需要向后端发送 cookie来进行身份验证,此时,服务器还需向响应头设置 Access-Control-Allow-Credentials:true,表示跨域时,允许cookie添加到请求中。设置 Access-Control-Allow-Credentials:true 后,要将 Access-Control-Allow-Origin 指定到具体的域,否则cookie不会带到客户端,例如设置成Access-Control-Allow-Origin:http://192.168.0.1:8088,http://192.168.0.1:8088 是前端服务器的域名,这就要求用webpack的时候,要指定具体的域来启动,不要直接用localhost。

1
2
3
4
5
...
devServer: {
host: '192.168.0.1:8088',
},
...

要向后端发送cookie,前端也需要有相应的配置。需要将credentials设置成include,表示允许跨越传递cookie,不要将credentials设置成same-origin,如果设置成same-origin,只会在同源的时候发送cookie。另外还要将 withCredentials 设为true

Response:响应处理

在fetch的then函数中提供了一个Response对象,即代表着对于服务端返回值的封装,你也可以在Mock的时候自定义Response对象,譬如在你需要使用Service Workers的情况下,在Response中,你可以作如下配置:

  • type - basic, cors
  • url
  • useFinalURL - 是否为最终地址
  • status - 状态码 (ex: 200, 404, etc.)
  • ok - 是否成功响应 (status in the range 200-299)
  • statusText - status code (ex: OK)
  • headers - 响应头

The Response also provides the following methods:

  • clone()- Creates a clone of a Response object.
  • error() - Returns a new Response object associated with a network error.
  • redirect() - Creates a new response with a different URL.
  • arrayBuffer() - Returns a promise that resolves with an ArrayBuffer.
  • blob() - Returns a promise that resolves with a Blob.
  • formData() - Returns a promise that resolves with a FormData object.
  • json() - Returns a promise that resolves with a JSON object.
  • text() - Returns a promise that resolves with a USVString (text).

处理HTTP错误状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}

function parseJSON(response) {
return response.json()
}

fetch('/users')
.then(checkStatus)
.then(parseJSON)
.then(function(data) {
console.log('request succeeded with JSON response', data)
}).catch(function(error) {
console.log('request failed', error)
})

处理JSON响应

1
2
3
4
5
6
7
8
9
10
11
12
13
fetch('https://davidwalsh.name/demo/arsenal.json').then(function(response) {

// Convert to JSON

return response.json();

}).then(function(j) {

// Yay, `j` is a JavaScript object

console.log(j);

});

处理文本响应

1
2
3
4
5
6
7
8
9
10
11
12
13
fetch('/next/page')

.then(function(response) {

return response.text();

}).then(function(text) {

// <!DOCTYPE ....

console.log(text);

});

Blob Responses

如果你希望通过fetch方法来载入一些类似于图片等资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fetch('flowers.jpg')

.then(function(response) {

return response.blob();

})

.then(function(imageBlob) {

document.querySelector('img').src = URL.createObjectURL(imageBlob);

});
blob()方法会接入一个响应流并且一直读入到结束。

评论和共享

作者的图片

Archie Shi

Nothing to say


Front-End Development Engineer