七大设计原则
1.开闭原则
2.依赖倒置原则
3.单一职责原则
4.接口隔离原则
5.迪米特法则
6.里氏替换原则
7.合成复用原则
开闭原则
开闭原则(Open-Closed Principle, OCP)是指一软件实体如类、模块和函数应该对扩展开放,
对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。强调的是用抽象构建框架,用实现扩展细节。可以提高软件系统的可复用性及可维护性。开闭原则,是面向对象设计中最基础的设计原则。它指导我们如何建立稳定灵活的系统,例如:我们版本更新,我尽可能不修改源代码,但是可以増加新功能。
下面我们用一个例子来理解开闭原则:
先写一个课程接口,定义公共使用的方法
/**
* 定义个课程接口,定义公共方法
* getName() 课程名称
* getPrice() 课程价格
*/
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
实现一个java课程类实现公共课程接口
/**
* 实现一个java课程类实现公共课程接口
*/
public class JavaCourse implements ICourse{
private Integer id;
private String name;
private Double price;
public JavaCourse(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
public Integer getId() {
return this.id;
}
public String getName() {
return this.name;
}
public Double getPrice() {
return this.price;
}
}
写一个main方法测试类
在main方法中初始化一个java课程信息并输出
public static void main(String[] args) {
ICourse course = new JavaCourse(1,"java课程",100D);
System.out.println("课程ID:" + course.getId() +
"\n课程名称:《" + course.getName() +"》" +
"\n课程价格:" + course.getPrice()
);
}
现在我们模拟一个场景 比如打折
更改java课程类实现公共课程接口
/**
* 实现一个java课程类实现公共课程接口
*/
public class JavaCourse implements ICourse {
private Integer id;
private String name;
private Double price;
public JavaCourse(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
public Integer getId() {
return this.id;
}
public String getName() {
return this.name;
}
public Double getPrice() {
return this.price * 0.6;
}
}
执行main方法得到结果
这样写暴露出一个问题,就是每次使用不同的折扣都需要修改一次JavaCourse类,也拿不到原价,还会因为经常打折会存在一定风险,也可能影响其他地方调用结果,如果不修改原有代码前提下,实现优惠功能呢?现在我们重写一个打折类,在类中实现细节:
此时,原来的代码我们就不要去修改了,关闭原来代码的修改,扩展原代码,通过继承原代码类实现需求细节的修改。让我们的项目代码风险可控。
/**
* 现在我们重写一个打折类,在类中实现细节
*/
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getDiscountPrice(){
return super.getPrice() * 0.6;
}
}
再修改main测试类
public static void main(String[] args) {
ICourse iCourse = new JavaDiscountCourse(1,"java课程",100D);
JavaDiscountCourse discountCourse = (JavaDiscountCourse)iCourse;
System.out.println("课程ID:" + discountCourse.getId() +
"\n课程标题:《" + discountCourse.getName() + "》" +
"\n原价:" + discountCourse.getPrice() +
"\n售价:" + discountCourse.getDiscountPrice());
}
得到结果
现在打折例子完成了,我们只是增加了一个JavaDiscountCourse 类,我们没有修改底层模块,代码改变量少,可以有效的防止风险的扩散。
如果要修改代码中某些逻辑实现细节的变化,尽量避开低层代码的逻辑,如果要修改低层代码,你要确定都有哪些高层次的代码使用了这些低层次代码,做统一的修改,低层次的代码的修改,肯定会引起高层次引用代码的变化,牵一发而动全身。所以尽量使用开闭原则,对原低层次代码进行扩展实现。
开闭原则是最基础的设计原则,其他五个原则都是以开闭原则为基础,你也可以这么理解:开闭原则是抽象类,其他五个设计原则是具体实现类,是指导设计的工具和方法。
依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。通过依赖倒置,可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所造成的风险。
下面我们用一个例子来理解依赖倒置原则:
我们还是已课程为例,定义使用的方法
public class Eric {
public void studyJavaCourse(){
System.out.println("Eric正在学习Java课程");
}
public void studyPythonCourse(){
System.out.println("Eric正在学习Python课程");
}
}
我们来调用一下
public static void main(String[] args) {
Eric eric = new Eric();
eric.studyJavaCourse();
eric.studyPythonCourse();
}
结果
Eric 目前正在学习java跟Py 课程,随着学习兴趣,现在Eric还想学习php,这个时候业务扩展,我们的代码要从底层到高层一次修改代码,在Eric类中增加学习课程,在调用层也要追加,如此每次需求变化都要修改代码,稳定性和可扩展性较差。在修改代码也会带来意向不到的风向,接下来我们优化一下代码,创建一个抽象课程接口
/**
* 创建一个抽象课程接口.
*/
public interface ICourse {
void study();
}
我们分别去实现这个接口
public class JavaCourse implements ICourse {
public void study() {
System.out.println("Eric正在学习Java课程");
}
}
public class PythonCourse implements ICourse {
public void study() {
System.out.println("Eric正在学习Python课程");
}
}
在修改Eric类
public class Eric {
public void study(ICourse course){
course.study();
}
}
我们再来调用
public static void main(String[] args) {
Eric eric = new Eric();
eric.study(new JavaCourse());
eric.study(new PythonCourse());
}
这个时候无论Eric在怎么学新的课程,我们只需要新建一个类,通过传参的方式告诉Eric,而不需要修改底层的代码,实际这个一种大家都熟悉的方式 叫依赖注入,注入方式还有构造器注入跟Setter方式注入,我们在来看下构造器方式注入。
public class Eric {
private ICourse iCourse;
public Eric(ICourse iCourse) {
this.iCourse = iCourse;
}
public void study() {
iCourse.study();
}
}
调用
public static void main(String[] args) {
Eric eric = new Eric(new JavaCourse());
eric.study();
}
}
根据构造器注入,在调用时,每次都要创建实例。
如果Eric是全局单例,则我们就只能选择Setter方式注入。我们继续修改代码
public class Eric {
private ICourse iCourse;
public void setiCourse(ICourse iCourse) {
this.iCourse = iCourse;
}
public void study() {
iCourse.study();
}
}
我们在来调用代码
public static void main(String[] args) {
Eric eric = new Eric();
eric.setiCourse(new JavaCourse());
eric.study();
eric.setiCourse(new PythonCourse());
eric.study();
}
}
我们在来看下最终的类图
单一职责原则
单一职责(Simple Responsibility Pinciple, SRP)是指不要存在多于一个导致类变更的原因。假设我们有4 Class负责两个职责,一旦发生需求变更,修改其中4职责的逻辑代码,有可能会导致
另一个职责的功能发生故障。这样一来,这个Class存在两个导致类变更的原因。如何解决这个问题呢? 我们就要给两个职责分别用两个Class来实现,进行解耦。后期需求变更维护互不影响。这样的设计, 可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说就是一个 Class/lnterface/Method 只负责一项职责
我们以一个方法级别的例子来理解
例子为假设我们对一门新课的直播课和录播课进行不同的加密或其他操作,如果仅输出是否是直播课或录播课,一个类中判断没问题,如果进行加密或权限其他操作,则要遵循单一职责原则,拆分到不同的类中分别处理,会使代码更简洁更易读
public class Course {
public void study(String courseName){
if("直播课".equals(courseName)){
System.out.println("不能快进");
}else{
System.out.println("可以任意的来回播放");
}
}
}
public static void main(String[] args) {
Course course = new Course();
course.study("直播课");
course.study("录播课");
}
从上面代码来看,Course类承担了两种处理逻辑。假如,现在要对课程进行加密,那么直播课和录播课的加密逻辑都不一样,必须要修改代码。而修改代码逻辑势必会相互影响容易造成不可控的风险。我们对职责进行分离解藕,来看代码,分别创建两个类ReplayCourse和LiveCourse
public class LiveCourse {
public void study(String courseName){
System.out.println("不能快进");
}
}
public class ReplayCourse {
public void study(String courseName){
System.out.println("可以任意的来回播放");
}
}
调用代码
public static void main(String[] args) {
LiveCourse liveCourse = new LiveCourse();
liveCourse.study("直播课");
ReplayCourse replayCourse = new ReplayCourse();
replayCourse.study("录播课");
}
结果
随着业务发展,课程要做权限,没有付费的同学可以看到课程基本信息。付费的学院可以获得视频流,那么对控制课程层面最少有二个职责,我们可以把展示职责跟管理职责分离开都实现同一个抽象,设计一个顶级的接口
/**
* 设计一个顶级的接口
*/
public interface ICourse {
//获取基本信息
String getCourseName();
//获取视频流
byte[] getCourseVideo();
//学习课程
void studyCourse();
//退款
void refundCourse();
...
}
然后我们把这个顶级接口拆分2个接口 创建
public interface ICourseInfo {
String getCourseName();
byte[] getCourseVideo();
}
public interface ICourseManager {
void studyCourse();
void refundCourse();
}
实现多接口
public class CourseImpl implements ICourseInfo,ICourseManager {
public String getCourseName() {
return null;
}
public byte[] getCourseVideo() {
return new byte[0];
}
public void studyCourse() {
}
public void refundCourse() {
}
}
我们先来看下类图
下面我们看一下方法层面的单一职责,有时候我们为了偷懒,通常会把一个方法写成下面这样
private void modifyUserInfo(String userName,String address){
userName = "Eric";
address = "Dalian";
}
private void modifyUserInfo(String userName,String ... fileds){
}
private void modifyUserInfo(String userName,String address,boolean bool){
if(bool){
}else{
}
}
上面的三个方法中都承担了多个职责,即可以修改username 也可以修改addr 等等 明显不符合单一职责,那么我们可以修改成
private void modifyUserName(String userName){
userName = "Eric";
}
private void modifyAddress(String address){
address = "Dalian";
}
这样修改之后 开发起来简单,维护也容易,但是我们实际开发中会依赖项目依赖,组合等等这些关系,还有项目的规模,周期,人员水平 对进度的把控,很多类不符合单一职责,但是我们在编写代码中,尽可能的让接口,方法保持单一职责,对我们项目后期维护有很大的帮助
接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP)是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。这个原则指导我们在设计接口时应当注意一下几点:
1、 一对一类的依赖应该建立在最小的接口之上。
2、建立单一接口,不要建立庞大臃肿的接口。
3、尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度)o
接口隔离原则符合我们常说的高内聚低藕合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。我们在设计接口的时候,要多花时间去思考,要考虑业务模型,包括以后有可能发生变更的地方还要做一些预判。
接口隔离原则与单一职责原则的审视角度不相同。单一职责原则要求是类和接口的职责单一,注重的是职责,这是业务逻辑上的划分。接口隔离原则要求接口的方法尽量少。
所以,对于抽象,对业务模型的理解是非常重要的。下面我们来看一段代码,
写一个动物行为的抽象
/**
* 一个动物行为的抽象
*/
public interface IAnimal {
void eat();
void fly();
void swim();
}
/**
* 鸟儿的实现
*/
public class Bird implements IAnimal {
public void eat() {}
public void fly() {}
public void swim() {}
}
/**
* 狗的实现
*/
public class Dog implements IAnimal {
public void eat() {}
public void fly() {}
public void swim() {}
}
通过上面代码我们可以分析出,Bird的swim()方法我们只能空着,Dog的Fly()方法貌似也做不到,这时候我们就要针对不同的动物来设计不同的接口 分别是
public interface IEatAminal {
void eat();
}
public interface IFlyAminal {
void fly();
}
public interface ISwimAminal {
void swin();
}
public class Dog implements ISwimAminal,IEatAminal {
public void eat() {}
public void swin() {}
public class Bird implements IEatAminal,IFlyAminal {
public void eat() {}
public void fly() {}
}
分别去实现 我们来对比下类图
这样我们就很清楚彼此的关系
迪米特法则
迪米特原则(Law of Demeter LoD)只指一个对象对其他对象保持最少的了解,也叫最少知道原则。尽量降低类与类之间的耦合,主要强盗之和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类, 而出现在方法体内部的类不属于朋友类。
现在来设计一个权限系统,TeamLeader需要查看目前发布到线上的课程数量。这时候,TeamLeader 要找到员工Employee去进行统计,Employee再把统计结果告诉TeamLeader。接下来我们还是来看代码:
public class Course {
}
public class Employee {
public void checkNumberOfCourses(List<Course> courseList){
System.out.println("目前已发布的课程数量是:"+ courseList.size());
}
}
public class TeamLeader {
public void commandCheckNumber(Employee employee){
List<Course> courseList =new ArrayList<Course>();
for (int i= 0; i < 20 ;i ++){
courseList.add(new Course());
}
employee.checkNumberOfCourses(courseList);
}
}
//调用
public static void main(String[] args) {
TeamLeader teamLeader = new TeamLeader();
Employee employee = new Employee();
teamLeader.commandCheckNumber(employee);
}
结果
写到这里,其实功能已经都已经实现,代码看上去也没什么问题。根据迪米特原则,TeamLeader只想要结果,不需要跟Course产生直接的交流。而Employee统计需要弓I用Course对象。TeamLeader 和Course并不是朋友,从下面的类图就可以看出来
下面我们改造下代码
public class Employee {
public void checkNumberOfCourses() {
List<Course> courseList = new ArrayList<Course>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
System.out.println("目前已发布的课程数量为:" + courseList.size());
}
}
public class TeamLeader {
public void commandCheckNumber(Employee employee){
employee.checkNumberOfCourses();
}
}
我们来看下,现在Course 跟TeamLeader 就没有关联了
但是大家要主要不能强迫症,碰到业务复制的场景 要随机应变
里氏替换原则
里氐替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为T1的对象o 1,都有类型为T 2 的对象02 , 使得以T 1 定义的所有程序P在所有的对象o 1 都替换成02 时, 程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
定义看上去还是比较抽象,我们重新理解一下,可以理解为一软件实体如果适用一^父类的话,
那一定是适用于其子类,所有弓I用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而酿逻辑不变。根据这个醐,我们雜一下:
引申含义:子类可以扩展父类的功能,但不能改变父类原有的功能。
1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2、子类中可以増加自己特有的方法。
3、当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入織更宽松。
4、当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。
在前面说开闭原则的时候埋下了一个伏笔,我们记得在获取折后时重写覆盖了父类的getPriceO方法,増加了1 获取源码的方法getOriginPriceO,显然就违背了里氏替换原则。所以我们,不应该覆盖getPriceO方法,増加getDiscountPriceO方法,忘了的同学往上翻
使用里氏替换原则有以下优点
1.提高代码的重用性,子类拥有父类的方法和属性;
2. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;
我们来描述个业务场景 用正方形,矩形和四边形的关系说明下原则,我们都知道正方形是一个特殊的长方形,那么创建一个长方形的父类
/**
* 长方形的父类
*/
public class Rectangle {
private long height;
private long width;
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
}
创建正方形类继承长方形
public class Square implements Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public long getWidth() {
return length;
}
public long getHeight() {
return length;
}
}
在测试类创建resize方法 跟据逻辑长方形的宽应该大于等于高,我们让高一直自增,直到等于宽变成正方形
public class IspTest {
public static void resize(Rectangle rectangle){
while (rectangle.getWidth() >= rectangle.getHeight()){
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
System.out.println("Resize End,Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
测试代码
public static void main(String[] args) {
Square square = new Square();
square.setLength(10);
resize(square);
}
运行结果
发现高比宽还大了,在长方形是一种正常情况,现在我们再来看下面的代码,把长方形的React替换成它的子类正方形
public static void main(String[] args) {
Square square = new Square();
square.setLength(10);
resize(square);
}
结果
这时候我们运行的时候就出现死循环,违背了里氏替换原则,将父类替换为子类后,程序运行结果没有达到预期,因此我们的设计的代码有存在一定的风险、 里氏替换原则只存在父类与子类之间约束继承泛滥。我们在来创建基于长方形与正方形共同抽象四边形 接口
/**
* 基于长方形与正方形共同抽象四边形
*/
public interface QuadRangle {
long getWidth();
long getHeight();
}
/**
* 修改长方形的父类
*/
public class Rectangle implements QuadRangle {
private long height;
private long width;
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
}
修改正方形
public class Square implements QuadRangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public long getWidth() {
return length;
}
public long getHeight() {
return length;
}
}
此时我们如果把resize方法的参数换成QuadRangle 类方法内部就会报错。因为正方形没有SetWidth()跟get方法了,因此为了约束继承泛滥。resize的方法参数只能用Rectanle长方形
合成复用原则
合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/ 聚合(contanis-a),而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一^的变化对其他类造成的影晌相对较少。
继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类。组合價合也称之为黑箱复用,对类以外的对象是无法获取到实现细节的。要根据具体的业务场景来做代码设计,其实也都需要遵循OOP模型。还是以数据库操作为例,先来创建DBConnection类
public class DBConnection {
public String getConnection(){
return "获取MySQL数据连接";
}
}
创建ProductDao类
public class ProductDao {
private DBConnection dbConnection;
public void setConnection(DBConnection dbConnection){
this.dbConnection = dbConnection;
}
public void addProduct(){
String conn = dbConnection.getConnection();
System.out.println("获得数据库连接");
}
}
这就是一种非常典型的合成复用原则应用场景。但是,目前的设计来说,DBConnection还不是一种抽象,不便于系统扩展。目前的系统支持MySQL数据库连接,假设业务发生变化,数据库操作层要支持Oracle数据库。当然, 我们可以在DBConnection中増加对Oracle数据库支持的方法。但是违背了开闭原则。其实,我们可以不必修改Dao的代码,将DBConnection修改为abstract,来看代码
public abstract class DBConnection {
public abstract String getConnection();
}
然后将mysql逻辑抽离
public class MySQLConnection extends DBConnection {
public String getConnection() {
return "获取MySQL数据连接";
}
}
在创建支持Oracle支持逻辑
public class MyOracleConnection extends DBConnection {
public String getConnection() {
return "获取Oracle数据连接";
}
}
具体选择交给应用层,看下类图
设计原则总结:
在实际开发中,并不是一定要求所有带的都遵循设计原则,我们要考虑人力。空间,成本,质量等等 不要刻意追求完美,要适当追寻设计原则,体现的是一种平衡取舍,帮助我们设计更加优雅的代码。
未完待续。。。。。。。。新篇章 设计模式