设计模式是安卓最重要的进阶知识之一,好的代码结构必定由各种设计模式组成,而学习设计模式之前,我们要了解面向对象的六大原则
六大原则是设计模式的基础理念,这里只需要理解记忆,学习完所有的设计模式后,再回来看六大原则,应该能有很大的收获
1、单一职责原则
单一原则很简单,就是将一组相关性很高的函数、数据封装到一个类中,每个方法的职责单一
这个原则是最简单,也是最难的一点,因为方法职责单一的界定,一般需要开发者自己的经验来判断
举例说明
class A{
// 执行第一步
private fun methodStep1(){}
// 执行第二步
private fun methodStep2(){}
// 执行第三步
private fun methodStep3(){}
// 真正的执行方法
fun realMethod(){
methodStep1()
methodStep2()
methodStep3()
}
}
上面的代码就符合单一职责的规则,即每个方法都有自己的职责,我们真正调用的方法是由多个单一职责的方法组成的
有人会说好麻烦,为什么不把这三个methodStep的代码放到realMethod()里面,让这个类只有一个方法呢?
我的回答是,不建议这么做,因为如果外部只需要调用某个步骤,例如methodStep2(),在只有一个realMethod()方法的情况下,我们必然要新增一个方法,无论是从realMethod()把那一段代码提取出来作为新方法,还是直接拷贝出来作为新方法,分别会出现修改原本的代码和重复代码的情况,这两种方式都是不建议的,所以设计这个类的时候,我们就要把单一职责的方法拆分出来,以便后续
为什么我的回答是“不建议”,而不是“不允许”呢,因为我们前面说过,“方法职责单一的界定,一般需要开发者自己的经验来判断”,如果一个方法虽然内部逻辑较多,但除了这个方法以外,不会再有其他方法会调用它里面的部分逻辑了,我们可以写在这个方法内,这样其他开发者也不会被一个类里的一堆方法弄得晕头转向,这种情况就要靠开发者的经验来判断了
2、开闭原则
它的定义是:一个类应该对于扩展是开放的,但是对于修改是封闭的
我们上面讲单一职责的时候,“修改原本的代码不被建议”,因为我们无法保证在修改的过程中不会引入Bug,这样会导致项目出现风险,一个好的设计是基于扩展的,我们的老代码稳定后可能从始至终都不会做逻辑的修改,我们只会在原来的基础上做扩展,实现新的需求
举例说明
class A{
// 执行第一步
private fun methodStep1(){}
// 执行第二步
private fun methodStep2(){}
// 执行第三步
private fun methodStep3(){}
// 真正的执行方法
fun realMethod(){
methodStep1()
methodStep2()
methodStep3()
}
// 新的扩展方法
fun newMethod(){
methodStep1()
methodStep2()
methodStep4()
}
// 步骤四
private fun methodStep4(){}
}
我们新的方法newMethod()在不改变老代码逻辑的情况下,即实现了自己的功能,又不会影响原有功能,这就是开闭原则的好处,也是我们遵守单一原则的好处
3、里式替换原则
它的定义是:所有引用基类的地方必须能透明地使用其子类对象
定义比较抽象,简单来说就是我们要多使用类的继承和多态
举个例子
// 父类
Class Parent {
fun method1()
fun method2()
}
// 子类 1
class ChildOne:Parent{}
// 子类 2
class ChildTwo:Parent{}
// 在方法里使用这两个类
var mChild:Parent? = null
// 初始化并调用父类的两个方法
fun init(child:Parent){
mChild = child
mChild.method1()
mChild.method2()
}
// 分别用子类一和二来实例化mChild对象
init(ChildOne())
init(ChildTwo())
var mChild:Parent
我们为什么要这样申明一个子类对象,而不是用
var mChild:ChildOne
var mChild:ChildTwo
原因其实很简单,如果我们用父类型来声明子对象,就可以使用父对象的所有子类来初始化这个对象了,而初始化后的子对象不管是任何类型,都可以调用method1()和method2(),这就是里式替换原则,即所有引用基类的地方必须能透明地使用其子类对象
那么这么做的好处是什么呢?这就要提到开闭原则了,它的定义建议我们扩展,我们上面那个
fun init(child:Parent)方法写好后,我们只需要扩展一个又一个child,就可以实现各种各样的method1()和method2(),在这个过程中,init方法所在的类甚至不需要做任何修改,这就是里式替换原则的好处
里氏替换原则,会作为一个技巧,出现在大部分设计模式里面,所以非常重要
4、依赖倒置原则
它的定义是:
(1)高层模块不应该依赖底层模块(具体实现),二者都应该依赖其抽象(抽象类或接口)
(2)抽象不应该依赖细节(废话,抽象类跟接口肯定不依赖具体的实现了)
(3)细节应该依赖于抽象(同样废话,具体实现类肯定要依赖其继承的抽象类或接口)
一句定义,两句废话,开个玩笑,2,3点主要是对第一点做出补充,这个原则看似复杂,其实学习了上面的里氏替换原则案例代码,理解起来很轻松
具体有多简单,就是说,这个原则的意思是——面向接口(抽象)编程
下面举例
// 接口一
interface A{
fun methodA()
}
// 接口二
abstact class B{
fun methodB()
}
// 执行的类里面
class Demo{
var mA:A? = null
var mB:B? = null
fun method(){
mA?.methodA()
mB?.methodB()
}
...
//省略mA和mB的初始化
}
我们可以看到,class Demo在对象声明里面声明了两个对象,接口A的实现mA和抽象类B的实现mB,method调用时根本不关心mA或者mB的具体实现,只关心他们的接口或者抽象类,这就是
高层模块不应该依赖底层模块(不关心mA和mB底层实现),二者都应该依赖其抽象(只关心他们的接口或者抽象类)
5、接口隔离原则
它的定义是:类之间的依赖关系应该建立在最小的接口上
简单来说,就是一个类不应该被强迫实现一些他们不会使用的接口,我们应该将一个复杂的接口拆分为多个小的单一职责的接口,这样类来实现时,可以根据需求,实现对应的接口
举例如下
//定义一个包含快乐行为的接口
interface Happy{
// 吃饭
fun eat()
// 唱歌
fun sing()
// 睡觉
fun sleep()
// 吃粑粑
fun eatShit()
}
// 人实现快乐行为
class Man:Happy{
override fun eat()
override fun sing()
override fun sleep()
override fun eatShit()
}
// 狗狗实现快乐行为
class Dog:Happy{
override fun eat()
override fun sing()
override fun sleep()
override fun eatShit()
}
人和狗都实现了快乐行为接口,重写了所有的快乐方法, 看上去是不是感觉哪里不对?
问题出在 sing()唱歌和eatShit()方法上,狗狗怎么会唱歌呢?人又怎么会因为吃粑粑开心呢?
那么我们改下代码
//定义一个通用的快乐行为的接口
interface Happy{
// 吃饭
fun eat()
// 睡觉
fun sleep()
}
// 定义一个人类独有的开心行为接口
interface ManHappy:Happy{
// 唱歌
fun sing()
}
// 定义一个狗狗独有的开心行为接口
interface DogHappy:Happy{
// 吃粑粑
fun eatShit()
}
// 人实现人快乐行为
class Man:ManHappy{
override fun eat()
override fun sing()
override fun sleep()
}
// 狗狗实现狗快乐行为
class Dog:AnimalHappy{
override fun eat()
override fun eatShit()
override fun sleep()
}
可以看到,人实现了人类快乐接口,狗狗实现了狗狗快乐接口,他们的快乐行为都是自己想要的,通过拆分接口,让各个接口职责单一,类不会被强迫实现一些他们不会使用的接口方法,这就是让类之间的依赖关系建立在了最小的接口上
6、迪米特原则
它的定义是:一个对象应该对其他的对象有最少的了解
这是最后一个原则,也很容易理解,就是说我们类之间互相调用的时候,应该暴露尽量少的方法,这种做法有什么好处?下面举例
// 定义一个人
class Man{
// 人的所有钱
private var totalMoney = 100
var mRestaurant:Restaurant? = null
// 人从餐厅获取食物,并吃掉
fun eat(){
totalMoney-=5
mRestaurant?.getFood(5)
}
}
// 定义一个餐厅
class Restaurant{
private val mCook:Cook? = null
// 取餐
fun getFood(price:Int):Food?{
getMoney(price)
val food = makeFood()
return food
}
private fun getMoney(price:Int){
}
// 生产食物
private fun makeFood():Food?{
return mCook?.make()
}
}
可以看到,基于一个对象应该对其他的对象有最少的了解的原则,人只知道到餐厅能取餐,但并不知道餐厅厨师的信息,以及食物的制造过程,餐厅也只知道人支付的钱,并不了解这个人的信息,这就是迪米特原则