设计模式之设计原则
本文部分节选和引用自
如何学好设计,做好架构? 核心思想才是关键
感谢作者提供的高质量文章
1.单一设计原则
单一设计原则很好理解,指一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够降低耦合性。
案例:获取本地用户信息,提交到网络
fun post() {
//创建数据库访问对象DAO
val userDao = ...
//从本地获取数据
val name = userDao.getName()
val age = userDao.getAge()
//发送请求并且携带数据
val http = ...
http.post(name, age,...)
}
案例中将,创建
、获取
、提交
三个步骤写到同一个函数中,很显然违背了单一设计原则
,当修改这三步中的逻辑时,都会影响到其他的步骤,当业务逐渐复杂后出现问题的概率就会指数级上升,因此可以通过单一设计原则做一次重构,代码如下:
fun getUserDao():UserDao {
...
return dao
}
fun getUserInfo():UserInfo {
val dao = getUserDao()
val userInfo = UserInfo()
userInfo.name = dao.getName()
userInfo.age = dao.getName()
...
return userInfo
}
fun post() {
val userInfo = getUserInfo()
getHttp().post(usetInfo.name,userInfo.age,...)
}
将逻辑独立的三步拆成三个函数,从根本上杜绝改动带来的额问题,在设计代码的时候不要先直接开始写,可以先考虑一下模块、类、函数的设计是否足够单一
2.开闭原则
一句话概括开闭原则
:对扩展开放,修改关闭
。它充分地诠释了抽象
、多态
的特性,并且也是多数行为型设计模式
的基础,遍布于各大优秀框架之中,是最重要的一条设计原则,光是这一条设计原则就能吧你的设计能力提升百分之四十以上
案例:通过SQLite做CRUD操作
class SQLiteDao{
public void insert(){
...
}
public void delete(){
...
}
}
SQLiteDao dao = new SQLiteDao();
dao.insert();
...
以上就是简单粗暴的写法,但是存在一个致命问题,如果摸一天想替换SQLite业务层基本要把调用dao对象的地方都动一遍,改动就会存在出错的可能,并且需要做大量的重复操作
下面就用利用抽象、多态基于开闭原则重构,代码如下
interface IDao{
void insert();
void delete();
}
class SQLite imlements IDao {
@Override
public void insert(){
...
}
@Override
public void delete(){
...
}
}
class RoomDao implements IDao{
@Override
public void insert() {
//通过Room做insert
}
@Override
public void delete() {
//通过Room做delete
}
}
//扩展点
IDao dao = new SQLiteDao();
dao.insert();
- 定义功能接口
IDao
- 定义类
SQLiteDao
、RoomDao
并实现IDao
的功能 - 业务基于接口
IDao
进行编程
重构后,需要将SQlite
替换至Room
,只要将注释扩展点处的SQLiteDao
替换成RoomDao
即可,其他地方完全不同改动。这就是所谓的扩展开放,修改关闭
在业务不断迭代的情况下,唯一不变的就是改变,这种背景下我们能做的只有在代码中基于开闭原则
多留扩展点
以不变应万变。
3.迪米特原则
基本概念:不该有直接依赖关系的模块不要有依赖。有依赖关系的模块之间,尽量只依赖必要的接口。
迪米特原则很好理解并且很实用,违背迪米特原则会产生什么问题?
案例:
class Wallet{
/**
* 余额
*/
int balance;
/**
* 存钱
*/
void saveMoney(int money){
balance += money;
}
/**
* 花钱
*/
void spendMoney(int money){
balance -= money;
}
}
Wallet
的设计违背了迪米特法则,毕竟外部只需要save
和spend
功能,将balance
暴漏使用者就有权限直接修改其值,可能会对整个Wallet
功能造成影响。此时应基于迪米特法则对Wallet
进行改造,将balance
通过封装特性增加private修饰符
迪米特法则和单一设计原则很像,前者符合松耦合后者符合高内聚
4.接口隔离原则
基本概念:接口的调用者不应该依赖它不需要的接口。
interface Callback{
/**
* 点击事件回调方法
*/
void clickCallback();
/**
* 滚动事件回调方法
*/
void scrollCallback();
}
接口Callback包含点击、滚动两个回调方法,面临的问题有两个:
- 某些特定场景使用者只需要依赖点击回调,那滚动回调便成了多余,把外部不需要的功能暴露出来就存在误操作的可能。
- 点击和滚动本来就是两种特性,强行揉到一块只能让接口更臃肿,进而降低其复用性
根据接口隔离原则改造后如下:
interface ClickCallback{
/**
* 点击事件回调方法
*/
void clickCallback();
}
interface ScrollCallback{
/**
* 滚动事件回调方法
*/
void scrollCallback();
}
基于单一设计原则把点击和滚动拆分成两个接口,将模块间隔离的更彻底。并且由于粒度更细,所以复用性也更高
接口隔离原则与迪米特法则目的很相似,都可以降低模块间依赖关系。但接口隔离更侧重于设计单一接口,提升复用性并间接降低模块间依赖关系,而迪米特法则是直接降低模块间依赖关
5.里式替换原则
基本概念:
设计子类的时候,要遵守父类的行为约定。父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定
里氏替换非常简单并且很容易遵守,在使用继承时,允许复写父类方法,但不要改变其功能。比如自定义View,子类的onMeasure
中一定要调用setMeasureaDimission()
方法(或者直接使用super),否则会影响父类方法功能(会抛异常),也既违背了里氏替换原则。
6.依赖倒置原则
控制反转: 提及依赖倒置便
不得不提控制反转
,一句话概括:将复杂的程序操作控制权由程序员交给成熟的框架处理,程序员->成熟的框架为反转,框架应暴露出扩展点
由程序员实现
什么是依赖倒置?
高层模块(使用者)不应依赖低层模块(被使用者),它们共同依赖同一个抽象,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
其实核心点就是基于接口而非实现编程,2数据库案例也符合依赖倒置原则,高层模块(业务层)不依赖于低层模块(SQLiteDao/RoomDao),
而是依赖于抽象(IDao)
,可见依赖倒置也是开闭原则扩展而来。
区别是依赖倒置更侧重于指导框架的设计,框架层应该尽量将更多的细节隐藏在内部,对外只暴露抽象(抽象类/接口),指导框架设计这方面核心就是控制反转