开闭原则
定义
:指一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
核心思想
:用抽象构建框架,面向抽象编程,用实现扩展细节,也是面向对象中最基础的设计原则
样例:
tx课堂有很多课程可以学习,例如:java,golang等等,首先创建一个课程体系的接口ITXCourse
// TX课堂抽象接口
// 不打广告,是确实在用的平台
public interface ITXCourse {
// 课程id
Integer getId();
// 课程名称
String getName();
// 课程价格
Double getPrice();
}
接下来,我们看下java的课程,创建一个java的课程类
// java教程
public class JavaCourse implements ITXCourse{
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;
}
}
还有golang的课程类
public class GoLangCourse implements ITXCourse{
private Integer id;
private String name;
private Double price;
public GoLangCourse(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;
}
}
假设毕业季到了,针对java课程需要打八折促销,又不想直接修改JavaCourse 类中的getPrice,防止有未知影响,这是可以重新创建一个java课程的促销类,继承JavaCourse 实现促销的扩展
public class JavaCoursePromotion extends JavaCourse {
public JavaCoursePromotion(Integer id, String name, Double price) {
super(id, name, price);
}
// 促销价格,打了八折
public Double getPrice(){
return super.getPrice() * 0.8;
}
}
总结:开闭原则通俗的说是指,可以自由扩展,并且不会影响到原有的代码,如果扩展功能需要改动之前代码就已经违背了对扩展开放,对修改关闭
依赖倒置原则
定义
:设计代码结构时,高层模块不应该依赖低层模块,而是都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象
核心思想
:减少类与类之间的耦合性,将依赖抽象化,要面向接口编程,先顶层再细节来设计代码结构
样例:
在前面提到的TX课堂举例了java课程和golang课程,但是只有课程基本信息,还缺少课程的学习方法,现在给课程接口添加学习的方法:
// 课程类接口
public interface ITXCourse {
// 课程id
Integer getId();
// 课程名称
String getName();
// 课程价格
Double getPrice();
void study();
}
学习golnag
// java教程
public class JavaCourse implements ITXCourse{
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 void study(){
System.out.println("学习java");
}
}
我现在想学习java,也想学习golang
public class GuoHuPerson {
// 学员实体类,学习多种课程
public void study(ITXCourse txCourse){
// 通过课程的抽象,调用学习的方法,学习课程
txCourse.study();
}
}
测试:
public static void main(String[] args) {
GuoHuPerson guohuPerson = new GuoHuPerson();
guohuPerson.study(new JavaCourse(1,"java",new Double(1000)));
guohuPerson.study(new GoLangCourse(2,"golang",new Double(1000)));
}
out:
学习java
学习golang
通过传入ITXCourse 的抽象完成课程的学习,把具体依赖抽离成抽象依赖,这种传入方式也叫依赖注入
,在这个场景下,还可以选择Setter方式注入,代码修改如下:
public class GuoHuPerson {
private ITXCourse txCourse;
public void setCourse(ITXCourse course) {
this.txCourse = course;
}
// 学员实体类,学习多种课程
public void study(){
// 通过课程的抽象,调用学习的方法,学习课程
txCourse.study();
}
}
public static void main(String[] args) {
GuoHuPerson guohuPerson = new GuoHuPerson();
guohuPerson.setCourse(new JavaCourse(1,"java",new Double(1000)));
guohuPerson.study();
guohuPerson.setCourse(new GoLangCourse(1,"java",new Double(1000)));
guohuPerson.study();
}
out
学习java
学习golang
单一职责原则
定义
:不要存在多于一个导致类变更的原因
核心思想
:一个类/一个接口/一个方法 只做一件事情,不要过多冗余复杂逻辑
样例:
假设java的课程部分时免费的,部分时收费的,那么对于学习同学就要做权限拦截,可以修改person的学习方法:
// 学员实体类,学习多种课程
public void study(boolean isVip){
if(isVip){
// 简写,实际情况可能很复杂
// 会涉及到vip课程各种权限和加密
System.out.println("架构升级课");
}else{
System.out.println("基础免费课");
}
}
study方法现在非常多的逻辑,可以考虑拆分下
// 学习基础课程
public void studyBasic(){
System.out.println("基础免费课");
}
// 学习架构课程
public void studyFramework(){
// 简写,实际情况可能很复杂
// 会涉及到vip课程各种权限和加密
System.out.println("架构升级课");
}
public static void main(String[] args) {
GuoHuPerson guohuPerson = new GuoHuPerson();
guohuPerson.setCourse(new JavaCourse(1,"java",new Double(1000)));
guohuPerson.studyBasic();
guohuPerson.studyFramework();
}
这个demo可能不是特别恰当,主题思想时方法或者类或者接口做拆分,一个方法只完成一件事,不要过多的冗余太多逻辑,方法的拆分之后,可以用组合的方式完成更多的事情
接口隔离原则
定义
:多个专门的接口,而不使用单一的总接口
核心思想
:高内聚低耦合的设计思想
- 一个类对一类的依赖应该建立在最小的接口之上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少(注意分析实际情况)
样例:
曾经我们在很多地方见到了IAnimal抽象接口,这个接口包含吃,飞,叫,游泳等等操作
public interface IAnimal {
// 吃
void eat();
// 飞
void fly();
// 叫
void call();
// 游泳
void swim();
}
从实际角度看,狗肯定不会飞,鸟也未必会游泳,所以可以针对操作类型设计针对性的接口,IEatAnimal,IFlyAnimal,ICallAnimal,ISwimAnimal
// 具有吃行为的动物抽象
public interface IEatAnimal {
void eat();
}
// 具有叫行为的动物抽象
public interface ICallAnimal{
void call();
}
// 具有游泳行为的动物抽象
public interface ISwimAnimal{
void swim();
}
具体的dog实体类:
public class Dog implements IEatAnimal,ICallAnimal,ISwimAnimal{
@Override
public void call() {
System.out.println("叫");
}
@Override
public void eat() {
System.out.println("吃");
}
@Override
public void swim() {
System.out.println("游泳");
}
}
如果时鱼类,可以实现 游泳接口 和 吃接口
注意
:并不是一个接口只有一个方法,在设计接口的过程中,是按照实际的公共的属性进行抽象,具体要看所看实际的应用场景,多用组合
迪米特法则
定义
:指一个对象应该对其他对象保持最少的了解
核心思想
:尽量降低类与类之间的耦合
样例:
某天leader想知道tx课堂有多少学员报名了学习
public class Admin {
public void SumStudentCount(List<Person> personList){
System.out.println("有"+personList.size()+"个学员报名了");
}
}
public class Learder {
public void SumStudentCount(Admin admin){
List<Person> personList= new ArrayList<Person>();
for (int i= 0; i < 5 ;i ++){
personList.add(new Person());
}
admin.SumStudentCount(personList);
}
}
不过感觉这么写不太好,leader没必要关系具体的学员,只需要管理员告诉他下有多少学生count就好了,所以改造下
public class Learder {
public void SumStudentCount(Admin admin){
admin.SumStudentCount(); // admin告诉leader 有多少count
}
}
public class Admin {
public void SumStudentCount(){
List<Person> personList = new ArrayList<Person>();
for (int i= 0; i < 5 ;i ++){
personList.add(new Person());
}
System.out.println("有"+personList.size()+"个学员报名了");
}
}
让leader与学员解耦,需要的信息从admin中获取即可
里氏替换原则
定义
:子类对象能够替换父类对象,而程序逻辑不变
核心思想
:子类可以扩展父类的功能,但不能改变父类原有的功能
注意点:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等
优缺点分析
优点
- 约束继承泛滥,开闭原则的一种体现
- 加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性,降低需求变更时引入的风险
缺点
- 通过继承来实现,降低了子类的灵活性,同时继承也会将父类的实现细节暴露给子类
- 增加了类与类之间的耦合性,父类如果发生变更需要考虑到子类,不利于代码的维护
样例:
在开闭原则中的举例就违背了里氏替换原则,因为重写了getPrice方法,如果要符合开闭原则,则在子类中重新定义自己的打折促销方法
public class JavaCoursePromotion extends JavaCourse {
public JavaCoursePromotion(Integer id, String name, Double price) {
super(id, name, price);
}
// 原始价格
public Double getOriginPrice(){
return super.getPrice();
}
// 促销价格,打了八折
public Double getPromotionPrice(){
return super.getPrice() * 0.8;
}
}
合成复用原则
定义
:尽量使用对象组合/聚合少用继承,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
核心思想
:多用组合/聚合,少用继承
- 组合:组合(合成)是一种强的“拥有”关系
- 聚合:聚合表示一种弱的“拥有”关系
样例:
在依赖倒置的分析中其实已经用到了合成复用原则,当时使用的是GuoHuPerson 的Setter方式注入,让person内部调用了ITXCourse 的方法
public class GuoHuPerson {
private ITXCourse txCourse;
public void setCourse(ITXCourse course) {
this.txCourse = course;
}
// 学员实体类,学习多种课程
public void study(){
// 通过课程的抽象,调用学习的方法,学习课程
txCourse.study();
}
}
public static void main(String[] args) {
GuoHuPerson guohuPerson = new GuoHuPerson();
guohuPerson.setCourse(new JavaCourse(1,"java",new Double(1000)));
guohuPerson.study();
guohuPerson.setCourse(new GoLangCourse(1,"java",new Double(1000)));
guohuPerson.study();
}
out
学习java
学习golang
学员和课程并没有继承关系,但却使用了课程的功能
总结
设计原则是一种规范,但并不是所有代码都要符合设计原则,具体情况要具体分析,适合的场景用适合的原则