登录和授权

登录和授权的区别

  • 登录:身份认证,即确认「你是你」的过程。
  • 授权:由身份或持有的令牌确认享有某些权限(例如获取⽤户信息)。⽽登录过程实质上的⽬的也是为了确认权限。

因此,在实际的应⽤中,多数场景下的「登录」和「授权」界限是模糊的。

Cookie

  • 起源:「购物⻋」功能的需求,由 Netscape 浏览器开发团队打造。

  • ⼯作机制:

    1. 服务器需要客户端保存的内容,放在 Set-Cookie headers ⾥返回,客户端会⾃动保存。

    1. 客户端保存的 Cookies,会在之后的所有请求⾥都携带进 Cookie header ⾥发回给服务器。

    2. 客户端保存 Cookie 是按照服务器域名来分类的,例如 shop.com 发回的 Cookie 保存下来以后,在之后向 games.com 的请求中并不会携带。

    3. 客户端保存的 Cookie 在超时后会被删除、没有设置超时时间的 Cookie(称作 Session Cookie)在浏览器关闭后就会⾃动删除;另外,服务器也可以主动删除还未过期的客户端 Cookies。

  • Cookie 的作⽤

    • 会话管理:登录状态、购物⻋ ( sessionid )
    • 个性化:⽤户偏好、主题
    • Tracking:分析⽤户⾏为
    • XSS (Cross-site scripting) :跨站脚本攻击。即使⽤ JavaScript 拿到浏览器的 Cookie 之后,发送到⾃⼰的⽹站,以这种⽅式来盗取⽤户Cookie。应对⽅式:Server 在发送 Cookie 时,敏感的 Cookie 加上HttpOnly。
      • 应对⽅式:HttpOnly——这个 Cookie 只能⽤于 HTTP 请求,不能被 JavaScript 调⽤。它可以防⽌本地代码滥⽤ Cookie。
    • XSRF (Cross-site request forgery)(了解即可):跨站请求伪造。即在⽤户不知情的情况下访问已经保存了 Cookie 的⽹站,以此来越权操作⽤户账户(例如盗取⽤户资⾦)。
      • 应对⽅式:Referer 校验。

Authorization

Basic

Bearer

  • 格式:Authorization: Bearer

  • bearer token 的获取⽅式:通过 OAuth2 的授权流程。

  • OAuth2 的流程 :

    1. 第三⽅⽹站向授权⽅⽹站申请第三⽅授权合作,拿到 client id 和 client secret

    2. ⽤户在使⽤第三⽅⽹站时,点击「通过 XX (如 GitHub) 授权」按钮,第三⽅⽹站将⻚⾯跳转到授权⽅⽹站,并传⼊ client id 作为⾃⼰的身份标识

    3. 授权⽅⽹站根据 client id ,将第三⽅⽹站的信息和第三⽅⽹站需要的⽤户权限展示给⽤户,并询问⽤户是否同意授权

    4. ⽤户点击「同意授权」按钮后,授权⽅⽹站将⻚⾯跳转回第三⽅⽹站,并传⼊ Authorization code 作为⽤户认可的凭证。

    5. 第三⽅⽹站将 Authorization code 发送回⾃⼰的服务器

    6. 服务器将 Authorization code 和⾃⼰的 client secret ⼀并发送给授权⽅的服务器,授权⽅服务器在验证通过后,返回 access token。OAuth 流程结束。

    7. 在上⾯的过程结束之后,第三⽅⽹站的服务器(或者有时客户端也会)就可以使⽤ access token 作为⽤户授权的令牌,向授权⽅⽹站发送请求来获取⽤户信息或操作⽤户账户。但这已经在 OAuth 流程之外。

  • 为什么 OAuth 要引⼊ Authorization code,并需要申请授权的第三⽅将 Authorization code 发送回⾃⼰的服务器,再从服务器来获取 access token,⽽不是直接返回 access token ?这样复杂的流程意义何在?

    为了安全。OAuth不强制授权流程必须使⽤ HTTPS,因此需要保证当通信路径中存在窃听者时,依然具有⾜够⾼的安全性。

  • 第三⽅ App 通过微信登录的流程(也是⼀个 OAuth2 流程):

    1. 第三⽅ App 向腾讯申请第三⽅授权合作,拿到 client id 和 client secret

    2. ⽤户在使⽤第三⽅ App 时,点击「通过微信登录」,第三⽅ App 将使⽤微信 SDK 跳转到微信,并传⼊⾃⼰的 client id 作为⾃⼰的身份标识

    3. 微信通过和服务器交互,拿到第三⽅ App 的信息,并限制在界⾯中,然后询问⽤户是否同意授权该 App 使⽤微信来登录

    4. ⽤户点击「使⽤微信登录」后,微信和服务器交互将授权信息提交,然后跳转回第三⽅ App,并传⼊ Authorization code 作为⽤户认可的凭证

    5. 第三⽅ App 调⽤⾃⼰服务器的「微信登录」Api,并传⼊ Authorizationcode,然后等待服务器的响应

    6. 服务器在收到登录请求后,拿收到的 Authorization code 去向微信的第三⽅授权接⼝发送请求,将 Authorization code 和⾃⼰的 client secret ⼀起作为参数发送,微信在验证通过后,返回 access token

    7. 服务器在收到 access token 后,⽴即拿着 access token 去向微信的⽤户信息接⼝发送请求,微信验证通过后,返回⽤户信息

    8. 服务器在收到⽤户信息后,在⾃⼰的数据库中为⽤户创建⼀个账户,并使⽤从微信服务器拿来的⽤户信息填⼊⾃⼰的数据库,以及将⽤户的 ID 和⽤户\的微信 ID 做关联

    9. ⽤户创建完成后,服务器向客户端的请求发送响应,传送回刚创建好的⽤户信息

    10. 客户端收到服务器响应,⽤户登录成功

  • 在⾃家 App 中使⽤ Bearer token

    有的 App 会在 Api 的设计中,将登录和授权设计成类似 OAuth2 的过程,但简化掉 Authorization code 概念。即:登录接⼝请求成功时,会返回 access token,然后客户端在之后的请求中,就可以使⽤这个 access token 来当做 bearer token 进⾏⽤户操作了。

  • Refresh token

    • ⽤法:access token 有失效时间,在它失效后,调⽤ refresh token 接⼝,传⼊refresh_token 来获取新的 access token。
    • ⽬的:安全。当 access token 失窃,由于它有失效时间,因此坏⼈只有较短的时间来「做坏事」;同时,由于(在标准的 OAuth2 流程中)refresh token 永远只存在与第三⽅服务的服务器中,因此 refresh token ⼏乎没有失窃的⻛险。

