設(shè)計(jì)模式六大原則之【單一職則原則】
一库物、什么是單一職責(zé)原則
首先, 我們來看單一職責(zé)的定義.
單一職責(zé)原則,全稱Single Responsibility Principle, 簡稱SRP.
A class should have only one reason to change
類發(fā)生更改的原因應(yīng)該只有一個(gè)
就一個(gè)類而言,應(yīng)該僅有一個(gè)引起它變化的原因颊亮。應(yīng)該只有一個(gè)職責(zé)晌梨。如果一個(gè)類有一個(gè)以上的職責(zé)豆茫,這些職責(zé)就耦合在了一起伸蚯。一個(gè)職責(zé)的變化可能會(huì)削弱或者抑制這個(gè)類完成其他職責(zé)的能力撬码。這會(huì)導(dǎo)致脆弱的設(shè)計(jì)。當(dāng)一個(gè)職責(zé)發(fā)生變化時(shí)掰吕,可能會(huì)影響其它的職責(zé)果覆。另外,多個(gè)職責(zé)耦合在一起殖熟,會(huì)影響復(fù)用性局待。想要避免這種現(xiàn)象的發(fā)生,就要盡可能的遵守單一職責(zé)原則吗讶。
單一職責(zé)原則的核心就是解耦和增強(qiáng)內(nèi)聚性燎猛。
二、為什么要遵守單一職責(zé)原則照皆?
通常 , 我們做事情都要知道為什么要這么做, 才回去做. 做的也有底氣, 那么為什么我們要使用單一職責(zé)原則呢?
1重绷、提高類的可維護(hù)性和可讀寫性
一個(gè)類的職責(zé)少了,復(fù)雜度降低了膜毁,代碼就少了昭卓,可讀性也就好了,可維護(hù)性自然就高了瘟滨。
2候醒、提高系統(tǒng)的可維護(hù)性
系統(tǒng)是由類組成的,每個(gè)類的可維護(hù)性高杂瘸,相對(duì)來講整個(gè)系統(tǒng)的可維護(hù)性就高倒淫。當(dāng)然,前提是系統(tǒng)的架構(gòu)沒有問題败玉。
3敌土、降低變更的風(fēng)險(xiǎn)
一個(gè)類的職責(zé)越多,變更的可能性就越大运翼,變更帶來的風(fēng)險(xiǎn)也就越大
如果在一個(gè)類中可能會(huì)有多個(gè)發(fā)生變化的東西返干,這樣的設(shè)計(jì)會(huì)帶來風(fēng)險(xiǎn), 我們盡量保證只有一個(gè)可以變化,其他變化的就放在其他類中血淌,這樣的好處就是 ** 提高內(nèi)聚矩欠,降低耦合 **。
三. 單一職責(zé)原則應(yīng)用的范圍
單一職責(zé)原則適用的范圍有接口悠夯、方法癌淮、類。按大家的說法疗疟,接口和方法必須保證單一職責(zé)该默,類就不必保證,只要符合業(yè)務(wù)就行策彤。
3.1 【方法層面】單一職責(zé)原則的應(yīng)用
現(xiàn)在有一個(gè)場(chǎng)景, 需要修改用戶的用戶名和密碼. 就針對(duì)這個(gè)功能我們可以有多種實(shí)現(xiàn).
第一種:
/**
* 操作的類型
*/
public enum OperateEnum {
UPDATE_USERNAME,
UPDATE_PASSWORD;
}
public interface UserOperate {
void updateUserInfo(OperateEnum type, UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void updateUserInfo(OperateEnum type, UserInfo userInfo) {
if (type == OperateEnum.UPDATE_PASSWORD) {
// 修改密碼
} else if(type == OperateEnum.UPDATE_USERNAME) {
// 修改用戶名
}
}
}
第二種方法:
public interface UserOperate {
void updateUserName(UserInfo userInfo);
void updateUserPassword(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate {
@Override
public void updateUserName(UserInfo userInfo) {
// 修改用戶名邏輯
}
@Override
public void updateUserPassword(UserInfo userInfo) {
// 修改密碼邏輯
}
}
來看看這兩種實(shí)現(xiàn)的區(qū)別:
第一種實(shí)現(xiàn)是根據(jù)操作類型進(jìn)行區(qū)分, 不同類型執(zhí)行不同的邏輯. 把修改用戶名和修改密碼這兩件事耦合在一起了. 如果客戶端在操作的時(shí)候傳錯(cuò)了類型, 那么就會(huì)發(fā)生錯(cuò)誤.
第二種實(shí)現(xiàn)是我們推薦的實(shí)現(xiàn)方式. 修改用戶名和修改密碼邏輯分開. 各自執(zhí)行各自的職責(zé), 互不干擾. 功能清晰明了.
由此可見, 第二種設(shè)計(jì)是符合單一職責(zé)原則的. 這是在方法層面實(shí)現(xiàn)單一職責(zé)原則.
3.2 【接口層面】單一職責(zé)原則的應(yīng)用
我們假設(shè)一個(gè)場(chǎng)景, 大家一起做家務(wù), 張三掃地, 李四買菜. 李四買完菜回來還得做飯. 這個(gè)邏輯怎么實(shí)現(xiàn)呢?
方式一
/**
* 做家務(wù)
*/
public interface HouseWork {
// 掃地
void sweepFloor();
// 購物
void shopping();
}
public class Zhangsan implements HouseWork{
@Override
public void sweepFloor() {
// 掃地
}
@Override
public void shopping() {
}
}
public class Lisi implements HouseWork{
@Override
public void sweepFloor() {
}
@Override
public void shopping() {
// 購物
}
}
首先定義了一個(gè)做家務(wù)的接口, 定義兩個(gè)方法掃地和買菜. 張三掃地, 就實(shí)現(xiàn)掃地接口. 李四買菜, 就實(shí)現(xiàn)買菜接口. 然后李四買完菜回來還要做飯, 于是就要在接口類中增加一個(gè)方法cooking. 張三和李四都重寫這個(gè)方法, 但只有李四有具體實(shí)現(xiàn).
這樣設(shè)計(jì)本身就是不合理的.
首先: 張三只掃地, 但是他需要重寫買菜方法, 李四不需要掃地, 但是李四也要重寫掃地方法.
第二: 這也不符合開閉原則. 增加一種類型做飯, 要修改3個(gè)類. 這樣當(dāng)邏輯很復(fù)雜的時(shí)候, 很容易引起意外錯(cuò)誤.
上面這種設(shè)計(jì)不符合單一職責(zé)原則, 修改一個(gè)地方, 影響了其他不需要修改的地方.
方法二
/**
* 做家務(wù)
*/
public interface Hoursework {
}
public interface Shopping extends Hoursework{
// 購物
void shopping();
}
public interface SweepFloor extends Hoursework{
// 掃地
void sweepFlooring();
}
public class Zhangsan implements SweepFloor{
@Override
public void sweepFlooring() {
// 張三掃地
}
}
public class Lisi implements Shopping{
@Override
public void shopping() {
// 李四購物
}
}
上面做家務(wù)不是定義成一個(gè)接口, 而是將掃地和做家務(wù)分開了. 張三掃地, 那么張三就實(shí)現(xiàn)掃地的接口. 李四購物, 李四就實(shí)現(xiàn)購物的接口. 后面李四要增加一個(gè)功能做飯. 那么就新增一個(gè)做飯接口, 這次只需要李四實(shí)現(xiàn)做飯接口就可以了.
public interface Cooking extends Hoursework{
void cooking();
}
public class Lisi implements Shopping, Cooking{
@Override
public void shopping() {
// 李四購物
}
@Override
public void cooking() {
// 李四做飯
}
}
如上, 我們看到張三沒有實(shí)現(xiàn)多余的接口, 李四也沒有. 而且當(dāng)新增功能的時(shí)候, 只影響了李四, 并沒有影響張三.
這就是符合單一職責(zé)原則. 一個(gè)類只做一件事. 并且他的修改不會(huì)帶來其他的變化.
3.3 【類層面】單一職責(zé)原則的應(yīng)用
從類的層面來講, 沒有辦法完全按照單一職責(zé)原來來拆分. 換種說法, 類的職責(zé)可大可小, 不想接口那樣可以很明確的按照單一職責(zé)原則拆分. 只要符合邏輯有道理即可.
比如, 我們?cè)诰W(wǎng)站首頁可以注冊(cè), 登錄, 微信登錄.注冊(cè)登錄等操作. 我們通常的做法是:
public interface UserOperate {
void login(UserInfo userInfo);
void register(UserInfo userInfo);
void logout(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void login(UserInfo userInfo) {
// 用戶登錄
}
@Override
public void register(UserInfo userInfo) {
// 用戶注冊(cè)
}
@Override
public void logout(UserInfo userInfo) {
// 用戶登出
}
}
那如果按照單一職責(zé)原則拆分, 也可以拆分為下面的形式
public interface Register {
void register();
}
public interface Login {
void login();
}
public interface Logout {
void logout();
}
public class RegisterImpl implements Register{
@Override
public void register() {
}
}
public class LoginImpl implements Login{
@Override
public void login() {
// 用戶登錄
}
}
public class LogoutImpl implements Logout{
@Override
public void logout() {
}
}
像上面這樣寫可不可以呢? 其實(shí)也可以, 就是類很多. 如果登錄栓袖、注冊(cè)匣摘、注銷操作代碼很多, 那么可以這么寫.
四、如何遵守單一職責(zé)原則
4.1 合理的職責(zé)分解
相同的職責(zé)放到一起裹刮,不同的職責(zé)分解到不同的接口和實(shí)現(xiàn)中去音榜,這個(gè)是最容易也是最難運(yùn)用的原則,關(guān)鍵還是要從業(yè)務(wù)出發(fā)捧弃,從需求出發(fā)赠叼,識(shí)別出同一種類型的職責(zé)。
例子:人的行為分析违霞,包括了生活和工作等行為的分析嘴办,生活行為包括吃、跑买鸽、睡等行為涧郊,工作行為包括上下班,開會(huì)等行為眼五,如下圖所示:
<image src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b2e0fea47ec4264aeb760aeb8aef76d~tplv-k3u1fbpfcp-zoom-1.image" width="500px">
人類的行為分成了兩個(gè)接口:生活行為接口妆艘、工作行為接口,以及兩個(gè)實(shí)現(xiàn)類看幼。如果都用一個(gè)實(shí)現(xiàn)類來承擔(dān)這兩個(gè)接口的職責(zé)批旺,就會(huì)導(dǎo)致代碼臃腫,不易維護(hù)诵姜,如果以后再加上其他行為汽煮,例如學(xué)習(xí)行為接口,將會(huì)產(chǎn)生變更風(fēng)險(xiǎn)(這里還用到了組合模式)棚唆。
4.2 來看看簡單的代碼實(shí)現(xiàn)
第一步: 定義一個(gè)行為接口
/**
* 人的行為
* 人的行為包括兩種: 生活行為, 工作行為
*/
public interface IBehavior {
}
這里面定義了一個(gè)空的接口, 行為接口. 具體這個(gè)行為接口下面有哪些接口呢?有生活和工作兩方面的行為.
第二步: 定義生活和工作接口, 并且他們都是行為接口的子類
生活行為接口:
public interface LivingBehavior extends IBehavior{
/** 吃飯 */
void eat();
/** 跑步 */
void running();
/** 睡覺 */
void sleeping();
}
工作行為接口:
public interface WorkingBehavior extends IBehavior{
/** 上班 */
void goToWork();
/** 下班 */
void goOffWork();
/** 開會(huì) */
void meeting();
}
第三步: 定義工作行為接口和生活行為接口的實(shí)現(xiàn)類
生活行為接口實(shí)現(xiàn)類:
public class LivingBehaviorImpl implements LivingBehavior{
@Override
public void eat() {
System.out.println("吃飯");
}
@Override
public void running() {
System.out.println("跑步");
}
@Override
public void sleeping() {
System.out.println("睡覺");
}
}
工作行為接口實(shí)現(xiàn)類:
public class WorkingBehaviorImpl implements WorkingBehavior{
@Override
public void goToWork() {
System.out.println("上班");
}
@Override
public void goOffWork() {
System.out.println("下班");
}
@Override
public void meeting() {
System.out.println("開會(huì)");
}
}
第四步: 行為組合調(diào)用.
行為接口定義好了. 接下來會(huì)定義一個(gè)行為集合. 不同的用戶擁有的行為是不一樣 , 有的用戶只用生活行為, 有的用戶既有生活行為又有工作行為.ewgni
我們并不知道具體用戶到底會(huì)有哪些行為, 所以,通常使用一個(gè)集合來接收用戶的行為. 用戶有哪些行為, 就往里面添加哪些行為.
1. 行為組合接口BehaviorComposer
public interface BehaviorComposer {
void add(IBehavior behavior);
}
2. 行為組合接口實(shí)現(xiàn)類IBehaviorComposerImpl
public class IBehaviorComposerImpl implements BehaviorComposer {
private List<IBehavior> behaviors = new ArrayList<>();
@Override
public void add(IBehavior behavior) {
System.out.println("添加行為");
behaviors.add(behavior);
}
public void doSomeThing() {
behaviors.forEach(b->{
if(b instanceof LivingBehavior) {
LivingBehavior li = (LivingBehavior)b;
// 處理生活行為
} else if(b instanceof WorkingBehavior) {
WorkingBehavior wb = (WorkingBehavior) b;
// 處理工作行為
}
});
}
}
第五步: 客戶端調(diào)用
用戶在調(diào)用的時(shí)候, 根據(jù)實(shí)際情況調(diào)用就可以了, 比如下面的代碼: 張三是全職媽媽, 只有生活行為, 李四是職場(chǎng)媽媽, 既有生活行為又有工作行為.
public static void main(String[] args) {
// 張三--全職媽媽
LivingBehavior zslivingBehavior = new LivingBehaviorImpl();
BehaviorComposer zsBehaviorComposer = new IBehaviorComposerImpl();
zsBehaviorComposer.add(zslivingBehavior);
// 李四--職場(chǎng)媽媽
LivingBehavior lsLivingBehavior = new LivingBehaviorImpl();
WorkingBehavior lsWorkingBehavior = new WorkingBehaviorImpl();
BehaviorComposer lsBehaviorComposer = new IBehaviorComposerImpl();
lsBehaviorComposer.add(lsLivingBehavior);
lsBehaviorComposer.add(lsWorkingBehavior);
}
可以看出單一職責(zé)的好處.
五逗物、單一職責(zé)原則的優(yōu)缺點(diǎn)
類的復(fù)雜性降低: 一個(gè)類實(shí)現(xiàn)什么職責(zé)都有清晰明確的定義了, 復(fù)雜性自然就降低了
可讀性提高: 復(fù)雜性降低了,可讀性自然就提高了
可維護(hù)性提高: 可讀性提高了瑟俭,代碼就更容易維護(hù)了
變更引起的風(fēng)險(xiǎn)降低: 變更是必不可少的,如果接口的單一職責(zé)做得好契邀,一個(gè)接口修改只對(duì)相應(yīng)的實(shí)現(xiàn)類有影響摆寄,對(duì)其他的接口和類無影響,這對(duì)系統(tǒng)的擴(kuò)展性坯门、維護(hù)性都有非常大的幫助
四微饥、最佳實(shí)踐
https://baijiahao.baidu.com/s?id=1646244620500787383&wfr=spider&for=pc