设计原则是基于面向对象思想衍变出来的一些规则,用来解决实际开发中的一些痛点,是所有设计的底层思想,也是我个人认为是设计/架构领域最重要的知识,所以请大家务必掌握好
2.1 单一设计原则
案例:本地获取用户信息,提交到网络
fun post(){
//创建数据库访问对象Dao
val userDao = …(这一过程很复杂)
//从本地获取
val age = dao.getAge()
val name = dao.getName()
//…省略大量字段
//将个人信息提交至网络
http.request(age,name,…)
}
以上案例将创建、获取、提交三步操作写到同一个函数中,很显然违背了单一设计原则,面临的问题也很明显,当修改创建、获取、提交任一过程时都会影响到其他二者,千万不要说"我注意一点就不会出错"这种话,因为人不是机器改动就可能出错,此时可以通过单一设计原则做一次重构,代码如下:
fun getUserDao():Us
erDao{
…
return dao
}
fun getUserInfo():UserInfo{
val dao = getUserDao()
val userInfo = UserInfo()
userInfo.age = dao.getAge()
userInfo.name = dao.getName()
…
return userInfo
}
fun post(){
val userInfo = getUserInfo()
//将个人信息提交至网络
http.request(userInfo.age,userInfo.name,…)
}
三步操作被拆至三个函数 互不影响,从根本上杜绝因改动带来的一系列问题。所以使用面向对象语言开发时,不要急着写代码,要优先考虑下模块、类、函数...的设计是否足够单一
2.2 开闭原则
一句话概括开闭原则:对扩展开放,修改关闭。它即充分诠释抽象、多态特性,又是多数行为型设计模式的基础,遍布于各大优秀框架之中,是最重要的一条设计原则,仅这一条原则就能把你的设计能力提高40%
举个例子让大家感受一下:
需求:通过SQLite做CRUD操作
class SQLiteDao{
public void insert() {
//通过SQLite做insert
}
public void delete() {
//通过SQLite做insert
}
}
SQLiteDao dao = new SQLiteDao();
dao.insert();
…
以上是最简单粗暴的写法,但存在一个致命问题,如果某一天想替换SQLite业务层基本要动一遍,改动就存在出错的可能,并且需要做大量的重复操作
面对以上问题可以利用抽象、多态特性基于开闭原则做出重构,代码如下:
interface IDao{
void insert();
void delete();
}
class SQLiteDao implements IDao{
@Override
public void insert() {
//通过SQLite做insert
}
@Override
public void delete() {
//通过SQLite做insert
}
}
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即可,其他地方完全不用改动。这就是所谓的扩展开放,修改关闭
在业务不断迭代情况下,唯一不变的就是改变,这种背景下我们能做的只有在代码中基于开闭原则多留扩展点以不变应万变。
2.3 迪米特法则
基本概念:不该有直接依赖关系的模块不要有依赖。有依赖关系的模块之间,尽量只依赖必要的接口。
迪米特法则很好理解并且非常实用,违背迪米特法则会产生什么问题?还以2.1面向过程代码举例:
class Wallet{
/**
- 余额
*/
int balance;
/**
- 存钱
*/
void saveMoney(int money){
balance += money;
}
/**
- 花钱
*/
void spendMoney(int money){
balance -= money;
}
}
Wallet的设计违背了迪米特法则,毕竟外部只需要save和spend功能,将balance暴漏使用者就有权限直接修改其值,可能会对整个Wallet功能造成影响。此时应基于迪米特法则对Wallet进行改造,将balance通过封装特性增加private修饰符
迪米特法则和单一设计原则很像,前者符合松耦合后者符合高内聚
2.4 接口隔离原则
基本概念:接口的调用者不应该依赖它不需要的接口。
乍一看与迪米特法则很相似。先来看下什么样的接口违背接口隔离原则:
interface Callback{
/**
- 点击事件回调方法
*/
void clickCallback();
/**
- 滚动事件回调方法
*/
void scrollCallback();
}
接口Callback包含点击、滚动两个回调方法,面临的问题有两个:
-
某些特定场景
使用者只需要依赖点击回调,那滚动回调便成了多余,把外部不需要的功能暴露出来就存在误操作的可能。 -
点击和滚动本来就是两种特性,强行揉到一块只能让接口更臃肿,进而降低其复用性
根据接口隔离原则改造后如下:
interface ClickCallback{
/**
- 点击事件回调方法
*/
void clickCallback();
}
interface ScrollCallback{
/**
- 滚动事件回调方法
*/
void scrollCallback();
}
基于单一设计原则把点击和滚动拆分成两个接口,将模块间隔离的更彻底。并且由于粒度更细,所以复用性也更高
接口隔离原则与迪米特法则目的很相似,都可以降低模块间依赖关系。但接口隔离更侧重于设计单一接口,提升复用性并间接降低模块间依赖关系,而迪米特法则是直接降低模块间依赖关
2.5 里氏替换原则
基本概念:
里氏替换非常简单并且很容易遵守,在使用继承时,允许复写父类方法,但不要改变其功能。比如自定义View,子类的onMeasure中一定要调用setMeasureaDimission()方法(或者直接使用super),否则会影响父类方法功能(会抛异常),也既违背了里氏替换原则。
2.6 依赖倒置原则
控制反转: 提及依赖倒置便不得不提控制反转,一句话概括:将复杂的程序操作控制权由程序员交给成熟的框架处理,程序员->成熟的框架为反转,框架应暴露出扩展点由程序员实现 想详细了解可至 关于Android架构,你是否还在生搬硬套? 2.1章节查看
什么是依赖倒置?
其实核心点就是基于接口而非实现编程,2.2数据库案例也符合依赖倒置原则,高层模块(业务层)不依赖于低层模块(SQLiteDao/RoomDao),而是依赖于抽象(IDao),可见依赖倒置也是开闭原则扩展而来。 区别是依赖倒置更侧重于指导框架的设计,框架层应该尽量将更多的细节隐藏在内部,对外只暴露抽象(抽象类/接口),指导框架设计这方面核心就是控制反转
3. 设计模式只是设计原则的产物而已
设计模式共有23种,详细描述都能出一本书出来。本小结仅会分享一些通用的思路,个人认为还是比较硬核的,毕竟设计主要还是思想,而非生搬硬套
3.1 设计模式该怎么去学?
本小节会分析几个常见的设计模式核心思想以及设计背景,用于抛砖引玉
工厂模式
基本概念:用于创建复杂对象
创建复杂对象常规写法如下:
class B{
…
}
class D{
void test(){
B b = …(创建B的过程很复杂)
…
}
}
在使用的地方直接创建,如果直接new倒也没啥问题,但如果创建过程过于复杂,当修改创建过程时就会影响到test(),进而存在一些未知的隐患。
这一问题可通过迪米特法则进行改造:
class FactoryB{
…
static B createB(){
…(B创建过程)
return b;
}
}
class D{
void test1(){
B b = FactoryB.create();
…
}
}
B的创建本身就于调用者无关,将创建过程转移到类FactoryB中,根本上避免了创建过程对调用者的影响。改造后就是一个标准的简单工厂模式,所以简单工厂模式的核心思想就是迪米特法则
观察者模式
基本概念:当一个对象发生改变时需要通知到另一个对象
粗暴写法:
/**
- 观察者
*/
class Observer{
/**
- 接收通知
*/
void receive(){
//具体逻辑
}
}
/**
- 被观察者
*/
class Observable{
/**
- 发送通知
*/
void send(){
Observer observer = new Observer();
observer.receive();
}
}
Observable(被观察者)内部直接持有Observer(观察者),在合适的时机发出通知,但这种写法有两个很明显的问题:
-
扩展性差:当存在多个观察者
Observer1,Observer2...时,Observable需要逐个手动创建发出通知 -
耦合性强:
Observable直接持有Observer对象,而Observer可能暴露出一些Observable不需要的属性/方法,存在误操作的风险
面对以上两个问题可以利用开闭原则和接口隔离原则进行改造:
interface IObserver{
/**
- 接收通知
*/
void receive();
}
class Observable{
/**
- 观察者集合
*/
private final List observers = new ArrayList<>();
/**
- 发送通知
*/
void send(){
for (IObserver observer : observers){
observer.receive();
}
}
}
以上是一个标准的观察者模式。通过接口隔离原则设计IObserver接口保证其单一性,避免模块之间依赖关系过强造成的安全隐患,解决了耦合性强问题。通过开闭原则维护一个observers,当新增观察者时只需添加到observers即可,符合扩展开放、修改关闭,解决类扩展性差问题。
所以开闭原则,接口隔离原则是观察者模式扩展性强,耦合性低的根本原因呐
以上三个案例足以表明设计模式的核心就是设计原则呐,所以学会设计模式的窍门就是先掌握设计原则
3.2 “盐加少许” 只可意会
性/方法`,存在误操作的风险
面对以上两个问题可以利用开闭原则和接口隔离原则进行改造:
interface IObserver{
/**
- 接收通知
*/
void receive();
}
class Observable{
/**
- 观察者集合
*/
private final List observers = new ArrayList<>();
/**
- 发送通知
*/
void send(){
for (IObserver observer : observers){
observer.receive();
}
}
}
以上是一个标准的观察者模式。通过接口隔离原则设计IObserver接口保证其单一性,避免模块之间依赖关系过强造成的安全隐患,解决了耦合性强问题。通过开闭原则维护一个observers,当新增观察者时只需添加到observers即可,符合扩展开放、修改关闭,解决类扩展性差问题。
所以开闭原则,接口隔离原则是观察者模式扩展性强,耦合性低的根本原因呐
以上三个案例足以表明设计模式的核心就是设计原则呐,所以学会设计模式的窍门就是先掌握设计原则










