设计模式学习笔记 - 设计原则 - 1.单一职责原则

本文介绍了SOLID原则中的单一职责原则,强调类或模块应只负责一个功能,提倡小而精的设计。讨论了如何判断类的职责是否单一,以及何时适当拆分以保持代码的高内聚和低耦合,指出过度拆分可能导致问题。
摘要由CSDN通过智能技术生成

前言

前面我们提到过 SOLID 原则,实际上 SOLID 由 5 个设计原则组成,分别是:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖反转原则。它们分别对应 SLOID 中的 S、O、L、I、D 这 5 个英文字母。

今天来学习下 SOLID 原则中的第一个原则:单一职责原则。

在这里插入图片描述


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

单一职责原则 (Single Responsibility Principle),缩写为 SRP。

英文原文:A class or module should have a single responsibility。
翻译中文:一个类或模块只负责完成一个职责或功能。

注意,这个原则描述的对象包含两个,一个是类,一个是模块。不管是哪个对象,单一职责在应用到这两个对象时,道理是一样的。接下来,会从 “类” 的角度,来讲解如何应用这个原则。

一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。要把大而全的类拆分成多个功能单一、粒度更细的类。

换个角度来说,一个类包含了两个或者两个以上业务不相干的功能,就不符合单一职责原则

例如,一个类里面包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,就违反了单一职责原则。为了满足单一职责原则,需要将这个类拆分成两个粒度更细、功能更单一的两个类:订单类和用户类。

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

从刚刚的例子来看,单一职责原则好像不难应用。那是因为我举的例子比较极端,一眼就能看出订单和用户毫不相干。但大部分情况下,类里的方法是归类为同一类功能,还是归为不相关的两类功能,并不是那么容易判定的。在真实的软件开发中,对一个类是否职责单一的判定,是很难拿捏的。

在一个社交产品中,用下面的 UserInfo 来记录用户的信息。你觉得是否符合单一职责原则呢?

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 detailOfAddress; // 详细地址
	// 省略其他属性和方法...
}

对于这个问题,有两种不同的观点。

  • 一种观点是,UserInfo 类包含的都是跟用户相关的信息,所有的属性和方法都隶属于用户这样一个业务模型,满足单一职责原则;
  • 另一种观点是,地址信息在 UserInfo 类中,所占比重较高,可以继续拆分成独立的 UserAddressUserInfo 只保留出 UserAddress 之外的属性,拆分之后的两个类的职责更加单一。

实际上,要做出选择,我们不能脱离具体的应用场景。如果,在这个社交产品中,用户地址信息和其他信息一样,只是单纯地用于展示,那 UserInfo 的设计就是合理的。但是,如果这个社交产品发展地比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息类。

再进一步延伸下,如果做这个社交产品的公司发展地越来越好,公司内部有开发出了很多其他产品。公司希望支持统一账号系统,也就是用户一个账号可以在公司内的所有产品中登录。这个时候,我们就需要对 UserInfo 进行拆分,将跟身份认证相关的信息(比如 emailtelephone 等)抽取成独立的类。

从上面的例子,可以总结出,在不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,都是不一样的。在某种应用场景下,一个类的设计可能符合单一职责原则了,但是如果换个应用场景,可能就不满足单一职责原则,需要继续拆分成粒度更细的类。

此外,从不同的业务层面去看同一个类的设计,对类是否职责单一,也会有不同的认识。比如,例子用的 UserInfo 类。如果从“用户”这个业务层面来看,UserInfo 包含的信息都属于用户,满足单一职责原则。如果从更加细分的 “用户展示信息” “地址信息” “登录认证信息” 等等这些细细粒度的业务层面来看,那 UserInfo 就应该继续拆分。

综上所述,评价一个类的职责是否足够单一,并没有非常明确的、可以量化的标准。实际上,在真正的软件开发中,也没必要过于未雨绸缪,过度设计。我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这个就是所谓的重构

这里还有些小技巧,能够辅助你,从侧面判断一个类的职责是否足够单一。

  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑读类进行拆分。
  • 类依赖的其他类过多,或者依赖的类的其他类过多,不符合高内聚、低耦合的设计思想,就需要考虑拆分。
  • 私有方法过多,就要考虑是否将私有方法独立到新的类中,设置为 public 方法,供更多的类适用,从而提高代码的复用性。
  • 比较难给类起合适的名字,很难用一个业务名词概括,或者只能用一些笼统的 ManagerContext 之类的词语来命名,这就说明类的职责定义得可能不够清楚。
  • 类中的大量方法都是集中操作类中的某几个属性,比如,在 UserInfo 中,如果有一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

