2022-01-01设计原则--单一职责与接口隔离原则总结

单一职责(SRP)

  • 如何理解单一职责原则(SRP)?

    单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single responsibility。如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。

    注意,这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块,不管哪种理解道理是想通的,下面以类作为分析对象,模块自行引申即可。

    一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

  • 如何判断类的职责是否足够单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。所以我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类(持续重构)。

实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

  1. 类中的代码行数、函数或者属性过多;
  2. 类依赖的其他类过多,或者依赖类的其他类过多;
  3. 私有方法过多;
  4. 比较难给类起一个合适的名字;
  5. 类中大量的方法都是集中操作类中的某几个属性。
/**
 * UserInfo类
 *
 * 该类是否满足单一职责?
 *
 * 分析问题要结合实际的应用场景:如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。
 * 但是,如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 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; // 区
    private String detailedAddress; // 详细地址

}
  • 类的职责是否设计得越单一越好?

    单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

/**
 * Serialization类
 *
 * 拆分过度问题:以序列化为例经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,
 * 但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列
 * 化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的
 * 内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而
 * 忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,
 * 拆分之后,代码的可维护性变差了。
 *
 *
 */
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。”直译成中文的话就是:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

    理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。

    • 如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
    /**
     * UserService接口
     *
     *  场景:用户系统提供了一组跟用户相关的 API 给其他系统使用,比如:注册、登录、获取用户信息等。现在,
     *  我们的后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口。这个时候我们该如何来做呢?
     *
     *  分析:方案一:在 UserService 中新添加一个 deleteUserByCellphone() 或 deleteUserById() 接口就可以了。
     *  这个方法可以解决问题,但是也隐藏了一些安全隐患,删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,
     *  所以这个接口只限于给后台管理系统使用,如果在没有鉴权的情况下,加限制地被其他业务系统调用,就有可能导致误删用户。
     *
     *  方案二:在没有鉴权情况下可以从代码层面规避上述风险,具体可以参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,
     *  将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台
     *  管理系统来使用。
     *
     */
    
    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 接口或函数,函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
    /**
     * Statistics类
     *
     * 接口设计分析:count() 函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等
     * 场景一:如果在项目中,对每个统计需求,Statistics 定义的那几个统计信息都有涉及,那 count()
     * 函数的设计就是合理的。
     *
     * 场景二:如果每个统计需求只涉及 Statistics 罗列的统计信息中一部分,比如,有的只需要用到 max、
     * min、average 这三类统计信息,在这个应用场景下,count() 函数的设计就有点不合理了,这种场景下
     * 需要将其拆分成粒度更细的多个统计函数。
     *
     * 总结:ISP提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只
     * 使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
     *
     */
    @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;
        }
    }
    
    • 如果把“接口”理解为 面向对象编程(OOP) 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
    /**
     * Application类
     *
     * 背景:假设我们的项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都对应一系列配置信息,
     *      比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中的其他模块来使用,我
     *      们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig
     *
     * 需求:
     *      1.希望支持 Redis 和 Kafka 配置信息的热更新。所谓“热更新(hot update)”就是,如果在配
     *      置中心中更改了配置信息,我们希望在不用重启系统的情况下,能将最新的配置信息加载到内存中(也就
     *      是 RedisConfig、KafkaConfig 类中)。
     *
     *      2.监控功能需求。通过命令行来查看 Zookeeper 中的配置信息是比较麻烦的。所以,我们希望能有一
     *      种更加方便的配置信息查看方式。我们可以在项目中开发一个内嵌的 SimpleHttpServer,输出项目的
     *      配置信息到一个固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 。我们只需要在浏览
     *      器中输入这个地址,就可以显示出系统的配置信息。不过,出于某些原因,我们只想暴露 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监控接口
     */
    public interface Viewer {
        /**
         * 监控-输出文本信息
         */
        String outputInPlainText();
    
        /**
         * 监控-输出监控项
         */
        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 。
            // 我们只需要在浏览器中输入这个地址,就可以显示出系统的配置信息。
        }
    }
    
    //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();
        }
    
        /**
         * 监控-输出文本信息
         */
        @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 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. 接口隔离原则与单一职责原则的区别

    单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容