加密、Hash、序列化和字符集

古典密码学

  • 起源 :古代战争

  • 移位式加密 :密码帮

    • 加密算法 :缠绕木棒后书写
    • 密钥 :木棒的尺寸规格
  • 替换式加密
    按规则使⽤不同的⽂字来替换掉原先的⽂字来进⾏加密。

    • 加密算法 :替换文字
    • 密钥 :替换的码表
1
2
3
4
5
6
码表:
原始字符:ABCDEFGHIJKLMNOPQRSTUVWXYZ
密码字符:BCDEFGHIJKLMNOPQRSTUVWXYZA
原始书信:I love you
加密书信:J mpwf zpv
解读后:I love you

现在密码学

  • 不⽌可以⽤于⽂字内容,还可以⽤于各种⼆进制数据
  • ⾮对称加密的出现使得密码学有了更⼴泛的⽤途:数字签名

对称加密

  • 原理: 通信双⽅使⽤同⼀个密钥,使⽤加密算法配合上密钥来加密,解密时使⽤加密过程的完全逆过程配合密钥来进⾏解密。

  • 经典算法 :DES(56 位密钥,密钥太短⽽逐渐被弃⽤)、AES(128 位、192 位、256 位密钥,现在最流⾏)

  • 缺点 :密钥泄露:不能在不安全⽹络上传输密钥,⼀旦密钥泄露则加密通信失败。

非对称加密

  • 原理:使⽤公钥对数据进⾏加密得到密⽂;使⽤私钥对数据进⾏解密得到原数据。
  • 延伸⽤途:数字签名。
  • 经典算法:RSA(可⽤于加密和签名)、DSA(仅⽤于签名,但速度更快)
  • 优缺点
    • 优点:可以在不安全⽹络上传输密钥
    • 缺点:计算复杂,因此性能相⽐对称加密差很多

数字签名

密码学密钥和登录密码

  • 密钥(key)
    • 场景:⽤于加密和解密
    • ⽬的:保证数据被盗时不会被⼈读懂内容
    • 焦点:数据
  • 登录密码(password)
    • 场景:⽤户进⼊⽹站或游戏前的身份验证
    • ⽬的:数据提供⽅或应⽤服务⽅对账户拥有者数据的保护,保证「你是你」的时候才提供权限
    • 焦点:身份

Base64

