本章主要介绍软件设计七大原则
实现开闭原则的核心思想就是面向抽象编程而不是面向具体的实现编程。
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
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;
}
@Override
public Integer getId() {
return this.id;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
}
public class Test {
public static void main(String[]args){
ICourse javaCourse = new JavaCourse(96, "Java 从零开始到企业级开发", 348d);
System.out.println("课程Id:" + javaCourse.getId() + "课程名称:" + javaCourse.getName() + "课程价格:" + javaCourse.getPrice());
}
}
输出结果如下:
课程Id:96课程名称:Java 从零开始到企业级开发课程价格:348.0
现在的类结构图如图所示:
假如现在双十一的时候,要进行课程的打折活动:我们应该如何去做呢 ?
我们可以这样来做:
添加一个计算打折价格的方法:
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
Double getDiscountPrice();
}
同样实现类也要实现这个方法:
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;
}
@Override
public Integer getId() {
return this.id;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
@Override
public Double getDiscountPrice() {
return this.price*0.8;
}
}
Test类:
public class Test {
public static void main(String[]args){
ICourse javaCourse = new JavaCourse(96, "Java 从零开始到企业级开发", 348d);
System.out.println("课程Id:" + javaCourse.getId() + "课程名称:" + javaCourse.getName() + "课程价格:" + javaCourse.getPrice()+"双十一打折价格:"+javaCourse.getDiscountPrice());
}
}
输出结果如下:
课程Id:96课程名称:Java 从零开始到企业级开发课程价格:348.0双十一打折价格:278.40000000000003
缺点:假如课程很多,那么所有的课程的实现类都要重写一下方法,接口应该是稳定的,不应该是经常修改的。
我们再换一种方法:
我们写一个Java课程打折类并且继承于Java课程类,之前的课程类里面的价格就不打折了:
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
@Override
public Double getPrice() {
return super.getPrice()*0.8;
}
}
然后,我们在测试的时候,就是可以直接指向Java课程打折类的这个对象就可以了:
public class Test {
public static void main(String[]args){
ICourse javaCourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d);
System.out.println("课程Id:" + javaCourse.getId() + "课程名称:" + javaCourse.getName() + "课程价格:" + javaCourse.getPrice());
}
}
结果:这里有丢失精度的问题,可以使用String构造器的BigDecimal来解决
课程Id:96课程名称:Java 从零开始到企业级开发课程价格:222.72000000000003
如果我们还想要原价,我们可以这样来做:
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
/** 获取原价的方法 */
public Double getOriginPrice() {
return super.getPrice();
}
@Override
public Double getPrice() {
return super.getPrice()*0.8;
}
}
我们就是可以这样来调用:
public class Test {
public static void main(String[]args){
ICourse iCourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d);
/** 如果调用实现类里面的方法,我们就必须要进行强转一下 */
JavaDiscountCourse javaCourse = (JavaDiscountCourse)iCourse;
System.out.println("课程Id:" + javaCourse.getId() + "课程名称:" + javaCourse.getName() + "课程价格:"+javaCourse.getOriginPrice()+"课程折后价格:" + javaCourse.getPrice());
}
}
执行结果如下:
课程Id:96课程名称:Java 从零开始到企业级开发课程价格:348.0课程折后价格:278.40000000000003
类图如图所示:
我们通过了继承了基类,然后对其进行扩展,对扩展是开发的,而对修改接口和基类是关闭的;
越是基层的模块的修改影响的范围是越大的。
依赖倒置原则的核心就是:面向接口编程
有一个类:里面有两个方法,一个学习java课程的方法,一个是学习FE课程的方法:
public class Tom {
public void studyJavaCourse() {
System.out.println("Tom在学习Java课程");
}
public void studyFECourse() {
System.out.println("Tom在学习FE课程");
}
}
测试类:
public class Test {
public static void main(String[]args){
Tom tom = new Tom();
tom.studyJavaCourse();
tom.studyFECourse();
}
}
如果这个时候,我还想要添加一个学习Python课程的方法,这个时候,我可以在基类里面进行添加方法:
public class Tom {
public void studyJavaCourse() {
System.out.println("Tom在学习Java课程");
}
public void studyFECourse() {
System.out.println("Tom在学习FE课程");
}
public void studyPythonCourse() {
System.out.println("Tom在学习Python课程");
}
}
以上我们的做法就是在面向实现来进行编程,面向实现类来进行编程的话, 扩展性比较的差,这个就是依赖于底层的实现的
现在我们来引入抽象来解决这个问题:
public interface ICourse {
void studyCourse();
}
有两个实现类:一个是学习Java的实现类,一个是学习前端的实现类,分别是:
public class JavaCourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("Tom在学习Java课程");
}
}
public class FECourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("Tom在学习FE课程");
}
}
原来的Tom类,我们就要进行重构了,写了一个学习课程的方法,传入了学习课程的接口,由具体的实现类来进行实现:
具体学了哪些课程,是由具体的实现类来决定的:
public class Tom {
public void studyCourse(ICourse iCourse) {
iCourse.studyCourse();
}
}
我们来进行测试:
public class Test {
public static void main(String[]args){
Tom tom = new Tom();
tom.studyCourse(new JavaCourse());
tom.studyCourse(new FECourse());
}
}
执行结果为:
Tom在学习Java课程
Tom在学习FE课程
这个时候,如果还要学习Python的课程的话,那我们就可以再写上一个实现类来对接口进行实现即可:
public class PythonCourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("Tom在学习Python课程");
}
}
我们就可以来调用了:
public class Test {
public static void main(String[]args){
Tom tom = new Tom();
tom.studyCourse(new JavaCourse());
tom.studyCourse(new FECourse());
tom.studyCourse(new PythonCourse());
}
}
输出结果:
Tom在学习Java课程
Tom在学习FE课程
Tom在学习Python课程
以上是通过接口方法的方式来注入具体的实现;
当然,我们也可以通过构造器的方式来注入具体的实现:
在Tom这个类里面写一个构造器,把接口作为Tom类里面的一个成员属性,然后通过构造器来对其进行赋值:
public class Tom {
private ICourse iCourse;
public Tom(ICourse iCourse) {
this.iCourse = iCourse;
}
/** 这里的方法,就只需要调用类成员变量ICourse里面的studyCourse()方法就可以了 */
public void studyCourse() {
iCourse.studyCourse();
}
}
测试:
public class Test {
public static void main(String[]args){
Tom tom = new Tom(new JavaCourse());
tom.studyCourse();
}
}
测试结果:
Tom在学习Java课程
以上的用构造器来进行传递接口的实现,也不是很好,每次学一个新的课程的时候,还有重新new一个类;
这个时候,我们可以利用set方法来进行注入:
public class Tom {
private ICourse iCourse;
public void setiCourse(ICourse iCourse) {
this.iCourse = iCourse;
}
public void studyCourse() {
iCourse.studyCourse();
}
}
这个时候,我们就是可以这样来用了:
public class Test {
public static void main(String[]args){
Tom tom = new Tom();
tom.setiCourse(new JavaCourse());
tom.studyCourse();
tom.setiCourse(new FECourse());
tom.studyCourse();
}
}
执行结果:
Tom在学习Java课程
Tom在学习FE课程
现在的类图:
Bird类:
public class Bird {
public void mainMoveMode(String birdName) {
System.out.println(birdName+"用翅膀飞");
}
}
测试类:
public class Test {
public static void main(String[]args){
Bird bird = new Bird();
bird.mainMoveMode("大雁");
bird.mainMoveMode("鸵鸟");
}
}
输出结果:
大雁用翅膀飞
鸵鸟用翅膀飞
那么现在就是有问题的 ,鸵鸟不是用翅膀飞的。
这个时候,我们在原来Bird类里面进行扩展:这个时候,是不遵循单一职责原则的
public class Bird {
public void mainMoveMode(String birdName) {
if ("鸵鸟".equals(birdName)) {
System.out.println(birdName + "用脚走");
} else {
System.out.println(birdName+"用翅膀飞");
}
}
}
运行结果:
大雁用翅膀飞
鸵鸟用脚走
这个时候,我们按照职责的不同来进行拆分:
会飞的鸟:
public class FlyBird {
public void mainMoveMode(String birdName) {
System.out.println(birdName+"用翅膀飞");
}
}
用脚走路的鸟:
public class WalkBird {
public void mainMoveMode(String birdName) {
System.out.println(birdName+"用脚走");
}
}
测试:
public class Test {
public static void main(String[]args){
FlyBird flyBird = new FlyBird();
flyBird.mainMoveMode("大雁");
WalkBird walkBird = new WalkBird();
walkBird.mainMoveMode("鸵鸟");
}
}
执行结果:
大雁用翅膀飞
鸵鸟用脚走
现在的类图:
这个接口里面含有两个大块的功能:一个是获取课程的相关的信息,一个是对课程进行管理:
public interface ICourse {
/** 或者课程的相关的信息 */
String getCourseName();
byte[] getCourseVideo();
/** 课程管理上的 */
void studyCourse();
void refundCourse();
}
这个时候,我们就是可以对上面的接口进行一个拆分:
public interface ICourseContent {
/** 或者课程的相关的信息 */
String getCourseName();
byte[] getCourseVideo();
}
public interface ICourseManager {
/** 课程管理上的 */
void studyCourse();
void refundCourse();
}
们写一个实现类,来实现上面的两个接口:
public class CourseImpl implements ICourseManager,ICourseContent {
@Override
public String getCourseName() {
return null;
}
@Override
public byte[] getCourseVideo() {
return new byte[0];
}
@Override
public void studyCourse() {
}
@Override
public void refundCourse() {
}
}
上面说的就是接口层面上的单一职责原则;
我们定义一个类,里面写上两个方法:
public class Method {
private void updateUserInfo(String userName, String address) {
/** 这个是一个伪代码,传进来的进来的值进行一个更新 */
userName = "Tom";
address = "beijing";
}
private void updateUserInfo(String userName, String... properties) {
/** 这个是一个伪代码,传进来的进来的值进行一个更新 */
userName = "Tom";
}
}
上面定义的两个方法,是没有遵循单一职责原则的:
下面我们把方法拆成两个:
public class Method {
private void updateUserName(String userName) {
userName = "Tom";
}
private void updateUserAddress(String address) {
address = "beijing";
}
}
如果遇到下面的这种情况,我们一般也要把这个方法进行拆开:
就是有通过布尔值来进行判断的,那么我们一般也要把这个方法进行拆开,这个时候,就是能体现出定义职责原则的,这样的话,开发起来简单,维护起来也会更容易:
public class Method {
private void updateUserInfo(String userName, String address,boolean bool) {
if (bool) {
//todo something1
} else {
//todo something2
}
userName = "Tom";
address = "beijing";
}
}
在实际的开发当中,我们的接口和方法一定要做到定义职责的原则类的话 ,是要看实际的情况,不能过少的运用单一职责原则,也不能过多的使用单一职责原则,如果类过多的话,会引起来类的爆炸的现象;
我们先写上一个接口:
public interface IAnimalAction {
void eat();
void fly();
void swim();
}
狗的实现类:
public class Dog implements IAnimalAction {
@Override
public void eat() {
}
@Override
public void fly() {
/** 狗不会飞,这里是会有一个空的实现在这里 */
}
@Override
public void swim() {
}
}
我们再来写上一个鸟的实现类:
在这个实现类里面也是会有空的实现存在的
public class Bird implements IAnimalAction {
@Override
public void eat() {
}
@Override
public void fly() {
/** 鸟不一定会飞,比如鸵鸟 */
}
@Override
public void swim() {
/** 鸟不一定会游泳 */
}
}
我们可以对上面的接口来进行细化:
我们把上面的接口拆分成3个接口:
public interface IEatAnimalAction {
void eat();
}
public interface IFlyAnimalAction {
void fly();
}
public interface ISwimAnimalAction {
void swim();
}
这个时候,狗的实现类就是可以这样来写了:
public class Dog implements ISwimAnimalAction,IEatAnimalAction{
@Override
public void eat() {
}
@Override
public void swim() {
}
}
但是,我们在设计接口的时候,也不能分的太细,让接口过多;接口隔离原则在使用的时候,一定要适度,用的过多,或者过少都是不好的。
public class Course {
}
有一个负责人类,里面有一个得到所有课程的数量的方法:
public class TeamLeader {
public void checkNumberOfCourses(List courseList) {
System.out.println("在线课程的数量是:"+courseList.size());
}
}
还有一个Boss类,调用负责人的获取课程数量的方法:
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader) {
List courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
teamLeader.checkNumberOfCourses(courseList);
}
}
我们对其进行测试:
public class Test {
public static void main(String[]args){
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
boss.commandCheckNumber(teamLeader);
}
}
输出结果为:
在线课程的数量是:20
我们来对其进行分析:
在Boss这个类的下达指令的方法里面,除开入参 ,出参, 还有类里面的成员变量,这些变量称为朋友,其他的都不能称为朋友,在下达指令的这个方法里面不应该和Course的这个类有任何的交互,这里就是违背了迪米特法则。
我们从类图就可以清晰的看出
我们对其进行修改:
Boss类为:
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader) {
teamLeader.checkNumberOfCourses();
}
}
TeamLeader类:
public class TeamLeader {
public void checkNumberOfCourses() {
List courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
System.out.println("在线课程的数量是:"+courseList.size());
}
}
这个时候,就是符合迪米特法则的,Boss的这个类是不需要了解Course的这个类;和Course这个类直接发生关系的是TeamLeader的这个类;
我们来对其进行测试:
public class Test {
public static void main(String[]args){
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
boss.commandCheckNumber(teamLeader);
}
}
测试结果:
在线课程的数量是:20
这个时候的类图就是这样的,是符合迪米特法则的:
在使用迪米特法则的时候,我们要区分哪些是朋友,哪些不是朋友;
我们来描述一下长方形和正方形的关系:
长方形:
public class Rectangle {
private long length;
private long width;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
}
正方形:
public class Square extends Rectangle{
private long sidelength;
public long getSidelength() {
return sidelength;
}
public void setSidelength(long sidelength) {
this.sidelength = sidelength;
}
@Override
public long getLength() {
return getSidelength();
}
@Override
public void setLength(long length) {
setSidelength(length);
}
@Override
public long getWidth() {
return getSidelength();
}
@Override
public void setWidth(long width) {
setLength(width);
}
}
测试:
public class Test {
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth()+1);
System.out.println("width:"+rectangle.getWidth()+"length"+rectangle.getLength());
}
System.out.println("resize方法结束,width"+rectangle.getWidth()+"length"+rectangle.getLength());
}
/** 长方形 */
/* public static void main(String[]args){
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setLength(20);
resize(rectangle);
}*/
/** 正方形 */
public static void main(String[]args){
Square square = new Square();
square.setLength(10);
resize(square);
}
}
上面的实现是不符合里氏替换原则的;
我们来写一个四边形的接口:
public interface Quadrangle {
long getWidth();
long getLength();
}
让长方形这个类来实现上面的接口:
public class Rectangle implements Quadrangle{
private long length;
private long width;
@Override
public long getWidth() {
return width;
}
@Override
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public void setWidth(long width) {
this.width = width;
}
}
正方形:
public class Square implements Quadrangle{
private long sidelength;
@Override
public long getWidth() {
return sidelength;
}
@Override
public long getLength() {
return sidelength;
}
public long getSidelength() {
return sidelength;
}
public void setSidelength(long sidelength) {
this.sidelength = sidelength;
}
}
测试如下:
public class Test {
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth()+1);
System.out.println("width:"+rectangle.getWidth()+"length"+rectangle.getLength());
}
System.out.println("resize方法结束,width"+rectangle.getWidth()+"length"+rectangle.getLength());
}
/** 长方形 */
/* public static void main(String[]args){
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setLength(20);
resize(rectangle);
}*/
}
在实际开发当中,在继承的时候,一定要谨慎,使用接口对其进行约束;
我们还可以从方法的入参和方法的返回值来看;
有一个连接数据库的类:
public class DBConnection {
public String getConnection() {
return "MySQL的数据库连接";
}
}
有一个Dao层:
public class ProductDao extends DBConnection {
public void addProduct() {
String conn = super.getConnection();
System.out.println("使用"+conn+"增加产品");
}
}
测试:
public class Test {
public static void main(String[]args){
ProductDao productDao = new ProductDao();
productDao.addProduct();
}
}
执行结果:
使用MySQL的数据库连接增加产品
现在需要再用Oracle的数据库来进行连接;
我们对之前的连接的类进行修改,改成了抽象类:
public abstract class DBConnection {
public abstract String getConnection();
}
具体获取什么连接交给实现它的子类
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL数据库连接";
}
}
public class OracleConnection extends DBConnection{
@Override
public String getConnection() {
return "Oracle数据库连接";
}
}
在Dao层里面就是可以这样来写:
public class ProductDao {
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct() {
String conn = dbConnection.getConnection();
System.out.println("使用"+conn+"增加产品");
}
}
测试:
public class Test {
public static void main(String[]args){
ProductDao productDao = new ProductDao();
productDao.setDbConnection(new OracleConnection());
productDao.addProduct();
}
}
测试结果:
使用Oracle数据库连接增加产品
如果我们还有再进行扩展的话,那么我们就再写上一个类 ,然后去实现DBConnection里面的抽象方法,具体的实现由调用者自己去决定new哪一个实现的实例。
主要来源:geely老师设计模式视频
相关代码:
Github地址:https://github.com/CoderMerlin/java-base-learning/tree/master/java-design-learning
欢迎关注并点赞~