👉文章示例代码👈

定义

就一个类而言,有且仅有一个引起它变更的原因。

一个类/接口/方法只负责一项职责。

如何理解

假设有个 A 类负责职责1和职责2两个职责。

当需求发生变更时,比方说职责1相关的功能需要发生改变,那我们在修改 A 这个类的时候就有可能会导致原本运行正常的职责2发生故障。

也就是说对于这个 A 类来说,当职责1和职责2的需求发生变化时候,都会影响这个 A 类,即存在两个导致类变更的原因。

解决方案

遵循单一职责原则,针对上述职责1和职责2建立 A 类和 B 类,分别负责职责1和职责2。

这种情况下,当我们在修改 A 类的时候,对于负责职责2的 B 类来说是不会发生变更的,也不会使其发生故障,反之亦然。

场景示例

笔者这里用饮料归类做个示范来帮助理解。

创建饮料类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author zhh
* @description 饮料类
* @date 2020-02-05 22:23
*/
public class Drink {

/**
* 饮料类型
* @param drinkName 饮料名称
*/
public void type(String drinkName) {
System.out.println(drinkName + "是汽水");
}
}

测试类及输出

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author zhh
* @description 测试类
* @date 2020-02-05 22:25
*/
public class Test {

public static void main(String[] args) {
Drink drink = new Drink();
drink.type("可乐");
drink.type("牛奶");
}
}

测试类的输出结果如下:

可乐是汽水
牛奶是汽水

方案调整

在上述的场景示例当中我们可以看到,牛奶是汽水这答案显然是不对的。

为了解决这个问题,我们也许会这样做,在原来饮料类的饮料类型方法中增加一个分支判断。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Drink {

/**
* 饮料类型
* @param drinkName 饮料名称
*/
public void type(String drinkName) {
if ("牛奶".equals(drinkName)) {
System.out.println(drinkName + "是奶制品");
} else {
System.out.println(drinkName + "是汽水");
}
}
}

上述方案调整看似解决了这个问题。

但是我们不妨再想想,如果在这个地方我们再传入一些其他的饮料(既不是奶制品也不是汽水),那么我们的这个饮料类型方法可能还需要继续去扩展,即需要不断地调整变更这个方法,这样是不遵循单一职责原则的。

职责分离

换种方式,这里我们按照类来进行职责分离,使得每个类中的职责单一。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author zhh
* @description 汽水饮料
* @date 2020-02-05 22:34
*/
public class SodaDrink {

/**
* 饮料类型
* @param drinkName 饮料名称
*/
public void type(String drinkName) {
System.out.println(drinkName + "是汽水");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author zhh
* @description 奶制品饮料
* @date 2020-02-05 22:34
*/
public class DairyDrink {

/**
* 饮料类型
* @param drinkName 饮料名称
*/
public void type(String drinkName) {
System.out.println(drinkName + "是奶制品");
}
}

对应的测试类调整

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
SodaDrink sodaDrink = new SodaDrink();
sodaDrink.type("可乐");

DairyDrink dairyDrink = new DairyDrink();
dairyDrink.type("牛奶");
}
}

类结构图

以上示例类的结构图如下所示

image.png

其余方式

以上方案调整是按类的职责分离实现的。我们看看其他两种方式是如何实现职责分离的。

接口方式

给定一个商品接口,我们可以看到这个接口的一个职责是获取商品的信息(名称、价格),而另一个职责可以看做是管理商品(购买、退款),与商品内容无关。

在这两个职责中退款商品势必会影响这个接口中获取商品信息相关内容的变化(比方说退了商品那可能就无法获得商品的名称和价格了),即这两个职责是互相影响的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author zhh
* @description 商品接口
* @date 2020-02-05 22:49
*/
public interface IGoods {
/**
* 获取商品名称
*/
String getGoodsName();

/**
* 获取商品价格
*/
Double getGoodsPrice();

/**
* 购买商品
*/
void buyGoods();

/**
* 退款商品
*/
void refundGoods();
}

对接口按职责进行拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author zhh
* @description 商品信息接口
* @date 2020-02-05 22:52
*/
public interface IGoodsInfo {

/**
* 获取商品名称
*/
String getGoodsName();

/**
* 获取商品价格
*/
Double getGoodsPrice();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author zhh
* @description 商品管理接口
* @date 2020-02-05 22:53
*/
public interface IGoodsManager {

/**
* 购买商品
*/
void buyGoods();

/**
* 退款商品
*/
void refundGoods();
}

创建商品实现类

创建一个商品的具体实现类,同时实现上述按职责分离的两个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author zhh
* @description 商品实现类
* @date 2020-02-05 22:56
*/
public class GoodsImpl implements IGoodsManager, IGoodsInfo {

public String getGoodsName() {
return "商品名称";
}

public Double getGoodsPrice() {
return 0.0;
}

public void buyGoods() {
System.out.println("购买商品");
}

public void refundGoods() {
System.out.println("退款商品");
}
}

类结构图

以上示例类的结构图如下所示

image.png

方法方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author zhh
* @description
* @date 2020-02-05 22:59
*/
public class Method {

/**
* 更新商品信息
* @param goodsName 商品名称
* @param goodsPrice 商品价格
*/
public void updateGoodsInfo(String goodsName, Double goodsPrice) {
// 模拟更新商品名称、价格
goodsName = "可乐";
goodsPrice = 3.0;
}
}

我们可以看到上面这个方法又是更新商品的名称,又是更新商品的价格。

然而按照方法方式进行职责分离应该是以下这种样式。代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class Method {
public void updateGoodsName(String goodsName) {
// 模拟更新商品名称
goodsName = "可乐";
}

public void updateGoodsPrice(Double goodsPrice) {
// 模拟更新商品价格
goodsPrice = 3.0;
}
}

即我们把刚才的方法拆解成两个方法。

要修改商品的名称只要调用修改商品名称的方法即可,要修改商品的价格只要调用修改商品价格的方法即可。

这两个方法的职责是非常单一且清晰的。

总结

单一职责原则的核心就是控制类的粒度的大小,将对象进行解耦,提高其内聚性。

职责分离条件

能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责,就可以考虑类的职责分离。

整合虽然说是一种很好的思维方式,但是在软件设计的时候,我们应该在类的职责分离上多做思考,尽量让每个类保持单一职责。但是由于每个人的看法不一,这就会导致职责边界的难以划分。有时候我们也需要做出相应的妥协,并不能完全死套单一职责原则,否则就会冗余出非常多的类文件,给维护带来不便。

单一职责不仅针对类来说,同样也适用于方法。一个方法应该尽可能地做好一件事情(只负责一项职责)。如果一个方法处理的事情过多(承担的职责过多),其颗粒度会变得很粗,从而不利于重用。

优点

  • 降低类的复杂度。一个类只需要去负责一项职责,逻辑相对于负责多项职责的类来说要简单的多。
  • 提高类的可读性。复杂度的降低,其可读性自然会提高。
  • 提高系统的可维护性。可读性的提高,使得系统更容易维护。
  • 降低变更引起的风险。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

缺点

  • 如果一个类承担的职责过多(等同于将这些职责耦合在一起),一个职责的变化可能会削弱或者抑制这个类去完成其他职责的能力,这种耦合会导致设计的脆弱。
  • 一个类如果承担过多的职责,当客户端需要该类的某一项职责的时候,不得不将其他不需要的职责全部包含进来,从而造成冗余的代码或者代码的浪费。

参考

  • 《Head First 设计模式》
  • 《大话设计模式》