将⼆进制数据转换成由 64 个字符组成的字符串的编码算法

  • 什么是⼆进制数据?

    • ⼴义:所有计算机数据都是⼆进制数据
    • 狭义:⾮⽂本数据即⼆进制数据
  • 算法

将原数据每 6 位对应成 Base 64 索引表中的⼀个字符编排成⼀个字符串(每个字符 8 位)。

  • Base64 索引表

  • 编码示例

    • 把「Man」进⾏ Base64 编码
    • Base64 的末尾补⾜
  • ⽤途

    1. 将⼆进制数据扩充了储存和传输途径(例如可以把数据保存到⽂本⽂件、可以通过聊天对话框或短信形式发送⼆进制数据、可以在 URL 中加⼊简单的⼆进制数据)
    2. 普通的字符串在经过 Base64 编码后的结果会变得⾁眼不可读,因此可以适⽤于⼀定条件下的防偷窥(较少⽤)
  • 缺点
    因为⾃身的原理(6 位变 8 位),因此每次 Base64 编码之后,数据都会增⼤约 1/3,所以会影响存储和传输性能。

「Base64 加密图⽚传输更安全和⾼效」???

不。⾸先,Base64 并不是加密;另外,Base64 会导致数据增⼤ 1/3,降低⽹络性能,增⼤⽤户流量开销,是画蛇添⾜的⼿段。(最好不要拿来diss 你们公司的后端哟,友善)
Base64 对图⽚进⾏编码的⽤于在于,有时需要使⽤⽂本形式来传输图⽚。除此之外,完全没必要使⽤ Base64 对图⽚进⾏额外处理。

变种:Base58 (存储比特币地址)

⽐特币使⽤的编码⽅式,去掉了 Base64 中的数字 “0”,字⺟⼤写 “O”,字⺟⼤写 “I”,和字⺟⼩写"l",以及 “+” 和 “/” 符号,⽤于⽐特币地址的表示。

Base58 对于 Base64 的改动,主要⽬的在于⽤户的便捷性。由于去掉了难以区分的字符,使得Base58 对于「⼈⼯抄写」更加⽅便。另外,去掉了 “+” “/” 号后也让⼤多数的软件可以⽅便双击选取。

URL 使⽤的百分号编码

在 URL 的字符串中,对⼀些不⽤于特殊⽤途的保留字符,使⽤百分号 “%” 为前缀进⾏单独编码,以避免出现解析错误。

⽬的:消除歧义,避免解析错误

例如,要在 http://hencoder.com/users 后⾯添加查询字符串,查询 name 为「隐匿&伟⼤」的⽤户,如果直接写成 http://hencoder.com/user/?name=隐匿&伟⼤ ,"&" 符号就会被解析为分隔符号,因此需要对它进⾏转码,转码后的 URL 为 http://hencoder.com/user/?name=隐匿&伟⼤

这种编码仅⽤于 URL,⽬的是避免解析错误的出现。

压缩与解压缩

含义

  • 压缩:将数据使⽤更具有存储优势的编码算法进⾏编码。(把数据换⼀种⽅式来存储,以减⼩存储空间)
  • 解压缩:将压缩数据解码还原成原来的形式,以⽅便使⽤。(把压缩后的数据还原成原先的形式,以便使⽤)

⽬的

减⼩数据占⽤的存储空间。

粗暴算法举例

将下⾯⽂本内容压缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

使⽤某种算法压缩后的数据为:

1
compress:a:1062;b:105
  • 注:具体的压缩场景有很多,因此压缩算法也会复杂得多,上⾯只是⼀个原型算法。

压缩是编码吗?

是。所谓编码,即把数据从⼀种形式转换为另⼀种形式。压缩过程属于编码过程,
解压缩过程属于解码过程。

常⻅压缩算法

DEFLATE、JPEG、MP3 等

图⽚与⾳频、视频编解码

含义

将图像、⾳频、视频数据通过编码来转换成存档形式(编码),以及从存档形式转换回来(解码)。

图⽚的编码:把图像数据写成 JPG、PNG 等⽂件的编码格式。
图⽚的解码:把 JPG、PNG 等⽂件中的数据解析成标准的图像数据。
⾳频与视频的编码与上⾯的图⽚编码同理。

⽬的

存储和压缩媒体数据(⼤多数媒体编码算法会压缩数据,但不是全部)。

图⽚压缩粗暴算法举例

⼀张纯⽩(⽩⾊的 16 进制数值为 0xffffff)的 64x64 不透明像素图⽚,原数据格式⼤致为:

1
2
3
4
5
6
7
8
9
10
11
12
width:64;height:64;ffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.......f
fffff

使⽤某种算法压缩后的数据为:

1
width:64;height:64;ffffff:[0,0]-[63,63]
  • 具体的压缩场景有很多,因此压缩算法也会复杂得多,上⾯只是⼀个原型算法。

序列化

把数据对象(⼀般是内存中的,例如 JVM 中的对象)转换成字节序列的过程。对象在程序内存⾥的存放形式是散乱的(存放在不同的内存区域、并且由引⽤进⾏连接),通过序列化可以把内存中的对象转换成⼀个字节序列,从⽽使⽤ byte[] 等形式进⾏本地存储或⽹络传输,在需要的时候重新组装(反序列化)来使⽤。

⽬的

让内存中的对象可以被储存和传输。

序列化是编码吗?和编码的区别 ?

不是 ,编码是把数据由⼀种数据格式转换成另⼀种数据格式;⽽序列化是把数据由内存中的对象(⽽不是某种具体的格式)转换成字节序列。

Hash

定义

把任意数据转换成指定⼤⼩范围(通常很⼩,例如 256 字节以内)的数据。

经典算法

MD5、SHA1、SHA256 等

作⽤

相当于从数据中提出摘要信息,因此最主要⽤途是数字指纹。

Hash 的实际⽤途:唯⼀性验证

例如 Java 中的 hashCode() ⽅法。

  • 怎么重写 hashCode ⽅法?
    把 equals() ⽅法中的每个⽤于判断相等的变量都放进 hashCode() 中,⼀起⽣成⼀个尽量不会碰撞的整数即可

  • 为什么每次重写 equals() ⽅法都需要?因为你要把新的判断条件放进 hashCode() 啊

Hash 的实际⽤途:数据完整性验证

从⽹络上下载⽂件后,通过⽐对⽂件的 Hash 值(例如 MD5、SHA1),可以确认下载的⽂件是否有损坏。如果下载的⽂件 Hash 值和⽂件提供⽅给出的 Hash 值⼀致,则证明下载的⽂件是完好⽆损的。

Hash 的实际⽤途:快速查找

HashMap

Hash 的实际⽤途:隐私保护

当重要数据必须暴露的时候,有事可以选择暴露它的 Hash 值(例如 MD5),以保障原数据的安全。
例如 ⽹站登录时,可以只保存⽤户密码的 Hash 值,在每次登录验证时只需要将输⼊的密码的 Hash值和数据库中保存的 Hash 值作⽐对就好,⽹站⽆需知道⽤户的密码。这样,当⽹站数据失窃时,⽤户不会因为⾃⼰的密码被盗导致其他⽹站的安全也受到威胁。

  • 注意:这不属于加密。

Hash 是编码吗?

不是。 Hash 是单向过程,往往是不可逆的,⽆法进⾏逆向恢复操作,因此 Hash 不属于编码。

Hash 是加密吗?

