依赖倒置原则

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象(模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的)

  2. 抽象不应该依赖细节(接口或抽象类不依赖于实现类), 细节应该依赖抽象(实现类依赖接口或抽象类)

优势

  1. 减少类间的耦合性,提高系统的稳定性
  2. 降低并行开发引起的风险
  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>) {
Client().onMotherNarrate()
}

class Book {
//故事内容
fun getContent(): String = "很久很久以前有一个阿拉伯的故事……"
}

class Mother {
/**
* 讲故事
*/
fun narrate(book: Book) {
println("妈妈开始讲故事")
println(book.getContent())
}
}

class Client {
fun onMotherNarrate() {
println("孩子要听故事")
val mother = Mother()
mother.narrate(Book())
}
}

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

1
2
3
4
5
6

class Newspaper{
//报纸内容
fun getContent(): String ="商务部相关负责人在接受本台记者采访时表示……"
}

只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。

我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

1
2
3
4
5
6
7
8
9
/**
* 读物接口
*/
interface IReader {
/**
* 读物内容
*/
fun getContent(): String
}

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

1
2
3
4
class Newspaper:IReader {
//报纸内容
override fun getContent(): String = "商务部相关负责人在接受本台记者采访时表示,目前,……"
}
1
2
3
4
class Book:IReader {
//故事内容
override fun getContent(): String = "很久很久以前有一个阿拉伯的故事……"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Mother {
/**
* 讲故事
*/
fun narrate(reader:IReader) {
println("妈妈开始讲${
if(reader is Book){
"故事"
}else{
"报纸"
}
}")
println(reader.getContent())
}
}

1
2
3
4
5
6
7
8
9
class Client {
fun onMotherNarrate() {
val mother = Mother()
println("孩子要听故事")
mother.narrate(Book())
println("孩子要听报纸")
mother.narrate(Newspaper())
}
}

这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

开发注意

  1. 低层模块尽量都要有抽象类或接口,或者两者都有。
  2. 变量的声明类型尽量是抽象类或接口。
  3. 使用继承时遵循里氏替换原则。

里氏替换原则

  1. 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。

  2. 所有引用基类的地方必须能透明地使用其子类的对象 (只要有父类出现的地方,都可以用子类来替代)

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

优势

  1. 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  2. 提高代码的重用性;
  3. 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
  4. 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
  5. 提高产品或项目的开放性。

缺点

  1. 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  2. 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
  3. 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果————大段的代码需要重构。

例子

  • 使用了里氏替换原则,将更基础的方法funcBase()提取出来,然后将A类和B类继承这个更基础的Base类,采用依赖、聚合或耦合的方式来减少父类和子类的耦合
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

fun main(args: Array<String>) {
val a = A()
println("1 - 8 = ${a.func1(1,8)}")
println("----------------------------------------")
val b = B()
println("2 + 8 = ${b.func1(2,8)}")
println("1 + 8 + 9 = ${b.func2(1,8)}")
println("3 - 8 = ${b.func3(3,8)}")
println("----------------------------------------")

}
open class Base {
fun funcBase(num1: Int, num2: Int): Int {
//两个数的积
return num1 * num2
}
}

class A : Base() {
fun func1(num1: Int, num2: Int): Int {
// 两个数的差
return num1 - num2
}
}

class B : Base() {
private val a: A = A()
fun func1(num1: Int, num2: Int): Int {
//两个数相加
return num1 + num2
}

fun func2(num1: Int, num2: Int): Int {
//两个数相加,然后和 9求和
return func1(num1, num2) + 9
}

fun func3(num1: Int, num2: Int): Int {
//使用A的方法
return this.a.func1(num1, num2)
}
}

单一职责原则

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

如何判断类的职责是否足够单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

  • 类中的代码行数、函数或者属性过多;
  • 类依赖的其他类过多,或者依赖类的其他类过多;
  • 私有方法过多;
  • 比较难给类起一个合适的名字;
  • 类中大量的方法都是集中操作类中的某几个属性。

优势

  1. 类的复杂性降低,实现什么职责都有清晰明确的定义
  2. 可读性提高,复杂性降低,那当然可读性提高了
  3. 可维护性提高,可读性提高,那当然更容易维护了
  4. 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助

总结

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

例子