不过,你可能有这样的疑问:在上面的小技巧中,提到的类的代码行数、函数或者属性过多,就有可能不满足单一职责原则。那多少行代码才算是行数过多呢?多少个函数、属性才称得上过多呢?

实际上,这个问题,很不好定量的回答。实际上,可以给你一个凑活能用的、比较宽泛的、可量化的标准,那就是一个类的代码行数最好不能超过 200 行,函数及属性的个数最好不要超过 10 个。

实际上,从另一种角度来看,有下面这些情况时,说明类的行数、函数、属性过多了

  • 当一个类的代码,读起来让你头大了;
  • 实现某个功能时不知道该用哪个函数了;
  • 想用哪个函数翻半天找不到了;
  • 只用到一个小功能却要引入整个类;

类的职责是否设计地越单一越好?

为了满足单一职责,是不是把类拆得越细就越好呢?答案是否定的。通过一个例子来解释下。 Serialization 类实现了一个简单协议的序列化和反序列化功能,具体代码如下:

/**
 * protocol format: identifier-string;(gson string)
 * For example: UEUEUE; {"a":"A", "b":"B"}
 */
public class Serialization {
	private static final String IDENTIFIER_STRING = "UEUEUE;";
	private Gson gson;
	
	public Serialization() {
		this.gson = new Gson();
	}
	
	public String serialize(Map<String, String> object) {
		StringBuilder textBuilder = new StringBuilder();
		textBuilder.append(IDENTIFIER_STRING);
		textBuilder.append(gson.toJson(object));
		return textBuilder.toString();
	}
	
	public Map<String, String> deserialize(String text) {
		if(!text.startsWith(IDENTIFIER_STRING)) {
			return Collections.emptyMap();
		}
		String gsonStr = text.substring(IDENTIFIER_STRING.length());
		return gson.fromJson(gsonStr, Map.class);
	}
}

如果我们想让类的职责更加单一,我们对 Serialization 类进一步拆分,拆分成一个只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer 类。拆分后的代码如下所示:

public class Serializer {
	private static final String IDENTIFIER_STRING = "UEUEUE;";
	private Gson gson;
	
	public Serializer() {
		this.gson = new Gson();
	}
	
	public String serialize(Map<String, String> object) {
		StringBuilder textBuilder = new StringBuilder();
		textBuilder.append(IDENTIFIER_STRING);
		textBuilder.append(gson.toJson(object));
		return textBuilder.toString();
	}
}

public class Deserializer {
	private static final String IDENTIFIER_STRING = "UEUEUE;";
	private Gson gson;
	
	public Deserializer() {
		this.gson = new Gson();
	}
	public Map<String, String> deserialize(String text) {
		if(!text.startsWith(IDENTIFIER_STRING)) {
			return Collections.emptyMap();
		}
		String gsonStr = text.substring(IDENTIFIER_STRING.length());
		return gson.fromJson(gsonStr, Map.class);
	}
}

虽然拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从 UEUEUE 改成了 DFDFDF,或者序列化方式从 JSON 改成了 XML,那 Serializer 类和 Deserializer 类都需要做相应的改动,代码的内聚性显然没有原来的 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而忘记了修改 Deserializer 类,那就会导致序列化和反序列化不匹配,程序运行出错,拆分之后,代码的可维护性变差了。

实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、可维护性、复用性等。我们在考虑应用某一设计原则是否合理的时候,也可以以此作为最终的考量标准。

回顾

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

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

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

不同的应用场景、不同阶段的需求、不同的业务背景,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的指标更具有指导意义和可执行性,比如,出现下面这些情况,就有可能说明这个类的设计不满足单一职责原则:

  • 类中的代码行数、函数、属性过多。
  • 类依赖的其他类过多,或者依赖类的其他类过多。
  • 私有方法过多。
  • 比较难给一个类起一个合适的名字。
  • 类中的大量方法都是集中操作类中的几个属性。

3.类的职责是否设计的越细越好?

单一职责原则通过避免设计大而全的类,避免将不相关的功能聚合在一起,来提高类的高内聚性。同时,类职责单一,类依赖和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。

但是,如果拆分地过细,实际上会适得其反,反倒会降低代码的内聚性,也会影响代码的可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值