不是。Hash 是单向过程,⽆法进⾏逆向回复操作,因此 Hash 不属于加密。(记住,MD5 不是加密!

字符集

  • 含义 :⼀个由整数向现实世界中的⽂字符号的 Map
  • 分⽀:
    • ASCII:128 个字符,1 字节
    • ISO-8859-1:对 ASCII 进⾏扩充,1 字节
    • Unicode:13 万个字符,多字节
      • UTF-8:Unicode 的编码分⽀
      • UTF-16 :Unicode 的编码分⽀
    • GBK / GB2312 / GB18030:中国⾃研标准,多字节,字符集 + 编码

开闭原则

软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的

  • 对扩展开放。模块对扩展开放,就意味着需求变化时,可以对模块扩展,使其具有满足那些改变的新行为。换句话说,模块通过扩展的方式去应对需求的变化。
  • 对修改关闭。模块对修改关闭,表示当需求变化时,关闭对模块源代码的修改,当然这里的“关闭”应该是尽可能不修改的意思,也就是说,应该尽量在不修改源代码的基础上面扩展组件。

优势

  1. 保持软件产品的稳定性
  2. 不影响原有测试代码的运行
  3. 使代码更具模块化,易于维护
  4. 提高开发效率

例子

  • 假设我们使用出售电脑为例,首选定义一个顶层接口Computer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 电脑信息
*/
interface Computer {
/**
* 价格
*/
fun getPrice(): Double

/**
* 颜色
*/
fun getColor(): String

/**
* 内存
*/
fun getMemory(): Int

/**
* 尺寸
*/
fun getSize(): Float
}
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
/**
* 华硕电脑
*/
class AsusComputer(
private var price: Double,
private var color: String,
private var memory: Int,
private var size: Float,
) : Computer {
override fun getComputerPrice(): Double = price

override fun getComputerColor(): String = color

override fun getComputerMemory(): Int = memory

override fun getComputerSize(): Float = size
}
/**
* 联想电脑
*/
class LenovoComputer(
private var price: Double,
private var color: String,
private var memory: Int,
private var size: Float,
) : Computer {
override fun getComputerPrice(): Double = price

override fun getComputerColor(): String = color

override fun getComputerMemory(): Int = memory

override fun getComputerSize(): Float = size
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main(args: Array<String>) {
val computer: Computer = AsusComputer(4888.88, "深蓝", 8, 14.0f)
println(
"电脑:${
if (computer is AsusComputer) {
"华硕"
} else if (computer is LenovoComputer) {
"联想"
} else {
"未知"
}
}\n售价:${computer.getComputerPrice()}\n颜色:${computer.getComputerColor()}\n内存:${computer.getComputerMemory()}\n尺寸:${computer.getComputerSize()}"
)
}

这是我们一开始的需求,但是随着软件发布运行,我们需求不可能一成不变,肯定要接轨市场。假设现在是双十一,需要搞促销活动。那么我们的代码肯定要添加新的功能。

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
/**
* 华硕电脑 折扣
*/
class AsusDiscountComputer(
/**
* 折扣
*/
private var discount: Float,
price: Double,
color: String,
memory: Int,
size: Float,
) : AsusComputer(price, color, memory, size) {
/**
* 折扣价
*/
fun getDiscountPrice(): Double = discount * getComputerPrice()
}

/**
* 华硕电脑
*/
open class AsusComputer(
private var price: Double,
private var color: String,
private var memory: Int,
private var size: Float,
) : Computer {
override fun getComputerPrice(): Double = price

override fun getComputerColor(): String = color

override fun getComputerMemory(): Int = memory

override fun getComputerSize(): Float = size
}

1
2
3
4
5
fun main(args: Array<String>) {
val computer = AsusDiscountComputer(0.7f, 4888.88, "深蓝", 8, 14.0f)
println("电脑:华硕\n原价:${computer.getComputerPrice()}\n售价:${computer.getDiscountPrice()}\n颜色:${computer.getComputerColor()}\n内存:${computer.getComputerMemory()}\n尺寸:${computer.getComputerSize()}"
)
}

迪米特法则

如何理解“迪米特法则”?

不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

如何理解“高内聚、松耦合”?

“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。

  • 高内聚 指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。

  • 松耦合 在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。

例子

用代码实现 我们看书的操作:唤醒手机,打开阅读软件,选择书籍,然后阅读。总共 3 个步骤,涉及了 3 样东西:手机、软件、书籍。

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
fun main(args: Array<String>) {
Phone().readBook()
}

class Phone {
private var app = App()

/**
* 读书
*/
fun readBook() {
app.read()
}
}

class App {
private var book: Book = Book("设计模式之美")

/**
* 读书
*/
fun read() {
println("读书 : ${book.title}")
}
}

class Book(var title: String)

接口隔离原则

  1. 客户端不应该依赖它不需要的接口

  2. 类间的依赖关系应该建立在最小的接口上

优点

  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  3. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  4. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码
  5. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。

例子

不满足接口隔离原则。

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
interface IWorker {
fun work()
fun eat()
}

class Worker : IWorker {
override fun work() {
println("Worker work 方法")
}

override fun eat() {
println("Worker eat 方法")
}
}

class SuperWorker : IWorker {
override fun work() {
println("SuperWorker work 方法")
}
override fun eat() {
}
}

class Manager {
var worker: IWorker? = null

fun work() {
worker?.work()
}
}

使用 接口隔离原则 进行拆分

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
interface IWorkable {
fun work()
}

interface IFeedable {
fun eat()
}

class Worker : IWorkable, IFeedable {
override fun work() {
println("Worker work 方法")
}

override fun eat() {
println("Worker eat 方法")
}
}

class SuperWorker : IWorkable {
override fun work() {
println("SuperWorker work 方法")
}
}

class Manager {
var worker: IWorkable? = null

fun work() {
worker?.work()
}
}

总结

  1. 接口应该尽量小,但是要有限度。
  2. 接口只暴露给依赖接口的类(调用端)需要的方法,而不需要暴露不需要的方法。
  3. 提高模块的内聚,减少它们之间的耦合。