如何学好设计,做好架构? 核心思想才是关键
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何学好设计,做好架构? 核心思想才是关键相关的知识,希望对你有一定的参考价值。
前言
其实好多小伙伴都认为学习设计
就是学习设计模式
,这是一个误区,没有底层思想的支持写出来的设计模式
无非就是生搬硬套罢了,这里的底层思想其实就是设计原则
,而设计原则
则是面向对象编程
基于现实背景衍生出来的一套规则,用来解决开发中的痛点。
好的架构需要反复进行思考以及设计,今天我将从面向对象
为出发点 来分享自己对设计/架构
衍变过程的理解,尽量帮你理清背景 抓住本质
文章目录看起来有些枯燥,但有别于其他八股文,跟着流程去学大概率可以短时间内见成效,所以请耐心阅读
目录
- 1. 面向对象
- 1.1 四大特性
- 1.2 诞生背景
- 2. 六大设计原则才是一切设计的基石
- 2.1 单一设计原则
- 2.2 开闭原则
- 2.3 迪米特法则
- 2.4 接口隔离原则
- 2.5 里氏替换原则
- 2.6 依赖倒置原则
- 3. 设计模式只是设计原则的产物而已
- 3.1 设计模式该怎么去学?
- 3.2 “盐加少许” 只可意会
- 3.3 自创设计模式
- 4. 如何做好架构?
1. 面向对象
什么是面向对象?
估计这个问题能难倒一大片同学,相信读完本文你心里应该会有一个合适的答案。先来看下基本定义:
面向对象是一种风格,会以类作为代码的基本单位,通过对象访问,并拥有封装、继承、多态、抽象四种特性作为基石,可让其更为智能。代表语言Java
1.1 四大特性(也有人说三种,不要纠结)
封装
封装也也可称之为信息隐藏。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。举个例子解释下:
class User{
private String idNumber;
private String name;
public String getIdNumber() {
return idNumber;
}
public void setIdNumber(String idNumber) {
this.idNumber = idNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
User
类中包含身份证号、姓名
等个人信息,这些属性一旦暴露那外界就可以随意修改,进而可能产生安全隐患。此时可通过private
修饰符将其隐藏在内部,如果确实需要访问只能通过暴露出来的唯一入口getter,setter
方法进行,这一过程就是封装
合理运用封装可以降低模块间依赖关系(松耦合)
继承
“继承”是面向对象中的第二特征,体现了类与类之间的“is-a”关系。当两个类进行继承关联绑定的时候,子类自动具备来自于的父类的属性和行为。可以提升复用性解决模板代码问题,提升开发效率的同时也解决了
错写,漏写
带来的问题
多态
一句话概括"多态":一个对象多种形态。举个例子说明下:
interface IFruit{
String getColor();
}
class Apple implements IFruit{
@Override
public String getColor() {
return "red";
}
}
IFruit fruit = new Apple();
fruit.getColor();
通过声明的IFruit
类型可以对其实现类
Apple进行编程,好处就是扩展性强,当需要替换具体实现
Apple时,对
IFruit`的操作完全不用改
合理运用多态可以写出易扩展
的代码,基于接口而非实现编程
和开闭原则
的核心
抽象
抽象的目的是为了隐藏方法的具体实现,让调用者只需要关心方法提供了哪些方法(功能),并不需要知道这些功能是如何实现的。在
Java
中体现方式是接口
和抽象类
接口和抽象类的区别
- 接口更侧重于功能的设计,并且能将具体实现与调用者隔离,一般要以接口隔离原则设计接口既粒度越细越好
- 抽象类更侧重于提升复用性,在原有的基础上预留扩展点供开发者灵活实现
- 区别:接口可以降低模块间耦合性,抽象类可提升复用性。
- 相同点:均有较好的扩展性,符合开闭原则
tips
面向对象的四大特性
相信大家都很熟悉,本小结只是帮大家做一次简单的回忆,关于其背景
和职责
下半问会详细描述
1.2 诞生背景
谈及面向对象必定磨不开面向过程,毕竟它就是由面向过程衍变而来,吸收其大部分优点并解决其痛点。那什么是面向过程呢?基本定义如下:
分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,更侧重于功能的设计。代表语言C
用代码体现就是下面这样:
#java版面向过程
public class Wallet {
/**
* 余额
*/
int balance;
/**
* 存钱
*/
void saveMoney(int money){
balance += money;
}
/**
* 花钱
*/
void spendMoney(int money){
balance -= money;
}
}
无权限修饰符将内部信息全部暴露,简单粗暴很符合初级程序员的思维,但带来的问题很明显,外部可直接访问balance
修改钱包内余额,现象就是"我钱包都没掏出来但里面钱却变少/多了"
。面向过程
在开发中带来的问题远不止这些,所以在此背景下诞生了面向对象 通过面向对象封装
特性将面向过程
代码做个改进,如下:
#java版面向对象
public class Wallet {
/**
* 余额
*/
private int balance;
/**
* 存钱
*/
void saveMoney(int money){
balance += money;
}
/**
* 花钱
*/
void spendMoney(int money){
balance -= money;
}
}
通过封装
特性将balance
通过private
修饰,这样外部就没有权限直接修改金额,避免误操作带来的未知风险,满足松耦合特性 面向过程
编程偏向于功能的开发,简单粗暴难以维护。而面向对象
在编程之前需要基于四大特性
对功能做建模
设计,可以提高代码安全性、复用性、扩展性
,更易于维护 既然面向对象
这么智能
为什么面向过程
语言还没有被淘汰?其实面向对象
语言的智能
是针对我们开发者
的,为了能让我们能写出易于维护的代码会多做一步设计,虽然离开发者更近
了 但离机器确远
了,毕竟机器只认识0和1而已。C语言规则简单易于形成机器码,所以执行效率高,这也是其没有被淘汰的原因。
小提示:
不要以为用了面向对象语言写出的就是面向对象代码,如果没有利用其特性那可能还是面向过程,比如没有利用权限修饰符、一个类一把梭等等…
2. 六大设计原则才是一切设计的基石
设计原则是基于面向对象思想衍变出来的一些规则,用来解决实际开发中的一些痛点,是所有设计的底层思想,也是我个人认为是设计/架构
领域最重要的知识,所以请大家务必掌握好
2.1 单一设计原则
单一原则很好理解,指一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性。
案例:本地获取用户信息,提交到网络
fun post(){
//创建数据库访问对象Dao
val userDao = ...(这一过程很复杂)
//从本地获取
val age = dao.getAge()
val name = dao.getName()
//....省略大量字段
//将个人信息提交至网络
http.request(age,name,....)
}
以上案例将创建、获取、提交
三步操作写到同一个函数中,很显然违背了单一设计原则
,面临的问题也很明显,当修改创建、获取、提交
任一过程时都会影响到其他二者,千万不要说"我注意一点就不会出错"
这种话,因为人不是机器改动就可能出错,此时可以通过单一设计原则
做一次重构,代码如下:
fun getUserDao():UserDao{
...
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<IObserver> observers = new ArrayList<>();
/**
* 发送通知
*/
void send(){
for (IObserver observer : observers){
observer.receive();
}
}
}
以上是一个标准的观察者模式
。通过接口隔离原则
设计IObserver
接口保证其单一性,避免模块之间依赖关系过强造成的安全隐患,解决了耦合性强问题。通过开闭原则
维护一个observers
,当新增观察者时只需添加到observers
即可,符合扩展开放、修改关闭
,解决类扩展性差问题。
所以开闭原则,接口隔离原则
是观察者模式扩展性强,耦合性低
的根本原因呐
以上三个案例足以表明设计模式
的核心就是设计原则
呐,所以学会设计模式
的窍门就是先掌握设计原则
3.2 “盐加少许” 只可意会
据我所知有一部分小伙伴觉得用设计模式
很酷,以至于拿着锤子看什么都是钉子,很简单的代码还非要用几个设计模式包装下
。还有另一部分小伙伴对设计模式理解不够深刻,把握不好应用场景,经常生搬硬套做出多余的设计。以上两种现象 不但解决不了任何问题反而会降低代码的可读性
究竟什么场景下需要用设计模式呢?关于这个问题我只能回答合适的场景
,因为它根本没有一个固定答案。就如同老师傅做饭时讲的少许盐、少许油
一样,因为不同的食材需要的油盐不一样,所以不好去量化,只能根据自己的经验去放。回归到代码中也是一样的,我们在实践中需要不断思考,尝试去发现开发中的痛点,设计模式就是用来解决这些痛点的,所以只有理清背景才能将设计模式
用的恰到好处
3.3 自创设计模式
有一说一23
种设计模式我也不是全懂,但由于我懂设计原则
我一样可以写出易维护
的代码,甚至自创设计模式
在接触LiveData
之前,其实我已经有意无意感受到了数据驱动
的思想,在传统的MVP
模式下我会在View
层事先写好对应UI
渲染逻辑,Presenter
由接口进行驱动(这其实也是数据驱动UI)。 当LiveData/DataBinding
走进视线并且大家开始讨论数据驱动UI
时,那一刻我仿佛找到了组织,自己的想法终于得到了验证。之所以我能够有意无意遵守数据驱动UI
是因为我原本就掌握了控制反转
思想
阅读Retrofit
源码前我甚至不知道门面模式
的存在,但我依旧能理解ApiService
奥妙之所在,无非就是想将Retrofit
的实现尽量屏蔽在其内部,尽可能降低模块间依赖关系,符合迪米特法则
同时也是门面模式
的一种写法。
说了这么多还是想告诉大家:设计原则才是根本
4. 如何做好架构?
掌握设计原则
可以写出扩展性强、复用性高...
的代码
掌握设计模式
可以设计出易用性强、安全性高...
成熟的框架
掌握设计原则、设计模式
,可以设计出容错率更高的架构
那什么是架构?
架构是一个很笼统的概念,上至框架选型下至业务代码都能称为架构
的一部分,比喻到盖房子 设计图,打地基,选料…
都能称之为架构,总之能够提升项目稳定性以及开发效率就是好架构。好的架构不是一蹴而就,而是根据面临的问题不断添砖加瓦
架构是如何衍变的?
- 远古时代,基于Activity和XML开发,XML这种结构可以天然的将视图与Activity隔离,看起来很美妙,我也很开心…
- 随着业务的发展,Activity代码不断壮大,各种逻辑全都揉到一块,常常改一处崩多处。我觉得不能再拖了,得赶紧基于
单一设计原则
将代码进行模块化。模块化后效果很明显,莫名其妙的bug少了很多…- 某一天网络请求时发现参数一直对不上,各种排查才发现原来是修改某个
View
时对应的数据却忘记改了,这个问题真的很头痛。偶然间发现LiveData、DataBinding
,这玩意基于控制反转+观察者
设计 改变数据就能修改UI,那我肯定毫不犹豫引入到项目中啊。从此我再也不用担心数据UI一致性
问题了…- 数年后,项目工程逐渐庞大,编译一次都要好几分钟,找个文件找半天还容易改错,令大家苦不堪言。听说
android
可以依据单一原则
将代码拆分至多个module
中并可以单独运行,试了试果然可以…- 最近有同事经常跟我抱怨:
“每个Activity都有好多重复代码啊,而且一不留神容易错写、忘写”
,这让我想到了模版设计模式
,将通用功能封装在内部并暴露一些抽象方法(钩子方法),新来的同事也变得开心的了,基于这套模板他可以无障碍开发…- 未完待续…
以上是一个简单架构
的衍变过程,选用的每一个库都是基于设计原则,设计模式
拓展出来用来解决开发痛点的。但是团队开发人员水平可能参次不齐,不一定能领悟到架构的含义,仅从口头
上约束可能作用不大,此时一般会通过模板模式
将通用信息做封装,在内部协调好各模块间关系,并暴露出对应的泛型、抽象方法(钩子),这样开发人员在使用模板类的时候就会被强制遵守现有的规则。
tips
关于
面向对象、设计原则、设计模式
如果详细讲解能写三大本
书出来。本文主要描述其基本概念、设计背景以及三者之间的关系,起到抛砖引玉的作用。想真的学好设计、做好架构 需要不断从实践中体会、思考。
最后
小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 Codechina地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。
以上是关于如何学好设计,做好架构? 核心思想才是关键的主要内容,如果未能解决你的问题,请参考以下文章