Java面向对象7大设计原则
单一职责原则 SRP
一个类只有一个引起修改变化的原因,也就是只负责一个职责。核心思想:高内聚,低耦合
假设一个类有多个功能,当修改其中一个功能的时候,可能会对其他功能造成影响
优点:
1:降低类的复杂度,一个类负责一个职责,比负责多项职责要简单
2:代码的可读性提高了,也方便以后的维护
下面举例子说明,小明要学习一些课程,每个课程都有不同的知识点,JAVA学习面向对象,MYSQL学习sql语句
先建一个学习的类study:
public class Study {
public void studyCourse(String courseName){
if ("JAVA".equals(courseName)) {
System.out.println("小明在学习面向对象,new了一个girl");
} else if ("MYSQL".equals(courseName)) {
System.out.println("小明在学习mysql,查询girl没有查询到");
}
}
}
然后建一个测试类test:
public static void main(String[] args) {
Study study = new Study();
study.studyCourse("JAVA");
}
下面老师说了,我们马上要学习html了,怎么去修改代码呢,在加一个else if当然没有问题,可是随着课程的变化和增加,study类会越来越庞大,负责的职责越来越多,后期的维护和修改可能都会比较麻烦
那么,下面进行下修改,把每个课程提取出来:
public class JavaCourse {
public void studyJava(){
System.out.println("小明在学习面向对象,new了一个girl");
}
}
public class MysqlCourse {
public void studyMysql(){
System.out.println("小明在学习mysql,查询girl没有查询到");
}
}
再次修改测试类:
public static void main(String[] args) {
JavaCourse javaCourse = new JavaCourse();
javaCourse.studyJava();
MysqlCourse mysqlCourse = new MysqlCourse();
mysqlCourse.studyMysql();
}
这样每个类负责的职责就只有一个了,可是现在又增加需求了,学习完了要测验学习效果,小明需要交作业了,每个课程都要交一份作业,怎么做,在当前的类中加一个交作业的方法是可以的,还有更好的办法吗
可以把学习和作业抽象出来,建一个接口,然后每个课程自己去实现:
接口Course:学习接口
public interface Course {
public void study(String studentName);
}
接口HomeWork:作业接口
public interface HomeWork {
public void homeWork(String studentName);
}
JavaCourse类:
public class JavaCourse implements Course {
@Override
public void study(String studentName) {
System.out.println(studentName + "在学习面向对象,new了一个girl");
}
}
JavaHomeWork类:
public class JavaHomeWork implements HomeWork {
@Override
public void homeWork(String studentName) {
System.out.println(studentName + "交了一份java作业");
}
}
MysqlCourse类:
public class MysqlCourse implements Course {
@Override
public void study(String studentName) {
System.out.println(studentName + "在学习mysql,查询girl没有查询到");
}
}
MysqlHomeWork类:
public class MysqlHomeWork implements HomeWork {
@Override
public void homeWork(String studentName) {
System.out.println(studentName + "交了一份mysql作业");
}
}
类图:
修改之后,学习接口只负责学习相关的功能,作业接口只负责作业相关的功能,方便维护和修改,是不是更加清晰了呢
开闭原则 OCP
对模块类的操作,对修改关闭,对扩展开放,多年验证的代码,可能你一修改就是Bug
这个原则说的是,在设计一个模块的时候,应当使这个模块在不被修改的情况下被扩展,换而言之,可以在不修改源代码的情况下改变这个模块的行为
优越性:
1:通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新的需求,使得软件有一定的适应性和灵活性
2:已经有的软件模块,特别是比较重要的抽象模块不能再被修改,这就使得变化中的软件有一定的稳定性和延续性
怎样做到开闭原则:
1:抽象化
2:对可变性的封装
下面举例子说明:
商家卖苹果,平时的价格都不会发生变化,到了一些特定的节日就会打折或者买三斤送一斤,下面根据这个场景写代码:
SellApple接口:
public interface SellApple {
String getName();
BigDecimal getPrice();
}
Apple类:平时的苹果价格从这个类获取
public class Apple implements SellApple {
private String name;
private BigDecimal price;
public Apple(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
@Override
public String getName() {
return this.name;
}
@Override
public BigDecimal getPrice() {
return this.price;
}
}
如果我们直接修改apple这个里面的getPrice()方法,可能会对其他用到这个类的方法造成影响,那么我们重新建一个类来提供一个打折的方法:
public class SaleApple implements SellApple {
private String name;
private BigDecimal price;
public SaleApple(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
@Override
public String getName() {
return this.name;
}
@Override
public BigDecimal getPrice() {
return this.price.multiply(BigDecimal.valueOf(0.8));
}
}
这样修改之后不会影响到apple类中的价格,也扩展了我们现在需要用到的价格,这就是简单的开闭原则的使用
类图:
如果后面还有其他的价格优惠,可以直接再实现接口即可
里氏替换原则 LSP
超类可以被替换为其子类来使用,这样就能做到代码不用修改,能够任意切换不同实现的子类,少修改复用
里氏替换原则严格来表达是:
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型
换而言之,一个软件实体如果使用的是一个基类的话,那么一定适用其子类,而且它根本不能察觉出基类对象和子类对象的区别
里氏替换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正被复用,而衍生类也才能够在基类的基础上增加新的行为
白马与马:
《墨子 小取》中说到:白马,马也;乘白马,乘马也。骊马,马也;乘骊马,乘马也
这个的意思就是说不论白马、黑马都是马,那么马可以骑,当然白马、黑马都可以骑
在这里,马就是抽象的马,白马和黑马是具体的子类,一匹白马和一匹黑马是对应的实例,下面从代码角度看:
Horse类:
public class Horse {
public void rideHorse() {
System.out.println("骑马");
}
}
WhiteHorse类:
public class WhiteHorse extends Horse {
@Override
public void rideHorse() {
System.out.println("骑白马");
}
}
BlackHorse类:
public class BlackHorse extends Horse {
@Override
public void rideHorse() {
System.out.println("骑黑马");
}
}
下面进行测试:
public static void main(String[] args) {
ride(new Horse());
ride(new WhiteHorse());
ride(new BlackHorse());
}
public static void ride(Horse horse) {
horse.rideHorse();
}
可以看到,ride方法接收的参数是Horse,那么WhiteHorse和BlackHorse也可以接收,但是反过来的代换则不成立
public static void rideHorse(WhiteHorse horse) {
horse.rideHorse();
}
上面的代码如果传Horse则会报错
类图:
依赖倒置原则 DIP
程序依赖于抽象接口,不依赖于具体实现,降低实现模块间的耦合度,你不要来找(new)我,我会来找(set)你
依赖倒转原则讲的是:要依赖于抽象,不要依赖于具体
什么是依赖倒转原则:
1:抽象不应该依赖于细节,细节应该依赖于抽象
2:要针对接口编程,不要针对实现编程
抽象层次包含的是应用系统的商务逻辑以及对整个系统来说非常重要的决定,是必然性的体现;而具体层次则包含了一些与算法和实现相关的逻辑,具体层次的代码是会经常变动的,如果抽象层级依赖具体层次,就是微观影响了宏观,这是很荒唐的
下面从一个例子入手:
淘宝、京东双11大促销,有很多的优惠活动,比如满减、打折等,顾客可以从中选择多个优惠活动来买买买
下面是一个商品,有满减和打折活动
public class Order {
public void fullReduction() {
System.out.println("满500减100");
}
public void reducedRate() {
System.out.println("满3件打7折");
}
}
public static void main(String[] args) {
Order order = new Order();
order.fullReduction();
order.reducedRate();
}
由于商品买的人较多,商家想出一个新的活动,每满200元减100和满3件最后一件最低价,那么要实现的话,就需要去修改上面的类,但是呢如果遇到别的节假日,优惠活动又不一样,这样一直改动代码是非常不科学的,而且上面的实现也是面对实现编程,而不是面向接口编程,下面改动下代码:
定义一个卖商品促销优惠的接口:
public interface SellOrder {
void sellOrder();
}
满减实现:
public class Discounts1 implements SellOrder {
@Override
public void sellOrder() {
System.out.println("满500减100");
}
}
打折实现:
public class Discounts2 implements SellOrder {
@Override
public void sellOrder() {
System.out.println("满3件打7折");
}
}
修改后的Order类:
public class Order {
public void sellOrder(SellOrder order) {
order.sellOrder();
}
}
测试类:
public static void main(String[] args) {
Order order = new Order();
order.sellOrder(new Discounts1());
order.sellOrder(new Discounts2());
}
上面一种方式是通过方法注入的,还有构造器注入和setter注入
构造器注入:
修改Order类:
public class Order {
public SellOrder sellOrder;
public Order(SellOrder sellOrder) {
this.sellOrder = sellOrder;
}
public void sellOrder() {
this.sellOrder.sellOrder();
}
}
public static void main(String[] args) {
Order order1 = new Order(new Discounts1());
order1.sellOrder();
Order order2 = new Order(new Discounts2());
order2.sellOrder();
}
可以看到,构造器注入,每次都创建一个新的实例,如果SellOrder是单例的话,就需要通过setter注入
setter注入:
修改Order类:
public class Order {
public SellOrder sellOrder;
public void setSellOrder(SellOrder sellOrder) {
this.sellOrder = sellOrder;
}
public void sellOrder() {
this.sellOrder.sellOrder();
}
}
public static void main(String[] args) {
Order order = new Order();
order.setSellOrder(new Discounts1());
order.sellOrder();
order.setSellOrder(new Discounts2());
order.sellOrder();
}
类图:
接口隔离原则 ISP
不该强迫客户程序依赖不需要使用的方法,一个接口只提供对外的功能,不是把所有功能都封装进去,减少依赖范围
优点:
1:整洁
2:系统的可维护性
下面通过一个例子说明,比如狗和鸟都需要吃东西,那么我们需要定义一个接口,然后狗和鸟分别去实现
先定义一个接口Animal:
public interface Animal {
void eat();
}
两个实现类:
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("狗吃肉");
}
}
public class Bird implements Animal {
@Override
public void eat() {
System.out.println("鸟吃虫子");
}
}
下面要求了,需要实现狗去游泳,那么我们直接在Animal接口中增加一个接口:
public interface Animal {
void eat();
void swimming();
}
@Override
public void eat() {
System.out.println("狗吃肉");
}
@Override
public void swimming() {
System.out.println("狗在游泳");
}
}
public class Bird implements Animal {
@Override
public void eat() {
System.out.println("鸟吃虫子");
}
@Override
public void swimming() {
}
}
这时发现了,鸟不会游泳,它的实现类里面这个方法是空的,这样就违背了接口隔离原则了,下面进行改造
定义两个接口,一个吃东西的接口和一个游泳的接口:
public interface AnimalEat {
void eat();
}
public interface AnimalSwimming {
void swimming();
}
狗的实现类:
public class Dog implements AnimalEat,AnimalSwimming {
@Override
public void eat() {
System.out.println("狗吃肉");
}
@Override
public void swimming() {
System.out.println("狗在游泳");
}
}
鸟的实现类:
public class Bird implements AnimalEat {
@Override
public void eat() {
System.out.println("鸟吃虫子");
}
}
这样,鸟的实现类中就可以不用去依赖它不需要的接口了
类图:
组合复用原则 CRP
尽量使用组合,而不是继承来达到复用的目的,继承强耦合,组合低耦合,组合还能运行时动态替换
组合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的
另一种表述:要尽量使用组合,尽量不要使用继承
好处:
1:新对象存取已有对象的唯一方法是通过已有对象的接口
2:这种复用是黑箱复用,因为已有对象的内部细节是新对象所看不见的
3:支持包装
4:所需要的依赖较少
5:每一个新的类可以将焦点集中在一个任务上
6:可以在运行时动态进行,新对象可以动态的引用与已有对象类型相同的对象
人在生活中有很多种角色,在家里是子女,在学校是学生,在公司是职员,那么如果使用继承,直接继承职员类,那么我就不能再是子女或者学生了,这显然不合理,那么通过组合原则,就可以一个人拥有多种角色
顶级接口:
public interface People {
void hashRole();
}
学生、职员、子女实现类:
public class Student implements People {
@Override
public void hashRole() {
System.out.println("我是一名学生");
}
}
public class Clerk implements People {
@Override
public void hashRole() {
System.out.println("我是一个公司职员");
}
}
public class Children implements People {
@Override
public void hashRole() {
System.out.println("我是父母的儿子或女儿");
}
}
我具有的角色类:
public class My {
private Student student;
private Clerk clerk;
private Children children;
public void setMy(Student student, Clerk clerk, Children children) {
this.student = student;
this.clerk = clerk;
this.children = children;
}
public void myRole() {
System.out.println("我的角色:");
this.student.hashRole();
this.clerk.hashRole();
this.children.hashRole();
}
}
测试类:
public class Test {
public static void main(String[] args) {
My my = new My();
my.setMy(new Student(), new Clerk(), new Children());
my.myRole();
}
}
类图:
迪米特法则 LOD
一个对象应当对其他对象有尽可能少的了解,不和陌生人说话,降低各个对象之间的耦合,提高系统的可维护性
各种表述:
1:只与你直接的朋友们通信
2:不要和陌生人说话
3:每一个软件单位对其他软件单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位
狭义的迪米特法则:
如果两个类不需要彼此之间直接通信,那么这两个类就不应该发生直接的相互作用,如果一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用
什么样的才可以称为朋友:
1:当前对象本身(this)
2:以参量的形式传入到当前对象方法中的对象
3:当前对象的实例变量直接引用的对象
4:当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
5:当前对象所创建的对象
任何一个对象如果满足上面的条件之一,就可以称为朋友,否则就是陌生人
迪米特法则主要的用意是控制信息的过载,在做系统设计时需要注意一下几点:
1:在类的划分上,应当创建有弱耦合的类,类之间的耦合越弱,越有利于复用,一个弱耦合的类被修改了,不会波及到其有关联的类
2:每一个类都应该降低成员的访问权限
3:一个类应该设计成不变类
4:在对其他类的引用上,一个对象对其对象的引用应该降到最低
举个例子,小明和小红是朋友,小刚只认识小红,现在小明有事情需要小刚帮忙去做,那么小明不应该直接去找小刚,而是找到小红,让小红找到小刚,然后去完成事情
public class XiaoMing {
public void xiaoMing(Xiaohong xiaohong) {
System.out.println("我是小明,找:" + xiaohong);
xiaohong.xiaohong();
}
}
public class XiaoHong {
public void xiaohong() {
System.out.println("我是小红,你要找我吗");
XiaoGang xiaoGang = new XiaoGang();
xiaoGang.xiaoGang();
}
}
public class XiaoGang {
public void xiaoGang() {
System.out.println("我是小刚。小红在找我");
}
}
测试类:
public class Test {
public static void main(String[] args) {
XiaoHong xiaohong = new XiaoHong();
XiaoMing xiaoMing = new XiaoMing();
xiaoMing.xiaoMing(xiaohong);
}
}
类图:
到此,7个设计原则已经表述完了,学习设计原则是为了写出更好更优秀的代码,但是也不需要刻意的去准守