  • 假设我们要做一个用户修改名字以及修改密码的功能
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
class UserModifyImpl : UserModify {
override fun modifyUserPassword(user: User, password: String) {
user.password = password
}

override fun modifyUserName(user: User, name: String) {
user.name = name
}

}

interface UserModify {
/**
* 修改用户密码
*/
fun modifyUserPassword(user: User, password: String)

/**
* 修改用户名称
*/
fun modifyUserName(user: User, name: String)
}

data class User(
var password: String,
var name: String,
)
  • 假设我们让小明去倒垃圾,小红去买菜,小红回来后再叫小红去洗碗
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
class XiaoMing : TakeOutTheTrash {
override fun doPourGarbage() {
println("小明 倒垃圾")
}
}

class XiaoHong : GroceryShopping, WashingUp {
override fun doShopping() {
println("小红 买菜")
}

override fun doWashingUp() {
println("小红 洗碗")
}
}

interface GroceryShopping {
/**
* 买菜
*/
fun doShopping()
}

interface TakeOutTheTrash {
/**
* 倒垃圾
*/
fun doPourGarbage()
}

interface WashingUp {
/**
* 洗碗
*/
fun doWashingUp()
}
  • 我们要实现一个用户注册、登录、注销操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 用户注册
*/
class UserRegister{
fun doRegister(user: User){}
}
/**
* 用户登录
*/
class UserLogin{
fun doLogin(user: User){}
}

/**
* 用户注销登录
*/
class UserLogout{
fun doLogout(user: User){}
}

data class User(
var password: String,
var name: String,
)

kotlin学习 - Kotlin 真泛型

  • Gson封装
1
2
3
4
5
6
7
8
9
10
11
/**
* 为 Gson 增加一个扩展方法
* 由于是 真泛型,因此必须是内联函数
*/
inline fun <reified T> Gson.fromJson(json: String): T {
/**
* 注意这里,可以直接获取 对应 T 的字节码
*/
return fromJson(json, T::class.java)
}

  • MVP 封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

class View<T>(val clazz: Class<T>) {
val presenter by lazy { clazz.newInstance() }

//伴生对象会在类放入类加载器中时执行。在类构造方法执行前。
companion object {
//重载构造函数
inline operator fun <reified T> invoke() = View(T::class.java)
}
}

class Presenter {
override fun toString(): String {
return "presenter"
}
}

fun main(args: Array<String>) {
val a = View.invoke<Presenter>().presenter
println(a)
val b = View<Presenter>().presenter
println(b)
}

kotlin学习 - Kotlin inline noinline crossinline

  • inline: 声明在编译时,将函数的代码拷贝到调用的地方(内联)
  • oninline: 声明 inline 函数的形参中,不希望内联的 lambda
  • crossinline: 表明 inline 函数的形参中的 lambda 不能有 return

inline 内联

使用 inline 声明的函数,在编译时将会拷贝到调用的地方。如果一个函数是 inline 的,那么编译器会在编译的时候,把这个函数复制到调用处。

优势:

  1. 减少函数调用的次数。虽然函数调用的开销很小,但是确实是有一定的开销的。尤其是在大量的循环中,这种开销会变得更加明显。

  2. 减少对象的生成。当方法中,有一个参数是 lambda 的时候,使用 inline 的方法,可以减少对象的生成。

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
//Kotlin

fun main(args: Array<String>) {
for (i in 0..10) {
sum(1, 2) { println("Result is: $it") }
}
}

inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int {
val r = a + b
lambda.invoke(r)
return r
}

//反编译为 Java

public static final void main(@NotNull String[] args) {
//...
int var1 = 0;

for(byte var2 = 10; var1 <= var2; ++var1) {
byte a$iv = 1;
int b$iv = 2;
int r$iv = a$iv + b$iv;
String var9 = "Result is: " + r$iv;
System.out.println(var9);
}
}

noinline

noinline 修饰的是 inline 方法中的 lambda 参数。当一个 inline 函数中,有多个 lambda 作为参数时,可以在不想内联的 lambda 前使用 noinline 声明。

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
//Kotlin

inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit, noinline lambda2: (result: Int) -> Unit): Int {
val r = a + b
lambda.invoke(r)
lambda2.invoke(r)
return r
}

fun main(args: Array<String>) {
sum(1, 2,
{ println("Result is: $it") },
{ println("Invoke lambda2: $it") }
)
}

//反编译为 Java

public static final int sum(int a, int b, @NotNull Function1 lambda, @NotNull Function1 lambda2) {
int r = a + b;
lambda.invoke(r);
lambda2.invoke(r);
return r;
}

public static final void main(@NotNull String[] args) {
byte a$iv = 1;
byte b$iv = 2;
Function1 lambda2$iv = (Function1)null.INSTANCE;
int r$iv = a$iv + b$iv;
String var8 = "Result is: " + r$iv;
System.out.println(var8);
lambda2$iv.invoke(r$iv);
}

crossinline

声明一个 lambda 不能有 return 语句(可以有 return@label 语句)。这样可以避免使用 inline 时,lambda 中的 return 影响程序流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
inline fun sum(a: Int, b: Int, crossinline lambda: (result: Int) -> Unit): Int {
val r = a + b
lambda.invoke(r)
return r
}

fun main(args: Array<String>) {
sum(1, 2) {
println("Result is: $it")
return // 编译错误: return is not allowed here
}
}

总结

  • 使用 inline,内联函数到调用的地方,能减少函数调用造成的额外开销,在循环中尤其有效
  • 使用 inline 能避免函数的 lambda 形参额外创建 Function 对象
  • 使用 noinline 可以拒绝形参 lambda 内联
  • 使用 crossinline 显示声明 inline 函数的形参 lambda 不能有 return 语句,避免lambda 中的 return 影响外部程序流程