單一職責(zé)(SRP)
-
如何理解單一職責(zé)原則(SRP)?
單一職責(zé)原則的英文是 Single Responsibility Principle我磁,縮寫為 SRP。這個原則的英文描述是這樣的:A class or module should have a single responsibility驻襟。如果我們把它翻譯成中文夺艰,那就是:一個類或者模塊只負責(zé)完成一個職責(zé)(或者功能)。
注意沉衣,這個原則描述的對象包含兩個郁副,一個是類(class),一個是模塊(module)豌习。關(guān)于這兩個概念存谎,有兩種理解方式。一種理解是:把模塊看作比類更加抽象的概念肥隆,類也可以看作模塊愕贡。另一種理解是:把模塊看作比類更加粗粒度的代碼塊,模塊中包含多個類巷屿,多個類組成一個模塊固以,不管哪種理解道理是想通的,下面以類作為分析對象,模塊自行引申即可憨琳。
一個類只負責(zé)完成一個職責(zé)或者功能诫钓。不要設(shè)計大而全的類,要設(shè)計粒度小篙螟、功能單一的類菌湃。單一職責(zé)原則是為了實現(xiàn)代碼高內(nèi)聚、低耦合遍略,提高代碼的復(fù)用性惧所、可讀性、可維護性绪杏。
如何判斷類的職責(zé)是否足夠單一下愈?
不同的應(yīng)用場景、不同階段的需求背景蕾久、不同的業(yè)務(wù)層面势似,對同一個類的職責(zé)是否單一,可能會有不同的判定結(jié)果僧著。所以我們可以先寫一個粗粒度的類履因,滿足業(yè)務(wù)需求。隨著業(yè)務(wù)的發(fā)展盹愚,如果粗粒度的類越來越龐大栅迄,代碼越來越多,這個時候皆怕,我們就可以將這個粗粒度的類毅舆,拆分成幾個更細粒度的類(持續(xù)重構(gòu))。
實際上端逼,一些側(cè)面的判斷指標更具有指導(dǎo)意義和可執(zhí)行性,比如污淋,出現(xiàn)下面這些情況就有可能說明這類的設(shè)計不滿足單一職責(zé)原則:
- 類中的代碼行數(shù)顶滩、函數(shù)或者屬性過多;
- 類依賴的其他類過多寸爆,或者依賴類的其他類過多礁鲁;
- 私有方法過多;
- 比較難給類起一個合適的名字赁豆;
- 類中大量的方法都是集中操作類中的某幾個屬性仅醇。
/**
* UserInfo類
*
* 該類是否滿足單一職責(zé)?
*
* 分析問題要結(jié)合實際的應(yīng)用場景:如果在這個社交產(chǎn)品中魔种,用戶的地址信息跟其他信息一樣析二,只是單純地用來展示,那 UserInfo 現(xiàn)在的設(shè)計就是合理的。
* 但是叶摄,如果這個社交產(chǎn)品發(fā)展得比較好属韧,之后又在產(chǎn)品中添加了電商的模塊,用戶的地址信息還會用在電商物流中蛤吓,那我們最好將地址信息從 UserInfo 中拆分出來宵喂,獨立成用戶物流信息(或者叫地址信息、收貨信息等)会傲。
*
*/
@Getter
@Setter
public class UserInfo {
private long userId;
private String username;
private String email;
private String telephone;
private long createTime;
private long lastLoginTime;
private String avatarUrl;
private String provinceOfAddress; // 省
private String cityOfAddress; // 市
private String regionOfAddress; // 區(qū)
private String detailedAddress; // 詳細地址
}
-
類的職責(zé)是否設(shè)計得越單一越好锅棕?
單一職責(zé)原則通過避免設(shè)計大而全的類,避免將不相關(guān)的功能耦合在一起淌山,來提高類的內(nèi)聚性裸燎。同時,類職責(zé)單一艾岂,類依賴的和被依賴的其他類也會變少顺少,減少了代碼的耦合性,以此來實現(xiàn)代碼的高內(nèi)聚王浴、低耦合脆炎。但是,如果拆分得過細氓辣,實際上會適得其反秒裕,反倒會降低內(nèi)聚性,也會影響代碼的可維護性钞啸。
/**
* Serialization類
*
* 拆分過度問題:以序列化為例經(jīng)過拆分之后几蜻,Serializer 類和 Deserializer 類的職責(zé)更加單一了,
* 但也隨之帶來了新的問題体斩。如果我們修改了協(xié)議的格式梭稚,數(shù)據(jù)標識從“UEUEUE”改為“DFDFDF”,或者序列
* 化方式從 JSON 改為了 XML絮吵,那 Serializer 類和 Deserializer 類都需要做相應(yīng)的修改弧烤,代碼的
* 內(nèi)聚性顯然沒有原來 Serialization 高了。而且蹬敲,如果我們僅僅對 Serializer 類做了協(xié)議修改暇昂,而
* 忘記了修改 Deserializer 類的代碼,那就會導(dǎo)致序列化伴嗡、反序列化不匹配急波,程序運行出錯,也就是說瘪校,
* 拆分之后澄暮,代碼的可維護性變差了。
*
*
*/
public class Serialization {
private static final String IDENTIFIER_STRING = "UEUEUE;";
public String serialze(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
textBuilder.append(JSON.toJSONString(object));
return textBuilder.toString();
}
public Map<String, String> deserialize(String text){
if(!text.startsWith(IDENTIFIER_STRING)){
return Collections.emptyMap();
}
text = text.substring(IDENTIFIER_STRING.length());
return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
}
}
public class Serializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
public String serialze(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
textBuilder.append(JSON.toJSONString(object));
return textBuilder.toString();
}
}
public class Deserializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
public Map<String, String> deserialize(String text){
if(!text.startsWith(IDENTIFIER_STRING)){
return Collections.emptyMap();
}
text = text.substring(IDENTIFIER_STRING.length());
return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
}
}
接口隔離原則(ISP)
-
如何理解“接口隔離原則”?
接口隔離原則的英文翻譯是“ Interface Segregation Principle”赏寇,縮寫為 ISP吉嫩。Robert Martin 在 SOLID 原則中是這樣定義它的:“Clients should not be forced to depend upon interfaces that they do not use⌒岫ǎ”直譯成中文的話就是:客戶端不應(yīng)該被強迫依賴它不需要的接口自娩。其中的“客戶端”,可以理解為接口的調(diào)用者或者使用者渠退。
理解“接口隔離原則”的重點是理解其中的“接口”二字忙迁。這里有三種不同的理解。
- 如果把“接口”理解為一組接口集合碎乃,可以是某個微服務(wù)的接口姊扔,也可以是某個類庫的接口等。如果部分接口只被部分調(diào)用者使用梅誓,我們就需要將這部分接口隔離出來恰梢,單獨給這部分調(diào)用者使用,而不強迫其他調(diào)用者也依賴這部分不會被用到的接口梗掰。
/** * UserService接口 * * 場景:用戶系統(tǒng)提供了一組跟用戶相關(guān)的 API 給其他系統(tǒng)使用嵌言,比如:注冊、登錄及穗、獲取用戶信息等〈蒈睿現(xiàn)在, * 我們的后臺管理系統(tǒng)要實現(xiàn)刪除用戶的功能埂陆,希望用戶系統(tǒng)提供一個刪除用戶的接口苛白。這個時候我們該如何來做呢? * * 分析:方案一:在 UserService 中新添加一個 deleteUserByCellphone() 或 deleteUserById() 接口就可以了焚虱。 * 這個方法可以解決問題购裙,但是也隱藏了一些安全隱患,刪除用戶是一個非常慎重的操作鹃栽,我們只希望通過后臺管理系統(tǒng)來執(zhí)行躏率, * 所以這個接口只限于給后臺管理系統(tǒng)使用,如果在沒有鑒權(quán)的情況下谍咆,加限制地被其他業(yè)務(wù)系統(tǒng)調(diào)用禾锤,就有可能導(dǎo)致誤刪用戶私股。 * * 方案二:在沒有鑒權(quán)情況下可以從代碼層面規(guī)避上述風(fēng)險,具體可以參照接口隔離原則摹察,調(diào)用者不應(yīng)該強迫依賴它不需要的接口, * 將刪除接口單獨放到另外一個接口 RestrictedUserService 中倡鲸,然后將 RestrictedUserService 只打包提供給后臺 * 管理系統(tǒng)來使用供嚎。 * */ public interface UserService { boolean register(String cellphone, String password); boolean login(String cellphone, String password); UserInfo getUserInfoById(long id); UserInfo getUserInfoByCellphone(String cellphone); } public interface RestrictedUserService { boolean deleteUserByCellphone(String cellphone); boolean deleteUserById(long id); } public class BackgroundUserServiceImpl implements UserService, RestrictedUserService { @Override public boolean deleteUserByCellphone(String cellphone) { return false; } @Override public boolean deleteUserById(long id) { return false; } @Override public boolean register(String cellphone, String password) { return false; } @Override public boolean login(String cellphone, String password) { return false; } @Override public UserInfo getUserInfoById(long id) { return null; } @Override public UserInfo getUserInfoByCellphone(String cellphone) { return null; } }
- 如果把“接口”理解為單個 API 接口或函數(shù),函數(shù)的設(shè)計要功能單一,不要將多個不同的功能邏輯在一個函數(shù)中實現(xiàn)克滴。部分調(diào)用者只需要函數(shù)中的部分功能逼争,那我們就需要把函數(shù)拆分成粒度更細的多個函數(shù),讓調(diào)用者只依賴它需要的那個細粒度函數(shù)劝赔。
/** * Statistics類 * * 接口設(shè)計分析:count() 函數(shù)的功能不夠單一誓焦,包含很多不同的統(tǒng)計功能,比如着帽,求最大值杂伟、最小值、平均值等 * 場景一:如果在項目中仍翰,對每個統(tǒng)計需求赫粥,Statistics 定義的那幾個統(tǒng)計信息都有涉及,那 count() * 函數(shù)的設(shè)計就是合理的予借。 * * 場景二:如果每個統(tǒng)計需求只涉及 Statistics 羅列的統(tǒng)計信息中一部分越平,比如,有的只需要用到 max灵迫、 * min秦叛、average 這三類統(tǒng)計信息,在這個應(yīng)用場景下龟再,count() 函數(shù)的設(shè)計就有點不合理了书闸,這種場景下 * 需要將其拆分成粒度更細的多個統(tǒng)計函數(shù)。 * * 總結(jié):ISP提供了一種判斷接口是否職責(zé)單一的標準:通過調(diào)用者如何使用接口來間接地判定利凑。如果調(diào)用者只 * 使用部分接口或接口的部分功能浆劲,那接口的設(shè)計就不夠職責(zé)單一。 * */ @Getter public class Statistics { private Long max; private Long min; private Long average; private Long sum; private Long percentile99; private Long percentile999; /** * 場景一下合理 */ public Statistics count(Collection<Long> dataSet) { Statistics statistics = new Statistics(); //求最大值 statistics.setMax(2L); // 最小值 statistics.setMin(0L); // 平均值 statistics.setAverage(1L); return statistics; } /** * 場景二下合理 * * @param dataSet * @return */ public Long max(Collection<Long> dataSet) { return 2L; } public Long min(Collection<Long> dataSet) { return 0L; } public Long average(Collection<Long> dataSet) { return 1L; } public void setMax(Long max) { this.max = max; } public void setMin(Long min) { this.min = min; } public void setAverage(Long average) { this.average = average; } }
- 如果把“接口”理解為 面向?qū)ο缶幊?OOP) 中的接口哀澈,也可以理解為面向?qū)ο缶幊陶Z言中的接口語法牌借。那接口的設(shè)計要盡量單一,不要讓接口的實現(xiàn)類和調(diào)用者割按,依賴不需要的接口函數(shù)膨报。
/** * Application類 * * 背景:假設(shè)我們的項目中用到了三個外部系統(tǒng):Redis、MySQL适荣、Kafka现柠。每個系統(tǒng)都對應(yīng)一系列配置信息, * 比如地址弛矛、端口够吩、訪問超時時間等。為了在內(nèi)存中存儲這些配置信息丈氓,供項目中的其他模塊來使用周循,我 * 們分別設(shè)計實現(xiàn)了三個 Configuration 類:RedisConfig强法、MysqlConfig、KafkaConfig * * 需求: * 1.希望支持 Redis 和 Kafka 配置信息的熱更新湾笛。所謂“熱更新(hot update)”就是饮怯,如果在配 * 置中心中更改了配置信息,我們希望在不用重啟系統(tǒng)的情況下嚎研,能將最新的配置信息加載到內(nèi)存中(也就 * 是 RedisConfig蓖墅、KafkaConfig 類中)。 * * 2.監(jiān)控功能需求临扮。通過命令行來查看 Zookeeper 中的配置信息是比較麻煩的置媳。所以,我們希望能有一 * 種更加方便的配置信息查看方式公条。我們可以在項目中開發(fā)一個內(nèi)嵌的 SimpleHttpServer拇囊,輸出項目的 * 配置信息到一個固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 靶橱。我們只需要在瀏覽 * 器中輸入這個地址寥袭,就可以顯示出系統(tǒng)的配置信息。不過关霸,出于某些原因传黄,我們只想暴露 MySQL 和 Redis * 的配置信息。 * */ public class Application { private static ConfigSource configSource = new ZookeerConfigSource(); private static final RedisConfig redisConfig = new RedisConfig(configSource); private static final KafkaConfig kafkaConfig = new KafkaConfig(configSource); private static final MysqlConfig mySqlConfig = new MysqlConfig(configSource); public static void main(String[] args) { ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig,300,300); redisConfigUpdater.run(); ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig,60,60); kafkaConfigUpdater.run(); SimpleHttpServer simpleHttpServer = new SimpleHttpServer("127.0.0.1",2389); simpleHttpServer.addViewer("/config",redisConfig); simpleHttpServer.addViewer("/config",mySqlConfig); } } /** * Updater熱更新接口 */ public interface Updater { /** * 熱部署,從configSource加載配置到address/timeout/maxTotal */ void update(); } /** * Viewer監(jiān)控接口 */ public interface Viewer { /** * 監(jiān)控-輸出文本信息 */ String outputInPlainText(); /** * 監(jiān)控-輸出監(jiān)控項 */ Map<String,String> output(); } //接口處理類 public class ScheduledUpdater { private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private long initialDelayInSeconds; private long periodInSeconds; private Updater updater; public ScheduledUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) { this.initialDelayInSeconds = initialDelayInSeconds; this.periodInSeconds = periodInSeconds; this.updater = updater; } public void run(){ executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { updater.update(); } },this.initialDelayInSeconds,this.periodInSeconds, TimeUnit.SECONDS); } } public class SimpleHttpServer { private String host; private int port; private Map<String, List<Viewer>> viewerMap = new HashMap<>(); public SimpleHttpServer(String host, int port) { this.host = host; this.port = port; } public void addViewer(String urlDirectory, Viewer viewer) { if (!viewerMap.containsKey(urlDirectory)) { viewerMap.put(urlDirectory, new ArrayList<Viewer>()); } viewerMap.get(urlDirectory).add(viewer); } public void run(){ // 輸出項目的配置信息到一個固定的 HTTP 地址 // 比如:http://127.0.0.1:2389/config 队寇。 // 我們只需要在瀏覽器中輸入這個地址膘掰,就可以顯示出系統(tǒng)的配置信息。 } } //config配置類 @Getter public abstract class AbstractConfig { /** * 配置中心(比如zookeeper) */ protected ConfigSource configSource; protected String address; protected int timeout; protected int maxTotal; } public class RedisConfig extends AbstractConfig implements Updater, Viewer { public RedisConfig(ConfigSource configSource) { super(); super.configSource = configSource; } /** * 熱部署佳遣,從configSource加載配置到address/timeout/maxTotal */ @Override public void update() { super.address = configSource.getAddress(); super.timeout = configSource.getTimeout(); super.maxTotal = configSource.getMaxTotal(); } /** * 監(jiān)控-輸出文本信息 */ @Override public String outputInPlainText() { return JSON.toJSONString(this); } /** * 監(jiān)控-輸出監(jiān)控項 */ @Override public Map<String, String> output() { return JSON.parseObject(this.outputInPlainText(), new TypeReference<HashMap<String, String>>(){}); } } public class MysqlConfig extends AbstractConfig implements Viewer { public MysqlConfig(ConfigSource configSource) { super(); super.configSource = configSource; } @Override public String outputInPlainText() { return JSON.toJSONString(this); } @Override public Map<String, String> output() { return JSON.parseObject(this.outputInPlainText(), new TypeReference<HashMap<String, String>>(){}); } } public class KafkaConfig extends AbstractConfig implements Updater { public KafkaConfig(ConfigSource configSource) { super(); super.configSource = configSource; } @Override public void update() { super.address = configSource.getAddress(); super.timeout = configSource.getTimeout(); super.maxTotal = configSource.getMaxTotal(); } }
-
接口隔離原則與單一職責(zé)原則的區(qū)別
單一職責(zé)原則針對的是模塊识埋、類、接口的設(shè)計零渐。接口隔離原則相對于單一職責(zé)原則窒舟,一方面更側(cè)重于接口的設(shè)計,另一方面它的思考角度也是不同的诵盼。接口隔離原則提供了一種判斷接口的職責(zé)是否單一的標準:通過調(diào)用者如何使用接口來間接地判定惠豺。如果調(diào)用者只使用部分接口或接口的部分功能,那接口的設(shè)計就不夠職責(zé)單一风宁。