分层思想
将逻辑分层处理,不同层次之间有不同的职责,不存在跨层访问,从而降低耦合,职责清晰,容易拓展,容易复用。
分层思想就是宏观的单一职责原则。
每一层有自己的职责,也只负责自己的职责,每一层只使用上层的服务并向下层提供服务。
最简单的例子就是计算机网络的五层协议,每一层协议只对下层负责,不会出现跨层访问的情况。
那么,分层有啥好处呢?我们上代码。
假如我们要做一个语音直播间,在不分层的情况下,我们就把所有代码都梭哈到页面里面:
class PageVoiceLiveRoom {
// 存储消息
private List<String> msgs = new ArrayList();
// 负责控制声音
private AudioEngine audioEngine;
// 负责控制消息
private SocketEngine socketEngine;
// 绘制UI
void drawUI() {
}
// 打开麦克风
void openMic() {
audioEngine.openMic();
}
// 打开扬声器
void openSpeaker() {
audioEngine.openSpeaker();
}
// 发消息
void sendMsg(String msg) {
socketEngine.sendMsg(msg);
}
// 收到消息
void onReceiveMsg(String msg) {
msgs.add(msg);
}
}
写完感觉美滋滋,代码良好运行,一点问题都没有。
突然有一天,产品说要加个小窗功能,小窗的情况下页面不存在,但是声音和逻辑都在。
我代码都写在页面上了,你这页面都没了,声音肯定也没了。
那这得改啊,不能全干在 UI 上,得拆开,UI 只处理 UI,其他的放在非 UI 上才行,这样的话,一旦页面销毁,也只是 UI 相关的销毁,声音和逻辑也都在,改吧。
于是,就有了下面的代码。
首先定义非 UI 逻辑,用来保存数据。
class VoiceLiveRoomData {
// 存储消息
private List<String> msgs = new ArrayList();
// 负责控制声音
private AudioEngine audioEngine;
// 负责控制消息
private SocketEngine socketEngine;
// 打开麦克风
void openMic() {
audioEngine.openMic();
}
// 打开扬声器
void openSpeaker() {
audioEngine.openSpeaker();
}
// 发消息
void sendMsg(String msg) {
socketEngine.sendMsg(msg);
}
// 收到消息
void onReceiveMsg(String msg) {
msgs.add(msg);
showMsg(msg);
}
}
然后定义 UI 逻辑,不再存储数据,只是负责展示。
class PageVoiceLiveRoom {
private VoiceLiveRoomData roomData;
// 绘制UI
void drawUI() {
}
// 打开麦克风
void openMic() {
roomData.openMic();
}
// 打开扬声器
void openSpeaker() {
roomData.openSpeaker();
}
// 发消息
void sendMsg(String msg) {
roomData.sendMsg(msg);
}
// 收到消息
void onReceiveMsg(String msg) {
showMsg(msg);
}
}
这样以来,UI 部分只负责展示数据和处理 UI 事件,不再存储数据。当 UI 销毁后,VoiceLiveRoomData会继续存在,并继续处理非 UI 部分的逻辑,比如存储消息。当 UI 再建立后,直接再次获取一次VoiceLiveRoomData即可,数据并没有缺失,声音也没有中断。此时PageVoiceLiveRoom就叫做 UI 层,VoiceLiveRoomData就叫做数据层,这也是 MVC 思想的核心。
我们可以给它起名叫做:数形分离思想,数是数据,形是图形,也就是 UI。
那么,代码这样就行了吗?
不行!这并不能完美体现分层思想的优点。
比如:现在我要做一个视频直播间,视频直播间包含了语音直播间的功能,怎么包含呢?
很简单啊,直接新建一个VideoLiveRoomData,让它继承VoiceLiveRoomData就行了,子类通过继承可以复用父类功能。
继承也是分层的一种体现。
但是,如果我想做一个没有声音没有视频、只能打字的聊天室,要怎么办呢?
这就不能继承了,因为它的功能太少了,它的功能是语音直播间的子集,所以,只能让语音直播间继承它。
所以,我们要改代码,我们的层级应该是下述这样的。
-
A:普通直播间,没有语音功能。
-
B:语音直播间,有语音功能,有 A 的所有功能,是 A 的下级,并且继承 A。
-
C:视频直播间,有视频功能,有 B 的所有功能,是 B 的下级,并且继承 B。
好,逻辑理清了,我们就上代码。
普通直播间定义:
// 普通直播间,只有打字功能
interface IBaseLiveRoomContext {
void sendMsg(String msg);
}
class BaseLiveRoomContextImpl implements IBaseLiveRoomContext {
private SocketEngine socketEngine;
void sendMsg(String msg) {
socketEngine.sendMsg(msg);
}
}
语音直播间定义:
// 语音直播间,有普通直播间的所有功能,并且拓展了语音相关逻辑
interface IVoiceLiveRoomContext extends IBaseLiveRoomContext {
void openMic();
void closeMic();
void openSpeaker();
void closeSpeaker();
}
class VoiceLiveRoomContextImpl extends BaseLiveRoomContextImpl implements IBaseLiveRoomContext {
private AudioEngine audioEngine;
void openMic() {
audioEngine.openMic();
}
void closeMic() {
audioEngine.closeMic();
}
//.....
}
视频直播间定义:
// 视频直播间,有语音直播间所有功能,并且拓展了视频相关逻辑
interface IVideoLiveRoomContext extends IVoiceLiveRoomContext {
void startPreview();
void stopPreview();
}
class VideoLiveRoomContext extends VoiceLiveRoomContextImpl implements IVideoLiveRoomContext {
private VideoEngine videoEngine;
// .......
}
我们看代码就知道,我们的层级关系为:
其中,下级可以使用上级的所有功能,这就是分层的另一个好处:复用。
这类似于树形结构,如果将来出了个新直播间 D,如果 D 具有视频直播间的所有功能,那么直接让 D 继承VideoLiveRoomContext即可,也就是放在VideoLiveRoomContext的下面;如果 D 没有视频直播间的所有功能呢,但是有语音直播间的所有功能,那么直接让它继承VoiceLiveRoomContextImpl即可。
其实,我们的分层架构,就像是一棵树,新增的功能就是树上的一个节点,总有插入这个节点的地方。
分层就是为了每一层能各司其职,互不影响,从而降低耦合,容易拓展。
粒度细化思想
将大功能拆成一个个的小功能,越小越好。
一个城市可以拆分成一个个的县,一个县又能拆成一个个的乡,一个乡又能拆成一个个的村,一个村又能拆成一个个的家庭,一个家庭又能拆成一个个的人,如果把城市比喻成沙漠,那么一个个的人就像一粒粒的沙子,这就是粒度细化。
这样有啥好处呢?
比如,我有个模块 A,包含了功能 B1 和 B2,假如,我只想使用 B1 功能,如果模块 A 没有拆分的话,我必须依赖模块 A,这样就可以使用 A 里面的 B1 了,但是这样导致我也依赖了 B2 了,如果将来 B2 有改动,我就不得不跟着变化,这明显是不好的,违背了最少知识原则,也不满足开放闭合原则。
所以,我们要对模块 A 进行拆分,拆分成 B1 和 B2 两个部分,这样,我只需要依赖 B1 即可,B2 的任何改动都不会对我造成任何影响。
所以,你看,粒度细化是不是宏观的接口隔离原则?接口隔离原则要求接口尽量小,而粒度细化要求模块尽量小。
那么,粒度是不是越细越好呢?
不是!
比如,一个人的基本功能是吃喝拉撒,你把这四个功能定义给一个人就行,而不需要把每一个功能都单独定义,因为这样就不符合现实了。
所以,粒度的大小要符合常规逻辑。
而且我们会发现,粒度细化思想也是宏观的最少知识原则,因为功能拆得细,所以跟自身无关的业务就不再包含到自身,比如上述的 B1 就跟 B2 没关系,那么其他模块使用 B1 的时候,也不会跟 B2 有关联,这不正是最少知识原则的体现吗!
易变性思想
把我们的代码写成容易修改的。
优先选择容易变化的数据类型。其实这就是易变性思想的体现。
我们在写代码的时候,一定要优先设计成容易变化的,优先使用接口而非对象,优先使用集合而非单一数据。
比如,现在我有个送礼接口,那么我就定义成:
public void sendGift(long uid,Gift gift) {
}
其实没毛病,但是,将来有一天,产品要求可以多人送礼,也就是一个礼物可以送给多个人。
所以,我们应该是将上述的uid定义成一个集合,也就是:
public void sendGift(List<Long> uids,Gift gift) {
}
这样一来,送一个人还是多个人,都没有问题。
所以你看,采用可变的数据类型,是不是减少了工作量。
再看个例子:
public void handleInput(String input) {
if("Java".equals(input)) {
doJava();
} else if("javascript".equals(input)) {
doJS();
} else if("python".equals(input)) {
doPython();
}
}
相信大家都写过这种if-else语句,如果条件非常多的话,那简直不要太酸爽。
这肯定不行啊,不满足开放闭合原则,那就改。
我们使用map来优化下:
// 定义一个map
Map<String, Runnable> actions = new HashMap();
// 将key和value存放在map中
actions.put("Java", {doJava()})
actions.put("javascript", {doJS()})
actions.put("python", {doPython()})
public void handleInput(String input) {
if(actions.containsKey(input)) {
actions.get(input).run();
}
}
我们的函数处理是不是非常简单了,而且也很容易修改,如果将来有添加或者删除,我们只需要在map中添加或者删除就行了,不需要改其他地方,这不正是开发闭合原则吗。
再比如,依赖倒置原则要求我们尽量依赖接口、依赖抽象,而不是具体的对象。
综合上面所有的例子,List、Map以及接口,它们的共同点都是可变。
所以,我们要面向可变编程,这就是易变性思想。
这跟依赖倒置原则有啥区别呢?
依赖倒置原则只描述了对象之间的关系,而我们的代码中,除了对象还有函数,还有变量,还有模块等,而这些都可以用易变性思想来描述,或者说:易变性思想是宏观层面的依赖倒置原则。
其实,易变性思想也可以叫做面向拓展编程,或者叫面向未来编程。
代码越容易改变,将来发生改变的时候就越容易修改甚至不需要修改,这就是易变性带来的好处。