SOLID 原则是软件工程中使用的面向对象的设计概念,可以在多种语言中使用此概念。本文将重点关注Java中的SOLID原则。
微信搜索关注《Java学研大本营》,加入读者群,分享更多精彩
SOLID是Single Responsibility Principle、Open-Closed Principle、Liskov Substitution Principle、Interface Segregation Principle、Dependency Inversion Principle的首字母缩写。
下面,将依次详细介绍这五个原则。
单一职责原则(SRP)
该原则表明计算机程序中的每个模块、类或函数都应该对该程序功能的单个部分负责,并且应该封装该部分。或者换句话说,一个类应该有并且只有一个改变的理由。
例如,如果有一个简单的类来实现如下所示的 Cashback 规则。
package br.com.supercash.services.cashback;
import java.math.BigDecimal;
public class Cashback {
public BigDecimal calculate(BigDecimal total, BigDecimal percent) {
return total.multiply(percent);
}
... another rules here
}
已经实施了有关它的规则。现在,需要保存与客户关联的Cashback。
因此,将其保存在同一个 Cashback 类中。
package br.com.supercash.services.cashback;
import java.math.BigDecimal;
public class Cashback {
public BigDecimal calculate(BigDecimal total, BigDecimal percent) {
return total.multiply(percent);
}
... another rules here
public void save(BigDecimal value, String clientAccount) {
//imagine calling a save method here
}
}
现在有一个问题。这个类破坏了 SRP,因为 Cashback 类知道如何将信息保存在表中。那么,有两个理由改变这个类:第一个是Cashback规则是否改变,另一个是save changes的实现(例如,改变数据库,或者表名等)。
这就让代码难以维护。如何解决这个问题呢?可以将这段代码分成两个不同的类。
package br.com.supercash.services.cashback;
import java.math.BigDecimal;
public class Cashback {
public BigDecimal calculate(BigDecimal total, BigDecimal percent) {
return total.multiply(percent);
}
... another rules here
}
package br.com.supercash.services.cashback;
import java.math.BigDecimal;
public class CashbackPersistence {
public void save(BigDecimal value, String clientAccount) {
//imagine calling a save method here
}
现在,代码中有一层持久性和一层业务。
开闭原则
OCP原则的定义是:
应该能够扩展类行为,而无需修改它。
例如,假设需要实施一个计算返现的规则,但每种类型的产品都有特定的返现百分比。如果客户买衣服,那么他将获得 5% 的返现,但如果他购买电子产品,他将获得 1% 的返现。一种简单的实现方法:
public class Cashback {
public BigDecimal calculate(BigDecimal value, ProductType productType) {
if(productType.equals(ProductType.ELETRONICS)) {
return value.multiply(BigDecimal.valueOf(0.01));
}
if(productType.equals(ProductType.GIFT_CARD)) {
return value.multiply(BigDecimal.valueOf(0.1));
}
if(productType.equals(ProductType.CLOTHES)) {
return value.multiply(BigDecimal.valueOf(0.05));
}
else {
return BigDecimal.ZERO;
}
}
}
这是有效的,但未来可能会有问题。如果明天有新的产品类型,或者利益相关者决定子产品会有不同的返现,那么这段代码将很难修改。修改这段代码的原因有很多,而且很危险。
解决该问题的最佳实践之一就是这样做:
创建一个抽象的 Cashback 类:
public abstract class Cashback {
public BigDecimal calculate(BigDecimal value) {
return value.multiply(percentageCashback());
}
protected abstract BigDecimal percentageCashback();
}
所以,返现的计算是一样的。变化的独特之处在于返现的百分比。所以可以创建可以扩展这种行为的类,只改变返现的百分比:
public class ClothesCashback extends Cashback{
@Override
protected BigDecimal percentageCashback() {
return BigDecimal.valueOf(0.05);
}
}
public class EletronicsCashback extends Cashback{
@Override
protected BigDecimal percentageCashback() {
return BigDecimal.valueOf(0.01);
}
}
public class GiftCardCashback extends Cashback{
@Override
protected BigDecimal percentageCashback() {
return BigDecimal.valueOf(0.1);
}
}
现在,无需修改目前已经工作的代码,就可以扩展代码了。
里氏替换原则与接口隔离原则
想象一下,有一个接口 Animal,它有一些可以实现的动作。
public interface Animal {
void walk();
void bark();
void meow();
}
如果创建一个 Dog 类来实现 Animal,就会发现问题。狗不会喵喵叫。所以,我们正在使用一个没有实现的方法。
public class Dog implements Animal {
@Override
public void walk() {
}
@Override
public void bark() {
}
@Override
public void meow() throws ExecutionControl.NotImplementedException {
throw new ExecutionControl.NotImplementedException("Not implemented");
}
}
如果有一个将实现该接口的 Cat 类,将遇到同样的问题。猫不叫。
但是狗和猫会走路。
所以,可以通过分离接口来解决这个问题(创建接口Animal、Dog和Cat)。
依赖倒置原则。
根据 Uncle Bob 的说法,这个原则可以定义如下:
高级模块不应该依赖于低级模块,两者都应该依赖于抽象。
这是什么意思呢?
举个例子来说明这个定义的含义。
假设有一个名为 Cashback 的类:
package br.com.supercash.services.cashback;
import java.math.BigDecimal;
public class CashbackPersistence {
public void save(BigDecimal value, String clientAccount) {
//imagine calling a save method here
Log log = new Log("Log.txt");
log.print("client:" + clientAccount + " cashback saved: " + value);
}
}
所以,这个类有一个问题,实例化一个类 Log 并将一些特性传递给这个类。如果以后想将日志的名称更改为“Log.json”,例如,更改 Cashback 类。
不应该更改 Cashback 类中的日志文件的名称,因为这不是它的责任。
然后,解决方案之一是:
package br.com.supercash.services.cashback;
import java.math.BigDecimal;
public class CashbackPersistence {
public void save(BigDecimal value, String clientAccount) {
//imagine calling a save method here
Log log = new Log("Log.txt");
log.print("client:" + clientAccount + " cashback saved: " + value);
}
}
现在,创建实例和命名日志文件的责任在 CashbackPersistence 类外部(在构造函数中使用 Log 接口)。所以,颠倒了依赖的控制。
Ps:依赖倒置的解决方案之一就是依赖注入。
推荐书单
《项目驱动零起点学Java》
购买链接:https://u.jd.com/VtU5jYL
《项目驱动零起点学Java》贯穿6个完整项目,经过作者多年教学经验提炼而得,项目从小到大、从短到长,可以让读者在练习项目的过程中,快速掌握一系列知识点。
作者是国内知名Java教学者和传播者,一路披荆斩棘,兢兢业业20余年。积累了丰富的“培”“训”经验,也产出了很多优质的教学理论。
Java语言经过数十年的发展,体系逐渐变得庞大而复杂,本书芟繁就简,提炼出了最为重要的知识点,可以让读者轻松上手。本书配套有专栏课程,课程中提供了扩展内容。
《项目驱动零起点学Java》共分 13 章,围绕 6 个项目和 258 个代码示例,分别介绍了走进Java 的世界、变量与数据类型、运算符、流程控制、方法、数组、面向对象、异常、常用类、集合、I/O流、多线程、网络编程相关内容。《项目驱动零起点学Java》总结了马士兵老师从事Java培训十余年来经受了市场检验的教研成果,通过6 个项目以及每章的示例和习题,可以帮助读者快速掌握Java 编程的语法以及算法实现。扫描每章提供的二维码可观看相应章节内容的视频讲解。
精彩回顾
微信搜索关注《Java学研大本营》
访问【IT今日热榜】,发现每日技术热点