当前位置 博文首页 > 蔡小明:Java设计模式(2:单一职责原则和依赖倒置原则详解)

    蔡小明:Java设计模式(2:单一职责原则和依赖倒置原则详解)

    作者:蔡小明 时间:2021-06-07 18:21

    单一职责原则:不要存在多于一个导致类变更的原因。简单来说,就是一个Class/Interface/Method只负责一项职责。

    一、单一职责原则

    不要存在多于一个导致类变更的原因。简单来说,就是一个Class/Interface/Method只负责一项职责

    这句话最为重要的就是这一段:一个Class/Interface/Method只负责一项职责

    我们先来举一个例子,我们在日常生活中都或多或少的听过LOL(英雄联盟)这个游戏,而这个游戏在各个直播平台都很火爆,那我们就以此为例:

    某个游戏直播平台会将主播直播时的视频录制下来,等到主播下播后再上传到平台,这样就形成了录播。对于这两种视频观看模式,平台有着这样的规定:观看直播不允许快进、回放,而录播则可以,那我们首先想到的应该是这样方式:

    /**
     * 平台
     */
    public class UuSee {
    
        private final String LiveName = "直播";
        
         private final String ReplayName = "录播";
    
        // 观看
        public void watch(String name){
            if (LiveName.equals(name)){
                System.out.println("观看LOL "+name+",不能快进、回放!");
            } else if(ReplayName.equals(name)) {
                System.out.println("观看LOL "+name+",可以快进、回放!");
            }
        }
    }
    

    我们来写一个测试的代码看看:

    public static void main(String[] args) {
        UuSee uuSee = new UuSee();
        uuSee.watch("直播");
        uuSee.watch("录播");
    }
    

    从测试的代码来看的话,UuSee类承担了两种不同的处理逻辑。那么现在来增加一个需求:对直播和录播的视频进行加密,而直播和录播视频的加密方式不同。那么我们必须要修改源码,而这可能影响其他地方的调用。所以现在我们来将两种观看方式分开,让它们互不干扰:

    直播(LiveVideo类):

    /**
     * 直播
     */
    public class LiveVideo {
        
        public void watch(String name){
            System.out.println("直播视频加密......");
            System.out.println("观看LOL "+name+",不能快进....");
        }
        
    }
    

    录播(RePlayVideo类):

    /**
     * 录播
     */
    public class RePlayVideo {
    
        public void watch(String name){
            System.out.println("录播视频加密......");
            System.out.println("观看LOL "+name+",可以快进.....");
        }
        
    }
    

    调用代码:

    public static void main(String[] args) {
        RePlayVideo rePlayVideo = new RePlayVideo();
        rePlayVideo.watch("录播");
        
        LiveVideo liveVideo = new LiveVideo();
        liveVideo.watch("直播");
    }
    

    这样看的话,直播类LiveVideo和录播类RePlayVideo都调用自己的处理逻辑,两者互不干扰。那么如果业务继续发展:增加VIP用户,并且只有VIP用户才能观看录播(获得视频流);而普通用户虽然不能观看录播,但可以获取录播视频的基本信息。这样就会增加两个方法:getReplayVideoInfo()getReplayVideo(),这时候发现录播类RePlayVideo和直播类LiveVideo都会拥有其中的两个方法,那不如设计一个顶级接口,将他们一起纳入管理:UuSeeInterface

    public interface UuSeeInterface {
    
        // 获得 录播视频信息
        public String getReplayVideoInfo();
    
        // 获得 录播视频 视频流
        public byte[] getReplayVideo();
        
        // 观看视频
        public void watch(String name);
    }
    

    写完这个接口,我们发现从控制视频播放权限的层面来看的话,可以分为两个职责:管理职责和展示职责;getReplayVideoInfo()getReplayVideo()方法都属于展示职责,watch()方法属于管理职责,这样的话我们就可以将上面的接口拆分一下:

    管理职责接口(UuSeeManagement

    /**
     * 管理职责
     */
    public interface UuSeeManagement {
        // 观看视频
        public void watch(String name);
    }
    

    展示职责接口(UuSeeInfo

    /**
     * 展示职责
     */
    public interface UuSeeInfo {
    
        // 获得 录播视频信息
        public String getReplayVideoInfo();
    
        // 获得 录播视频 视频流
        public byte[] getReplayVideo();
    }
    

    说完了一个Class/Interface只负责一项职责的事情后,我们再来看一看一个Method只负责一项职责的问题。

    假如有这么一个方法:更改一个用户的用户名和地址,我们可能会偷懒写成这样。

    // 修改用户名称和地址
    public void modifyUserInfo(String userName,String address){
        System.out.println("用户名改为:"+userName);
        System.out.println("用户地址改为:"+address);
    }
    

    那么当我们又增加一个需求:只更改用户名称,不更改用户地址。那么我们在调用modifyUserInfo()方法时,还要去千方百计的获得用户的地址,非常的麻烦,倒不如将上面的方法拆分为两个方法:

    // 修改用户名称
    public void modifyUserName(String userName){
        System.out.println("用户名改为:"+userName);
    }
    
    // 修改用户地址
    public void modifyUserAddress(String address){
        System.out.println("用户地址改为:"+address);
    }
    

    这样来看,我们需要修改用户名称的时候就调用modifyUserName()方法,需要修改用户地址的时候就调用modifyUserAddress()方法,两个方法独立分开,不相互干扰,后期也好维护。

    当然,在日常工作中,我们不可能面面俱到,也不要为了某种原则而将自己陷入某个陷阱中,还是要根据实际情况做出不同的选择。但是,编写代码时,要尽量的满足单一原则,这样也方便我们后期的代码维护。

    二、依赖倒置原则

    这个原则是开闭原则的基础,是指设计结构代码时,高层模块不应该依赖于底层模块,二者应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。即:针对接口编程,依赖于抽象而不依赖于具体

    我们来先看一段代码:

    /**
     * 动物园
     */
    public class Zoom {
    
        // 老虎
        public void tiger(){
            System.out.println("老虎在吃鸡肉.....");
        }
    
        // 猴子
        public void monkey(){
            System.out.println("猴子在吃香蕉.....");
        }
    }
    
    // 测试
    public static void main(String[] args) {
        Zoom zoom = new Zoom();
        zoom.tiger();
        zoom.monkey();
    }
    

    小明周末去动物园游玩,正值晌午,动物饲养员在给动物喂食,觉得有趣就想记录一下。在记录完tiger和monkey之后,小明又发现了熊猫(panda)在吃竹子,于是兴冲冲的准备记录下来,可动笔时却发现一个问题:tiger()monkey()方法已经写好了,调用没有任何问题,但在增加一个panda()方法的话,不就修改了Zoom类的源代码,这样会不会有额外的风险呢?似乎也不符合设计模式中的开闭原则。思考良久,小明将上述代码改成了下面这样:

    /**
     * 动物园
     */
    public interface Zoom {
    
        // 吃午饭
        public void eat();
    }
    
    // 老虎
    public class Tiger implements Zoom {
        @Override
        public void eat() {
            System.out.println("老虎在吃鸡肉.....");
        }
    }
    
    // 猴子
    public class Monkey implements Zoom{
        @Override
        public void eat() {
            System.out.println("猴子在吃香蕉......");
        }
    }
    
    // 调用类
    public class Xming {
        public void eat(Zoom zoom){
            zoom.eat();
        }
    }
    
    // 测试
    public static void main(String[] args) {
            Xming xming = new Xming();
            xming.eat(new Tiger()); // 老虎
            xming.eat(new Monkey()); // 猴子
        }
    

    这样一看,TigerMonkey互不干扰,当需要记录熊猫吃竹子时,只需要在创建一个Panda类就可以了,不用去修改现有的源代码:

    // 熊猫
    public class Panda implements Zoom {
        @Override
        public void eat() {
            System.out.println("熊猫正在吃竹子......");
        }
    }
    
    // 测试
    public static void main(String[] args) {
            Xming xming = new Xming();
            xming.eat(new Tiger()); // 老虎
            xming.eat(new Monkey()); // 猴子
            xming.eat(new Panda());// 熊猫
        }
    

    这个时候我们发现Xming类调用eat()方法时注入方式很熟悉,想了想是依赖注入,而依赖注入的方式还有构造器注入setter注入。我们先来看看构造器注入的方式:

    // 调用类
    public class Xming {
        
        private Zoom zoom;
    
        public Xming(Zoom zoom) {
            this.zoom = zoom;
        }
    
        public void eat(){
            zoom.eat();
        }
    }
    

    调用代码:

    public static void main(String[] args) {
        Xming tiger = new Xming(new Tiger());
        tiger.eat();
        Xming panda = new Xming(new Panda());
        panda.eat();
    }
    

    构造器注入的方式在每次调用时都要创建实例。那么,如果Xming是全局单例的话,我们就只能选择setter注入的方式了:

    // 调用类
    public class Xming {
    
        private Zoom zoom;
    
        public void setZoom(Zoom zoom) {
            this.zoom = zoom;
        }
    
        public void eat(){
            zoom.eat();
        }
    }
    
    // 调用
    public static void main(String[] args) {
        Xming tiger = new Xming();
        tiger.setZoom(new Tiger());
        tiger.eat();
        Xming panda = new Xming();
        panda.setZoom(new Panda());
        panda.eat();
    }
    

    结语:依赖倒置原则的本质还是面向接口编程,而事实就是以抽象为基准比以细节为基准搭建起来的框架要稳定的多,后期维护与查看都相对的容易与清晰一些。所以,我们在日常的开发中,要根据实际的业务需求来分析,尽量的使用面向接口编程,先顶层再细节的步骤来设计代码架构。

    bk
    下一篇:没有了