设计模式系列之设计原则(3) 单一职责原则

单一职责原则要求类、接口或方法有且仅有一个引起它变化的原因。遵循此原则有助于降低代码复杂度,提高可读性和可维护性,减少修改风险。本文通过实例展示了如何在类、接口及方法层面应用此原则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

定义

一个类应该有且仅有一个能引起它变化的原因,否则类应该被拆分。

​ 从定义可以看出,“职责”指导致类发生变化的原因,实质上就是项目的业务需求。定义只给出了类,方法和接口同样也可作为单一职责原则适用的对象,本文中以类作为举例,描述内容同样适用于方法、接口。

​ 类对应的职责有改变的潜在区域。超过一个职责,意味着超过一个会发生改变的区域,会提高修改代码的风险。从词义角度出发,遵循单一职责原则似乎很简单,只需将项目划分为若干子模块,各自对应一个类即可。但在编程实践中,区分设计中的责任,也就是我们常说的对业务的抽象划分能力,恰恰是最困难的环节之一。更困难的是,即使在项目初期能够对其进行合理的功能拆分,由于无法避免出现职责扩散,原本遵循单一职责原则的软件实体变得不再遵循。因此,遵循单一职责原则要立足于当前需求,同时考虑未来的需求扩展。

职责扩散:软件需求发生变化等原因,职责P被分化为粒度更细的职责P1 和 P2.

作用

单一职责原则的核心是控制类的粒度大小,使其承担最小的责任,具备较高的内聚性。同时,减少了修改代码的可能性。

  • 降低复杂度。职责单一,代码逻辑较多项职责简单。
  • 提高可读性。复杂度降低,可读性必然提高。
  • 提高可维护性。程序可读性高,维护更容易。
  • 降低风险。这分为两个层次,其一是只有一个原因会导致修改代码动作,强调修改入口只有一个。其二是修改代码只会影响一个功能,不会影响其他功能。

实现方法

​ 在两种情况下,要考虑遵循单一职责原则:

  • 软件实体的设计阶段。这一阶段要求设计人员站在项目整体的高度,发现类的不同职责并将其分离,再封装到不同的类或模块中。由于软件实体较多且关系复杂,同时为了平衡设计模式的各个原则,该阶段易出现不遵循单一职责原则的软件实体。

  • 需求变更导致职责扩散。此时可对原有软件实体进行重构,但最终是否分离旧类还需考虑多方面因素。

以上两种情况是对业务模型进行拆分,直至达到符合要求的粒度。一般来说,真是世界中的实体包含多想功能,譬如项目组长包含编码功能和管理功能,在完成拆分之后,需要利用继承或实现组合成符合业务的软件实体。因此,遵循单一职责原则总体上分为两步:

  1. 拆分。对职责进行分离。
  2. 组合。对职责进行组装。

在编码实践上,以下角度可使代码贴近单一职责原则

  • 孤立变化
  • 追踪依赖。检查类的构造从参数是否包含太多参数,因为每个参数都作为类的依赖存在,同时这些参数自身也拥有依赖,这样就形成了依赖链。可使用DI机制(依赖注入)动态的注入参数。
  • 追踪方法参数。一般来讲,方法参数个数代表内部实现的职能,因此避免使用太多的方法参数。同时注意方法的命名,长的方法名称意味着内部存在复杂的逻辑。
  • 尽早重构。发现可以重构时,果断进行微重构,使项目按照简洁的方式生长。

代码实践

​ 正如上文的讨论,在设计类、接口、方法时都要遵循单一职责原则,否则应对其进行重构。

类层面

下图是不遵循单一职责的Bird类,可以看到存在明显的if-else语句:

public class Bird {
    //程序中存在分支判断,这段代码可以拆分
    public void moveMode(String birdName){
        if("鸵鸟".equals(birdName)){
            System.out.println(birdName + "用脚走路");
        }else{
            System.out.println(birdName + "用翅膀飞");
        }
    }
}

我们对其进行重构,以判断条件为分界,将该类拆分为两个类:

//类一,只包含翅膀分功能
public class FlyBird {
    String birdName;
    public FlyBird(String birdName){
        this.birdName = birdName;
    }
    public void moveMode(){
        System.out.println(birdName + "用翅膀飞");
    }
}


//类二 ,只包含用脚走功能
public class FootBird {
    String birdName;
    public FootBird(String birdName){
        this.birdName = birdName;
    }
    public void moveMode(){
        System.out.println(birdName + "用脚走");
    }
}

重构之后类图如下所示:

接口层面

同类层面类似,不遵循单一职责原则的接口如下

//接口包含了两项功能
public interface ICourse {
    //获取课程名称
    String getCourseName();

    //管理课程
    void managerCourse();
}

对上面接口进行拆分,得到两个接口如下

//定义获取课程名称的规范
public interface IGetName {
    //获取课程名称
    String getCourseName();
}

//定义管理课程的操作
public interface IManagerCourse {
    //管理课程
    void managerCourse();
}

同类不同的是,实现类可以继承多个接口,进行对功能进行组装,如下面代码

//实现两个接口,具备两个接口定义的功能
public class StudyCourse implements ICourse, IManagerCourse {
    @Override
    public String getCourseName() {
        System.out.println("java课程");
        return "java课程";
    }

    @Override
    public void managerCourse() {
        System.out.println("管理课程");
    }
}

显然,这种方式对应的类图如下

方法层面

方法的情况同类和接口的情况类似,代码如下

public class Student {
    //多个参数,不遵循单一职责原则
    public void updataStudentInfo(String name, String age){
        System.out.println(name + age);
    }
    //将上面方法拆分为下面两个方法
    public void updateName(String name){
        System.out.println(name);
    }
    public void updataAge(String age){
        System.out.println(age);
    }


    //存在判断,可对其进行拆分
    public void someAction(Boolean b){
        if(b){
            //do something
        }else{
            //do other thing
        }
    }
}

编程知识点

  • 可变长度参数列表,其语法就是类型后跟…,表示此处接受的参数为0到多个Object类型的对象,或者是一个Object[]。使用可变长度参数列表时,
    1. 参数可以不传
    2. 可以传入单一的参数
    3. 可以传入多个参数
    4. 可以直接传入一个数组

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值