2022-01-01設(shè)計(jì)原則--單一職責(zé)與接口隔離原則總結(jié)

單一職責(zé)(SRP)

  • 如何理解單一職責(zé)原則(SRP)?

    單一職責(zé)原則的英文是 Single Responsibility Principle,縮寫為 SRP。這個(gè)原則的英文描述是這樣的:A class or module should have a single responsibility。如果我們把它翻譯成中文,那就是:一個(gè)類或者模塊只負(fù)責(zé)完成一個(gè)職責(zé)(或者功能)。

    注意,這個(gè)原則描述的對(duì)象包含兩個(gè),一個(gè)是類(class),一個(gè)是模塊(module)。關(guān)于這兩個(gè)概念,有兩種理解方式。一種理解是:把模塊看作比類更加抽象的概念,類也可以看作模塊。另一種理解是:把模塊看作比類更加粗粒度的代碼塊,模塊中包含多個(gè)類,多個(gè)類組成一個(gè)模塊,不管哪種理解道理是想通的,下面以類作為分析對(duì)象,模塊自行引申即可。

    一個(gè)類只負(fù)責(zé)完成一個(gè)職責(zé)或者功能。不要設(shè)計(jì)大而全的類,要設(shè)計(jì)粒度小、功能單一的類。單一職責(zé)原則是為了實(shí)現(xiàn)代碼高內(nèi)聚、低耦合,提高代碼的復(fù)用性、可讀性、可維護(hù)性。

  • 如何判斷類的職責(zé)是否足夠單一?

不同的應(yīng)用場景、不同階段的需求背景、不同的業(yè)務(wù)層面,對(duì)同一個(gè)類的職責(zé)是否單一,可能會(huì)有不同的判定結(jié)果。所以我們可以先寫一個(gè)粗粒度的類,滿足業(yè)務(wù)需求。隨著業(yè)務(wù)的發(fā)展,如果粗粒度的類越來越龐大,代碼越來越多,這個(gè)時(shí)候,我們就可以將這個(gè)粗粒度的類,拆分成幾個(gè)更細(xì)粒度的類(持續(xù)重構(gòu))。

實(shí)際上,一些側(cè)面的判斷指標(biāo)更具有指導(dǎo)意義和可執(zhí)行性,比如,出現(xiàn)下面這些情況就有可能說明這類的設(shè)計(jì)不滿足單一職責(zé)原則:

  1. 類中的代碼行數(shù)、函數(shù)或者屬性過多;
  2. 類依賴的其他類過多,或者依賴類的其他類過多;
  3. 私有方法過多;
  4. 比較難給類起一個(gè)合適的名字;
  5. 類中大量的方法都是集中操作類中的某幾個(gè)屬性。
/**
 * UserInfo類
 *
 * 該類是否滿足單一職責(zé)?
 *
 * 分析問題要結(jié)合實(shí)際的應(yīng)用場景:如果在這個(gè)社交產(chǎn)品中,用戶的地址信息跟其他信息一樣,只是單純地用來展示,那 UserInfo 現(xiàn)在的設(shè)計(jì)就是合理的。
 * 但是,如果這個(gè)社交產(chǎn)品發(fā)展得比較好,之后又在產(chǎn)品中添加了電商的模塊,用戶的地址信息還會(huì)用在電商物流中,那我們最好將地址信息從 UserInfo 中拆分出來,獨(dú)立成用戶物流信息(或者叫地址信息、收貨信息等)。
 *
 */
@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; // 詳細(xì)地址

}
  • 類的職責(zé)是否設(shè)計(jì)得越單一越好?

    單一職責(zé)原則通過避免設(shè)計(jì)大而全的類,避免將不相關(guān)的功能耦合在一起,來提高類的內(nèi)聚性。同時(shí),類職責(zé)單一,類依賴的和被依賴的其他類也會(huì)變少,減少了代碼的耦合性,以此來實(shí)現(xiàn)代碼的高內(nèi)聚、低耦合。但是,如果拆分得過細(xì),實(shí)際上會(huì)適得其反,反倒會(huì)降低內(nèi)聚性,也會(huì)影響代碼的可維護(hù)性。

