第一章 为什么学习设计模式
第二章 理解设计原则
包含五个设计原则,S O L I D
- S:单一责任原则(SRP),Single Responsibility Principle
- O:开放封闭原则(OCP),Open Closed Principle
- L:里式替换原则(LSP),Liskov Substitution Principle
- I:接口分离原则(ISP),Interface Segregation Principle
- D:依赖倒置原则(DIP),Dependency Inversion Principle
一、单一原则
1、如何理解单一职责原则(SRP)?
单一职责原则(Single Responsibility Principle),它要求一个类或模块应该只负责一个特定的功能,这有助于降低类之间的耦合度,提高代码的可读性和可维护性。
单一职责原则的定义描述非常简单,一个类或模块只负责完成一个职责或功能,也就是说,不要设计大而全的类,要设计力度小、功能单一的类。换个角度来讲就是,一个类包含了两个或两个以上业务不相干的功能,那就可以说他职责不够单一,应该将它拆分成多个功能更加单一、力度更细的类。
2、如何判断类的职责是否足够单一?
举例:在一个社交产品中,下面的UserInfo类来记录用户的信息,你认为UserInfo类的设计是否满足单一职责原则呢?
public class UserInfo {
private long userId;
private String username;
private String password;
private String telephone;
private String eamil;
private String avatarUrl;
// ..省略
private String province; // 省
private String city; // 市
private String detailAddress; // 区
}
一种观点是,UserInfo包含的都是跟用户相关的信息,所有的方法和属性都隶属于用户这样一个业务模型,满足单一职责原则。
另一种观点是:地址信息在UserInfo类中,所占的比重比较高,可以继续拆分出独立的Address类,UserInfo只保留除了Address之外的其他信息,拆分后两个类的职责更加单一。
哪一种说法更加准确呢?
实际上有一句话,脱离了业务谈设计就是耍流氓,事实上脱离了业务谈什么都是耍流氓,技术服务于业务这是亘古不变的道理。
上边两种情况,我们不能脱离具体的应用场景,如果这个社交产品中,用户的地址信息和其他信息一样,只是单纯地用来做展示,那么UserInfo现在的设计就是合理的。但是。如果这个社交产品发展的比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商的五六种,那我们最好将地址信息从UserInfo中拆分出来,独立成用户的物流信息(或者地址信息、收货信息等)。
按照上述所讲继续延伸,如果这个社交产品的公司发展的越来越好。公司内部又开发出了很多其他的产品。公司希望支持统一账号系统,也就是用户一个账号可以在公司内部的所有产品中登录。这个时候,就需要对UserInfo再次进行拆分,将跟身份认证相关的信息(比如email、telephone等)抽取成独立的类。
从上述所说可以看出,不同应用场景,不同阶段的需求背景,对同一个类的职责是否单一的判定,可能都是不一样的,可能在某种应用场景下,一个类的设计已经满足单一职责原则了,但是如果换一个应用场景,可能就不满足了,需要拆分成力度更细的类。
综上所述,评价一个类的职责是否足够单一,并没有一个非常明确的、可以量化的标准。实际上,在进行设计的时候,也没有必要过于未雨绸缪,过度设计。所以,可以先写一个粗粒度的类,满足业务需求,随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,就可以将这个粗粒度的类,拆分成几个更细粒度的类,这就是所谓的持续重构。
以下这几条原则,可以比较主管的去思考类是否职责单一:
- 类中的代码行数,函数或属性过多,会影响代码的可读性和可维护性,这时候需要考虑对类进行拆分
- 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,考虑对类进行拆分
- 私有方法过多,就要考虑能否将私有方法独立到新的类中,设置为public方法,供更多的类使用,提高代码的重用性
- 比较难给类起一个合适的名字,很难用一个业务名词概括,或者只能用一些比较笼统的Manager、Context等等这些词语来命名那个,这就说明类的 职责可能不够清晰
- 类中的大量方法都是集中操作类中的某几个属性,比如,在UserInfo这个例子中,如果一半的方法都是在操作address的信息,就可以考虑将这几个属性和对应的方法拆分出来
可能上边所讲的这些,还会存在一些疑问,这些问题不好定量的去回答,要多少才算是多?要多少是少?我觉得这部分的判断需要用在职业生涯中,接触的项目和代码越来越多,自然就会知道一个量的标准。
3、类的职责是否设计得越单一越好?
答案并不是,拆分的越细,就说明后续的维护会更加复杂,虽然提高了扩展性,但是代码的可维护性就变差了。
实际上,不管是应用设计原则还是设计模式,最终目的都是提高代码的可读性、可扩展性、复用性、可维护性,当在衡量一个设计原则是否合理的时候,也可以以此作为最终的考量标准。
二、开闭原则
1、原理概述
开闭原则的英文全称是 Open Closed Principle,简称OCP,软件实体(模块、类、方法等)应该“对扩展开放,对修改关闭”。
通俗一点讲就是,当需要新增一个功能时,应该是在已有代码的基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等),下面用一个简化的电商平台的订单折扣策略来说明:
Class Order{
private double totalAmount;
public Order(double totalAmount){
this.totalAmount = totalAmount;
}
// 计算打折后的金额
public double getDiscountedAmount(String discountType){
double discountedAmount = totalAmount;
if("FESTIVAL".equals(discountType)){
discountedAmount = totalAmountl * 0.9; // 节日打九折
} else if ("SEASONAL".equals(discountType)) {
discountAmount = totalAMount * 0.8; // 季节打八折
}
return discountedAmount;
}
}
上述代码,Order类中包含了一个计算折扣金额的方法,当需要添加新的折扣类型的时候,就需要去修改 getDiscountedAmount() 方法的代码,显然是不合理的,违反了开闭原则。
以下是遵循开闭原则的代码:
// 抽象折扣策略接口
interface DisocuntStrategy {
double getDiscountAmount(double totalAmount);
}
// 节日折扣策略
class FestivalDiscount implements DiscountStrategy {
@Override
public double getDiscountedAmount(double totalAmount) {
return totalAmount * 0.9; // 节日九折
}
}
// 季节折扣策略
class SeasonalDiscount implements DiscountStrategy {
@Override
public double getDiscountedAmount(double totalAmount) {
return totalAmount * 0.8; // 季节八折
}
}
Class Order{
private double totalAmount;
private DiscountStrategy discountStrategy;
public Order(double totalAmount, DiscountStrategy discountStrate){
this.totalAmount = totalAmount;
this.discountStrategy = discountStrategy;
}
// 形参是一个接口,具体传递可以传递任意一个实现了该接口的类对象 FestivalDiscount SeasonalDiscount
public void setDiscountStrategy(DiscountStrategy discountStrate){
this.discountStrategy = discountStrategy;
}
// 计算打折后的金额
public double getDiscountedAmount(){
return discountStrategy.getDiscountAmount(totalAmount);
}
}
2、修改代码就意味着违背开闭原则吗?
开闭原则的核心思想就是尽量减少对现有代码的修改,以降低修改带来的风险和影响,在实际开发过程中,完全不修改代码是不现实的,当需求变更或发现代码中的错误时,修改代码是正常的。然而,开闭原则鼓励通过设计更好的代码结构,使得在添加新功能或者扩展系统时,尽量减少现有代码的修改。
下面是一个简化日志记录器的实例,展示了在适当情况修改该代码,也不违背开闭原则。在这个例子中,应用程序支持将日志输出到控制台和文件,假设需要添加一个新功能,以便输出日志时同时添加一个时间戳。
interface Logger {
void log(String message);
}
class ConsoleLogger implements Logger {
@Override
public void log(String message){
System.out.println("Console:" + message);
}
}
Class FileLogger implements Logger(){
@Override
public void log(String message){
System.out.println("File:" + message);
// 将日志写入文件的实现省略...
}
}
为了添加时间戳的功能,需要修改现有的 ConsoleLogger 和 FileLogger 类,虽然需要修改代码,但是这是对现有功能的改进,而不是添加新的功能,所以这种修改是可以接受的,不违背开闭原则。
当遵循开闭原则的时候,其目的是为了让代码更容易进行维护,更具有复用性,同时降低了引入新缺陷的风险。但是,在某些情况下,遵循开闭原则可能会导致过度设计,增加代码的复杂性。因此在实际开发中,应该根据实际需求和预期的变化来平衡遵循开闭原则的程度,写代码不是为了设计而设计,脱离需求谈设计都是耍流氓,有些场景,比如项目的使用频率不高,修改的可能性很低,或者代码本来就很简单,使用了设计模式反而会增加开发难度,提高开发成本,得不偿失。