单一职责原则
概述
单一职责原则(SRP:Single responsibility principle)又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。
就一个类而言,应该仅有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。
如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。
问题由来
拿一个比较切合实际的例子来说明,现在我们做一个观看视频的网站,有以下那么几个要求
- 访客用户观看视频,只能看360P清晰度,并且需要观看1年的广告
- 普通用户观看视频,可以看480P清晰度,并且需要观看60秒的广告
- VIP用户观看视频,可以看1080P清晰度,并且不需要观看广告
那么我们在登录的时候每个用户访问观看视频接口的时候,就需要判断他的身份,如果用比较冗余的做法,如下所示
这种判断方法其实也可以实现我们需要的功能,首先从代码观感上来说,可能每个用户都不是一句代码可以搞定的,这样显得if-else方法太过复杂,另外如果后期需要对此进行维护的话,别人可能不太好理解
@Service
public class VideoUserService {
public void play(String userType){
if ("访客用户".equals(userType)){
System.out.println("访客用户,观看360P视频");
}else if ("普通用户".equals(userType)){
System.out.println("普通用户,观看480P视频");
}else if ("VIP用户".equals(userType)){
System.out.println("VIP用户,观看1080P视频");
}
}
}
@SpringBootTest
class Practice210ApplicationTests {
@Autowired
VideoUserService videoUserService;
@Test
void contextLoads() {
videoUserService.play("访客用户");
videoUserService.play("普通用户");
videoUserService.play("VIP用户");
}
}
解决办法
遵守单一职责原则,将不同的职责封装到不同的类或模块中
如下图所示,我们将属于用户的功能,封装到用户接口中,不同的用户统一实现该接口,重写该接口的方法用来操作
//用户接口
public interface IVideoUserService {
/**
* 清晰度
*/
void definition();
/**
* 广告时长
*/
void advertisement();
}
//访客用户服务类
@Service
public class VisitorVideoUserService implements IVideoUserService {
@Override
public void definition() {
System.out.println("访客用户,观看360P视频");
}
@Override
public void advertisement() {
System.out.println("访客用户,观看时长1年广告");
}
}
//普通用户服务类
@Service
public class OrdinaryVideoUserService implements IVideoUserService {
@Override
public void definition() {
System.out.println("普通用户,观看480P视频");
}
@Override
public void advertisement() {
System.out.println("普通用户,观看60秒广告");
}
}
//VIP用户服务类
@Service
public class VipVideoUserService implements IVideoUserService {
@Override
public void definition() {
System.out.println("VIP用户,观看1080P视频");
}
@Override
public void advertisement() {
System.out.println("VIP用户,不用观看广告");
}
}
//测试
@Test
void test1(){
IVideoUserService user = new VipVideoUserService();
user.definition();//VIP用户,观看1080P视频
user.advertisement();//VIP用户,不用观看广告
}
小黄在学习的时候还在思考另外一个问题,我们应该如何拿到用户的参数并进行判断,如果用户身份只有以上三种,我们写一个if-else循环也可以解决,但如果用户身份特别多,几百个上千个,我们则需要通过循环来解决问题
@Test
void contextLoads() {
//将所有用户类型放入list集合中
ArrayList<IVideoUserService> list = new ArrayList<>();
IVideoUserService visitor = new VisitorVideoUserService();
IVideoUserService ordinary = new OrdinaryVideoUserService();
IVideoUserService vip = new VipVideoUserService();
list.add(visitor);
list.add(ordinary);
list.add(vip);
//遍历该集合
IVideoUserService userService = new VipVideoUserService();
for (IVideoUserService service : list){
if (userService.getClass() == service.getClass()){
userService.definition();
userService.advertisement();
}
}
}
开闭原则
概述
在面向对象领域中,开闭原则规定“软件中的对象应该对于扩展事开放的,但对于修改时关闭的”,这意味着一个实体时允许在不改变它的源代码的前提下变更他的行为。总结十个字就是对扩展开放,对修改关闭
- 对扩展开放,意味着有新的需求或者变化时,可以对现有的代码进行扩展,以适应新的情况
- 对修改关闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对已有代码进行任何修改
为什么要遵循开闭原则
所有的产品,需求都是变换的,其实在学习整个设计模式的过程中,我们是为了实现软件的高内聚、低耦合,为了后期的维护、拓展更加的方便,开闭原则也是设计模式的核心思想
梅耶开闭原则
梅耶的定义提倡实现继承。具体实现可以通过继承方式来重用,但是接口规格不必如此。已存在的实现对于修改是封闭的,但是新的实现不必实现原有的接口。
案例分析1
现在有一个类,用于求各种图形的面积比如长方形、三角形、圆形
public interface ICalculationArea {
/**
* 长方形面积计算公式 S = x * y
* @param x 长
* @param y 宽
* @return
*/
double rectangle(double x,double y);
/**
* 三角形面积计算公式 S = √p * (p - a) * (p - b) * (p - c) 其中p = (a+b+c)/2
* @param a 边长
* @param b 边长
* @param c 边长
* @return
*/
double triangle(double a,double b,double c);
/**
* 圆形面积计算公式 S = π * r * r
* @param r 圆形半径
* @return
*/
double circle(double r);
}
public class CalculationArea implements ICalculationArea{
private final double π = 3.14D;
@Override
public double rectangle(double x, double y) {
return x * y;
}
@Override
public double triangle(double a, double b, double c) {
double p = (a + b + c) / 2;
return Math.sqrt(p * (p - a) * (p - b) * (p - c));
}
@Override
public double circle(double r) {
return π * r * r;
}
}
在大部分模块中,咱们都使用了以上求圆形面积方法,但是突然一个需求来了,在某个模块中,咱们需要将π的精度计算的更加仔细,很显然这种情况下不能改变CalculationArea类,我们可以使用继承的方式来重写圆形面积计算方法
public class CalculationAreaExt extends CalculationArea{
private final double π = 3.1415926D;
@Override
public double circle(double r) {
return π * r * r;
}
}
案例分析2
来看另外一个例子,我们实现卖书的例子,这个案例比较贴近现实,先看看一下基础代码
//图书类接口
public interface IBook {
//获取书籍名称
String getName();
//获取图书价格
int getPrice();
}
//小说书籍实现类
public class NovelBook implements IBook {
private String name;
private int price;
public NovelBook(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
}
//测试
@Slf4j
@SpringBootTest
class Practice220ApplicationTests {
@Test
void contextLoads() {
NovelBook novelBook = new NovelBook("99天环游地球", 199);
log.info("书籍名称:{},价格:{}",novelBook.getName(),novelBook.getPrice());
}
}
现在我们有一个需求,对于一些书籍,需要进行打折处理,我们应该如何修改上述代码?
- 修改IBook接口,增加一个方法getOffPrice(),这种方法是不可取的,对于接口来讲,一旦增加了方法,所有的实现类都需要去实现该方法,牵一发而动全身,这不符合开闭原则
- 修改NovelBook接口,修改getPrice(),这种方法直接导致了我们无法获取书的原价,不可取;增加一个方法getOffPrice(),这个方法可取,但我们回顾一下开闭原则:对扩展开放,对修改关闭,也就是说想要扩展功能时,不应该去修改源代码,而是使用添加代码的方式来实现新需求
- 定义一个OffNovelBook类,继承NovelBook,覆盖getPrice(),这种方法在现阶段看来时最优解,不需要去修改原来的代码,稳定性高
public class OffNovelBook extends NovelBook{
public OffNovelBook(String name, int price) {
super(name, price);
}
@Override
public int getPrice() {
//五折处理
return super.getPrice() / 2;
}
}
多态开闭原则
相比梅耶的使用方式,多态开闭原则的定义倡导对抽象基类的继承。接口规约可以通过继承来重用,但是实现不必重用。已存在的接口对于修改是封闭的,并且新的实现必须,至少,实现那个接口。
通俗来讲,多态开闭原则是通过实现接口的方式来实现的,因为在Java遵循单继承、多实现的规定,所以继承就显得尤为重要,能不使用继承的方式就不使用继承
案例分析
一个银行业务的例子,现在银行的业务有取款、存款、转账,我们通过普通方式来实现
public class BankBusiness {
public void operate(int type) {
if (type == 1) {
save();
} else if(type == 2) {
take();
} else if(type == 3) {
transfer();
}
}
public void save(){
System.out.println("存钱");
}
public void take(){
System.out.println("取钱");
}
public void transfer() {
System.out.println("转账");
}
}
现在银行越做越大了,新增了一个理财的功能,如果通过上述代码来实现,首先需要在BankBusiness新增一个if-else判断,然后要再定义一个理财的方法,很显然修改了原先的代码,不可取
通过以下代码,如果我们需要新增一个理财方法,再创建一个类实现Bussiness接口即可
仔细研究其实我们遵循了单一职责原则,六大原则都是相辅相成的
//服务接口
public interface Bussiness {
void operate();
}
//存钱
public class SaveBussiness implements Bussiness {
@Override
public void operate() {
System.out.println("存钱");
}
}
//取钱
public class TakeBussiness implements Bussiness {
@Override
public void operate() {
System.out.println("取钱");
}
}
//转账
public class TransferBussiness implements Bussiness {
@Override
public void operate() {
System.out.println("转账");
}
}
//服务类
public class BankBusiness {
public void operate(Bussiness bussiness){
bussiness.operate();
}
}
//测试
@Test
void test1(){
BankBusiness bankBusiness = new BankBusiness();
TakeBussiness takeBussiness = new TakeBussiness();
bankBusiness.operate(takeBussiness);
}
里氏替换原则
概述
里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。简而言之,任何基类出现的地方,子类一定可以出现,通俗来讲子类继承父类,可以在父类的基础上新增功能,但不能重写父类中的方法。
正方形不是长方形
不遵循里氏替换原则
这是一个里氏替换原则最经典的案例,按照人的思维来讲,正方形就是长方形的一种特殊的形式,所以我们可以将正方形看出长方形的一个子类
//长方形
public class Rectangle {
//长
private double length;
//宽
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
//正方形
public class Square extends Rectangle{
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setLength(width);
}
}
以上的代码看似没有什么问题,但实际上他是没有遵循里氏替换原则的,因为子类重写了父类的方法,在以下这种场景中,会陷入死循环
public class RectangleDemo {
//将长方形转化成正方形
public static void resize(Rectangle rectangle){
while (rectangle.getWidth() <= rectangle.getLength()){
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
//打印长方形的宽高
public static void print(Rectangle rectangle){
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setLength(20);
resize(rectangle);
print(rectangle);
System.out.println("==============");
Rectangle square = new Square();
square.setLength(20);
resize(square);//死循环
print(square);
}
}
遵循里氏替换原则
那么在遵循里氏替换原则的情况下,我们该如何构建这几个模块呢?抽象出一个四边形接口,让正方形、长方形都实现该接口
//四边形接口
public interface Quadrilateral {
double getLength();
double getWidth();
}
//长方形实现类
public class Rectangle implements Quadrilateral {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double getLength() {
return length;
}
@Override
public double getWidth() {
return width;
}
}
//正方形实现类
public class Square implements Quadrilateral {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
this.width = length;
}
public void setWidth(double width) {
this.width = width;
this.length = width;
}
@Override
public double getLength() {
return length;
}
@Override
public double getWidth() {
return width;
}
}
迪米特法则
概述
不知道大家有没有看过越狱这部美剧,这部美剧简直堪称经典,在越狱第四季中,一个特工就因为知道公司太多秘密,公司打算清除他,相当于使用kill命令。
言归正传,迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。
没有遵循迪米特法则
这里举一个校长、老师、学生的例子,如果按照现实生活中,校长应该即可以管理学生,又可以管理老师,这样我们的代码如下,在下面代码中,校长类中调用了教师类和学生类,这并不符合迪米特法则
//学生类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name; //姓名
private int grade; //总分
private int rank; //总排名
}
//教师类
@Data
@NoArgsConstructor
public class Teacher {
private String name; //姓名
private String clazz; //班级
private static List<Student> students; //班级里的学生
public Teacher(String name, String clazz) {
this.name = name;
this.clazz = clazz;
}
static {
students = new ArrayList<>();
students.add(new Student("张三",552,3));
students.add(new Student("李四",562,2));
students.add(new Student("王五",580,1));
students.add(new Student("小白",532,5));
students.add(new Student("小明",542,4));
}
public static List<Student> getStudents(){
return students;
}
}
//校长类
public class Rector {
private Teacher teacher = new Teacher("李华","高三2班");
//查询班级的信息
public Map<String,Object> queryClazzInfo(String clazzName){
int stuCount = Teacher.getStudents().size();
double totalScore = totalScore();
double average = averageScore();
HashMap<String, Object> map = new HashMap<>();
map.put("班级名称",teacher.getClazz());
map.put("老师",teacher.getName());
map.put("学生人数",stuCount);
map.put("总分",totalScore);
map.put("平均分",average);
return map;
}
//总分
private double totalScore(){
double totalGrade = 0;
for (Student s : Teacher.getStudents()){
totalGrade += s.getGrade();
}
return totalGrade;
}
//平均分
private double averageScore(){
double totalScore = totalScore();
double average = totalScore / Teacher.getStudents().size();
return average;
}
}
//测试
@Slf4j
@SpringBootTest
class Practive240ApplicationTests {
@Test
void contextLoads() {
Rector rector = new Rector();
Map<String, Object> map = rector.queryClazzInfo("高三2班");
for (Map.Entry<String,Object> entry : map.entrySet()){
log.info("{} : {}",entry.getKey(),entry.getValue());
}
}
}
遵循迪米特法则
我们只需要将计算学生的总分平均分等信息,在Teacher中实现即可
//教师类
@Data
@NoArgsConstructor
public class Teacher {
private String name; //姓名
private String clazz; //班级
private static List<Student> students; //班级里的学生
public Teacher(String name, String clazz) {
this.name = name;
this.clazz = clazz;
}
static {
students = new ArrayList<>();
students.add(new Student("张三",552,3));
students.add(new Student("李四",562,2));
students.add(new Student("王五",580,1));
students.add(new Student("小白",532,5));
students.add(new Student("小明",542,4));
}
public List<Student> getStudents(){
return students;
}
//总分
public double totalScore(){
double totalGrade = 0;
for (Student s : students){
totalGrade += s.getGrade();
}
return totalGrade;
}
//平均分
public double averageScore(){
double totalScore = totalScore();
double average = totalScore / students.size();
return average;
}
}
//校长类
public class Rector {
private Teacher teacher = new Teacher("李华","高三2班");
//查询班级的信息
public Map<String,Object> queryClazzInfo(String clazzName){
int stuCount = teacher.getStudents().size();
double totalScore = teacher.totalScore();
double average = teacher.averageScore();
HashMap<String, Object> map = new HashMap<>();
map.put("班级名称",teacher.getClazz());
map.put("老师",teacher.getName());
map.put("学生人数",stuCount);
map.put("总分",totalScore);
map.put("平均分",average);
return map;
}
}
接口隔离原则
概述
客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。通俗来讲,一个类实现一个接口,接口中的方法都需要全部重写,那么这个类一定要重写全部的方法,如果有一个重写的方法用不到,则不满足接口隔离原则
没有遵循接口隔离原则
举一个例子,在英雄联盟中有击飞、缓速、增幅等一些dBuff,这里我们举两个英雄,一个是石头人,一个是牛头,石头人拥有击飞、缓速的技能,而牛头拥有击飞、增幅的技能。
我们发现无论是石头人还是牛头,都没有实现Dbuff中所有的方法,这是不遵循接口隔离原则的
//DBuff接口
public interface DBuff {
//击飞
void strike();
//缓速
void retardance();
//增幅
void increases();
}
//石头人
public class Malphite implements DBuff {
@Override
public void strike() {
System.out.println("石头人释放了击飞技能");
}
@Override
public void retardance() {
System.out.println("石头人偷取了敌方的移速");
}
@Override
public void increases() {
//没有增幅的主动技能
}
}
//牛头
public class Alistar implements DBuff {
@Override
public void strike() {
System.out.println("牛头释放了击飞技能");
}
@Override
public void retardance() {
//没有缓速技能
}
@Override
public void increases() {
System.out.println("牛头增强了自身双抗");
}
}
遵循接口隔离原则
我们把三个DBuff分别展开成三个独立的接口,要实现该DBuff的对象,直接继承该接口即可。但这时候有些小伙伴就会问了,如果接口中有100个实现方法,难道要分成100个接口嘛?其实不然,我们只是说遵循接口隔离原则,而不是照猫画虎,在实际开发过程中,我们应该根据需求来设计接口
//击飞接口
public interface StrikeDBuff {
void strike();
}
//缓速接口
public interface RetardanceDBuff {
void retardance();
}
//增幅接口
public interface IncreasesDBuff {
void increases();
}
//石头人
public class Malphite implements StrikeDBuff, RetardanceDBuff {
@Override
public void strike() {
System.out.println("石头人释放了击飞技能");
}
@Override
public void retardance() {
System.out.println("石头人偷取了敌方的移速");
}
}
//牛头
public class Alistar implements StrikeDBuff, IncreasesDBuff {
@Override
public void strike() {
System.out.println("牛头释放了击飞技能");
}
@Override
public void increases() {
System.out.println("牛头增强了自身双抗");
}
}
依赖倒置原则
概述
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
依赖倒置原则就好像古代的皇帝下指令,皇帝就相当于接口,他只给你指明方向,告诉将军要去攻打哪里,将军接到指令后就做出实际行动
没有遵循依赖倒置原则
我们是一家研发自动驾驶技术的公司,现在福特、本田公司需要使用我们的自动驾驶技术
从下述代码来看,我们的核心技术是AutoSystem自动驾驶类,而如果后期我们的公司接到了宝马、奔驰等汽车品牌的订单后,需要修改AutoSystem类,这并不遵循依赖倒置原则
//本田
public class HondaCar{
public void Run(){
System.out.println("本田开始启动了");
}
public void Turn(){
System.out.println("本田开始转弯了");
}
public void Stop(){
System.out.println("本田开始停车了");
}
}
//福特
public class FordCar{
public void Run(){
System.out.println("福特开始启动了");
}
public void Turn(){
System.out.println("福特开始转弯了");
}
public void Stop(){
System.out.println("福特开始停车了");
}
}
//自动驾驶
public class AutoSystem {
public enum CarType{
Ford,Honda
};
private HondaCar hcar=new HondaCar();
private FordCar fcar=new FordCar();
private CarType type;
public AutoSystem(CarType type){
this.type=type;
}
public void RunCar(){
if(type==CarType.Ford){
fcar.Run();
} else {
hcar.Run();
}
}
public void TurnCar(){
if(type==CarType.Ford){
fcar.Turn();
} else {
hcar.Turn();
}
}
public void StopCar(){
if(type==CarType.Ford){
fcar.Stop();
} else {
hcar.Stop();
}
}
}
//测试
@SpringBootTest
class Practice260ApplicationTests {
@Test
void contextLoads() {
AutoSystem autoSystem = new AutoSystem(AutoSystem.CarType.Ford);
autoSystem.RunCar();
autoSystem.TurnCar();
autoSystem.StopCar();
}
}
遵循依赖倒置原则
解决办法也很简单,创建一个ICar接口,所有品牌的车需要上自动驾驶技术的,都需要实现该接口中的方法,而在AutoSystem类中,只需要使用ICar来定义即可
这样无论后期添加什么型号的汽车,都不需要动到我们的核心方法AutoSystem
//汽车接口
public interface ICar {
//启动
void Run();
//转弯
void Turn();
//停车
void Stop();
}
//福特汽车
public class FordCar implements ICar {
@Override
public void Run() {
System.out.println("福特开始启动了");
}
@Override
public void Turn() {
System.out.println("福特开始转弯了");
}
@Override
public void Stop() {
System.out.println("福特开始停车了");
}
}
//本田汽车
public class HondaCar implements ICar {
@Override
public void Run() {
System.out.println("本田开始启动了");
}
@Override
public void Turn() {
System.out.println("本田开始转弯了");
}
@Override
public void Stop() {
System.out.println("本田开始停车了");
}
}
//自动驾驶实现类
public class AutoSystem {
private ICar iCar;
public AutoSystem(ICar iCar) {
this.iCar = iCar;
}
public void run(){
iCar.Run();
}
public void turn(){
iCar.Turn();
}
public void stop(){
iCar.Stop();
}
}
//测试
@SpringBootTest
class Practice261ApplicationTests {
@Test
void contextLoads() {
ICar fordCar = new FordCar();
AutoSystem autoSystem = new AutoSystem(fordCar);
autoSystem.run();
autoSystem.turn();
autoSystem.stop();
}
}