/**
 * Serialization類
 *
 * 拆分過度問題:以序列化為例經(jīng)過拆分之后,Serializer 類和 Deserializer 類的職責(zé)更加單一了,
 * 但也隨之帶來了新的問題。如果我們修改了協(xié)議的格式,數(shù)據(jù)標(biāo)識(shí)從“UEUEUE”改為“DFDFDF”,或者序列
 * 化方式從 JSON 改為了 XML,那 Serializer 類和 Deserializer 類都需要做相應(yīng)的修改,代碼的
 * 內(nèi)聚性顯然沒有原來 Serialization 高了。而且,如果我們僅僅對(duì) Serializer 類做了協(xié)議修改,而
 * 忘記了修改 Deserializer 類的代碼,那就會(huì)導(dǎo)致序列化、反序列化不匹配,程序運(yùn)行出錯(cuò),也就是說,
 * 拆分之后,代碼的可維護(hù)性變差了。
 *
 *
 */
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)

  1. 如何理解“接口隔離原則”?

    接口隔離原則的英文翻譯是“ Interface Segregation Principle”,縮寫為 ISP。Robert Martin 在 SOLID 原則中是這樣定義它的:“Clients should not be forced to depend upon interfaces that they do not use。”直譯成中文的話就是:客戶端不應(yīng)該被強(qiáng)迫依賴它不需要的接口。其中的“客戶端”,可以理解為接口的調(diào)用者或者使用者。

    理解“接口隔離原則”的重點(diǎn)是理解其中的“接口”二字。這里有三種不同的理解。

    • 如果把“接口”理解為一組接口集合,可以是某個(gè)微服務(wù)的接口,也可以是某個(gè)類庫的接口等。如果部分接口只被部分調(diào)用者使用,我們就需要將這部分接口隔離出來,單獨(dú)給這部分調(diào)用者使用,而不強(qiáng)迫其他調(diào)用者也依賴這部分不會(huì)被用到的接口。
    /**
     * UserService接口
     *
     *  場景:用戶系統(tǒng)提供了一組跟用戶相關(guān)的 API 給其他系統(tǒng)使用,比如:注冊(cè)、登錄、獲取用戶信息等?,F(xiàn)在,
     *  我們的后臺(tái)管理系統(tǒng)要實(shí)現(xiàn)刪除用戶的功能,希望用戶系統(tǒng)提供一個(gè)刪除用戶的接口。這個(gè)時(shí)候我們?cè)撊绾蝸碜瞿兀? *
     *  分析:方案一:在 UserService 中新添加一個(gè) deleteUserByCellphone() 或 deleteUserById() 接口就可以了。
     *  這個(gè)方法可以解決問題,但是也隱藏了一些安全隱患,刪除用戶是一個(gè)非常慎重的操作,我們只希望通過后臺(tái)管理系統(tǒng)來執(zhí)行,
     *  所以這個(gè)接口只限于給后臺(tái)管理系統(tǒng)使用,如果在沒有鑒權(quán)的情況下,加限制地被其他業(yè)務(wù)系統(tǒng)調(diào)用,就有可能導(dǎo)致誤刪用戶。
     *
     *  方案二:在沒有鑒權(quán)情況下可以從代碼層面規(guī)避上述風(fēng)險(xiǎn),具體可以參照接口隔離原則,調(diào)用者不應(yīng)該強(qiáng)迫依賴它不需要的接口,
     *  將刪除接口單獨(dú)放到另外一個(gè)接口 RestrictedUserService 中,然后將 RestrictedUserService 只打包提供給后臺(tái)
     *  管理系統(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;
        }
    }
    
    • 如果把“接口”理解為單個(gè) API 接口或函數(shù),函數(shù)的設(shè)計(jì)要功能單一,不要將多個(gè)不同的功能邏輯在一個(gè)函數(shù)中實(shí)現(xiàn)。部分調(diào)用者只需要函數(shù)中的部分功能,那我們就需要把函數(shù)拆分成粒度更細(xì)的多個(gè)函數(shù),讓調(diào)用者只依賴它需要的那個(gè)細(xì)粒度函數(shù)。
    /**
     * Statistics類
     *
     * 接口設(shè)計(jì)分析:count() 函數(shù)的功能不夠單一,包含很多不同的統(tǒng)計(jì)功能,比如,求最大值、最小值、平均值等
     * 場景一:如果在項(xiàng)目中,對(duì)每個(gè)統(tǒng)計(jì)需求,Statistics 定義的那幾個(gè)統(tǒng)計(jì)信息都有涉及,那 count()
     * 函數(shù)的設(shè)計(jì)就是合理的。
     *
     * 場景二:如果每個(gè)統(tǒng)計(jì)需求只涉及 Statistics 羅列的統(tǒng)計(jì)信息中一部分,比如,有的只需要用到 max、
     * min、average 這三類統(tǒng)計(jì)信息,在這個(gè)應(yīng)用場景下,count() 函數(shù)的設(shè)計(jì)就有點(diǎn)不合理了,這種場景下
     * 需要將其拆分成粒度更細(xì)的多個(gè)統(tǒng)計(jì)函數(shù)。
     *
     * 總結(jié):ISP提供了一種判斷接口是否職責(zé)單一的標(biāo)準(zhǔn):通過調(diào)用者如何使用接口來間接地判定。如果調(diào)用者只
     * 使用部分接口或接口的部分功能,那接口的設(shè)計(jì)就不夠職責(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è)計(jì)要盡量單一,不要讓接口的實(shí)現(xiàn)類和調(diào)用者,依賴不需要的接口函數(shù)。
    /**
     * Application類
     *
     * 背景:假設(shè)我們的項(xiàng)目中用到了三個(gè)外部系統(tǒng):Redis、MySQL、Kafka。每個(gè)系統(tǒng)都對(duì)應(yīng)一系列配置信息,
     *      比如地址、端口、訪問超時(shí)時(shí)間等。為了在內(nèi)存中存儲(chǔ)這些配置信息,供項(xiàng)目中的其他模塊來使用,我
     *      們分別設(shè)計(jì)實(shí)現(xiàn)了三個(gè) Configuration 類:RedisConfig、MysqlConfig、KafkaConfig
     *
     * 需求:
     *      1.希望支持 Redis 和 Kafka 配置信息的熱更新。所謂“熱更新(hot update)”就是,如果在配
     *      置中心中更改了配置信息,我們希望在不用重啟系統(tǒng)的情況下,能將最新的配置信息加載到內(nèi)存中(也就
     *      是 RedisConfig、KafkaConfig 類中)。
     *
     *      2.監(jiān)控功能需求。通過命令行來查看 Zookeeper 中的配置信息是比較麻煩的。所以,我們希望能有一
     *      種更加方便的配置信息查看方式。我們可以在項(xiàng)目中開發(fā)一個(gè)內(nèi)嵌的 SimpleHttpServer,輸出項(xiàng)目的
     *      配置信息到一個(gè)固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 。我們只需要在瀏覽
     *      器中輸入這個(gè)地址,就可以顯示出系統(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)控項(xiàng)
         */
        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(){
            // 輸出項(xiàng)目的配置信息到一個(gè)固定的 HTTP 地址
            // 比如:http://127.0.0.1:2389/config 。
            // 我們只需要在瀏覽器中輸入這個(gè)地址,就可以顯示出系統(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)控項(xiàng)
         */
        @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();
        }
    }
    
  2. 接口隔離原則與單一職責(zé)原則的區(qū)別

    單一職責(zé)原則針對(duì)的是模塊、類、接口的設(shè)計(jì)。接口隔離原則相對(duì)于單一職責(zé)原則,一方面更側(cè)重于接口的設(shè)計(jì),另一方面它的思考角度也是不同的。接口隔離原則提供了一種判斷接口的職責(zé)是否單一的標(biāo)準(zhǔn):通過調(diào)用者如何使用接口來間接地判定。如果調(diào)用者只使用部分接口或接口的部分功能,那接口的設(shè)計(jì)就不夠職責(zé)單一。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容