开闭原则
定义
开闭原则(Open-Closed Principle OCP),指一个软件实体如类、模块和函数应该对外扩展开放,对修改关闭。
强调用抽象构建看框架,用实现扩展细节,可以提高软件系统的可复用性及可维护性。
例子
课程生态有Java,Python,Web课程。现在要给Java课程做活动,给予8折的优惠处理。此时如果修改Java课程的价格,则会存在一定的风险,可能影响其他地方的调用结果。那么如果可以在不修改原有代码的的前提下,实现价格优惠呢?
类
public interface Course {
Integer getId();
String getName();
Double getPrice();
}
public class JavaCourse implements Course{
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 JavaDiscountCourse extends JavaCourse{
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getOriginPrice(){
return super.getPrice() * 0.8;
}
}
依赖倒置原则
定义
依赖倒置原则(Dependence Inversion Principle,DIP),指设计代码结构时,高层模块不应该依赖底层模块,二者应该依赖起抽象。
通过依赖倒置可以降低类与类之间的耦合度,提高系统的稳定性,提高代码的可读性和可维护性,并降低修改程序带来的风险。
例子
存在一类学生热爱学习,目前正在学习Java课程和Python课程。当这类学生还想学习Web课程时,则需要进行业务员扩展。代码便要从底层到高层(调用层)一次修改代码。这样则是不稳定的,容易伴随意想不到的风险。
public class Student {
public void studyJavaCourse(){
System.out.println("study Java...");
}
public void studyPythonCourse(){
System.out.println("study Python...");
}
}
此时,可以通过高度依赖抽象进行修改。可以创建课程接口,然后实现各个课程的实现类,再对学生类进行改造。
public interface Course {
void study();
}
public class JavaCourse implements Course{
@Override
public void study() {
System.out.println("study Java...");
}
}
public class PythonCourse implements Course{
@Override
public void study() {
System.out.println("study Python...");
}
}
public class Student {
public void study(Course course){
course.study();
}
}
此时,不管学生类学习什么课程,都只需要新建一个类,便可以知道该学生正在学习什么。这种方式叫依赖注入。还有构造器注入方式和Setter注入方式。
构造器注入方式
public class Student {
private Course course;
public Student(Course course){
this.course = course;
}
public void study(){
course.study();
}
}
根据构造器注入方式,调用时,每次都要创建实例。而当学生类是单例时,则只能选择Setter注入方式。
public class Student {
private Course course;
public void setCourse(Course course){
this.course = course;
}
public void study(){
course.study();
}
}
以抽象为基准 比 以细节为基准 搭建起来的架构要稳定得多,因此在拿到需求时,要面向接口编程,按照先顶层再谢姐的顺序设计代码结构。
单一职责原则
定义
单一职责原则(Simple Responsibilty Principle SRP),指一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
当一个Class负责两个职责,如若有一个职责发生需求变更,修改了逻辑代码,则可能会导致另一个职责的功能发生故障。
根据单一职责,需要分别用两个Class来实现来给那个职责,进行解藕。后期需求变更时互不影响。
即一个Class、Interface、Method只负责一项职责
例子
课程含有直播课和录播课,其中直播课不可以快进和快退,录播课可以任意地观看。假如选择需要对课程进行加密,而直播课和录播课的加密逻辑时不同的,此时必须要修改代码,势必会互相影响,带来不可控的风险。
public class Course {
public void study(String courseName){
if ("Live".equals(courseName)){
System.out.println("不能快退");
}else{
System.out.println("可以任意播放");
}
}
}
此时,需要对职责进行分离解藕,分别创建两给类LiveCourse和ReplayCourse。
public class LiveCourse {
public void study(String courseName){
System.out.println(courseName + "不能快进");
}
}
public class ReplayCourse {
public void study(String courseName){
System.out.println(courseName + "可以任意播放");
}
}
当业务继续发展时,要对课程做权限。没有付费的学院可以获得课程的基本信息,已经付费的学员可以获取视频流,即学习权限。“那么对于控制课程层面,至少有两个职责。我们可以把展示职责和管理职责分离开,都实现同一个抽象依赖。设计一个顶层接口,创建Course接口
public interface Course {
//获取课程的基本信息
String getCourse();
//获取视频流
byte[] studyCourse();
//退款
void refundCourse();
}
可以再将这个接口拆分成两个接口
public interface CourseInfo {
String getCourseName();
byte[] getCourseVideo();
}
public interface CourseManager {
void studyCourse();
void refundCourse();
}
接口隔离原则
定义
接口隔离原则(Interface Segregation Principle,ISP)指用多个专门的接口,而不是用单一的总接口,并且客户端不应该依赖它不需要的接口。
这个原则需要注意几点:
1.一个类对另一个类的依赖应该建立在最小接口上
2.建立单一接口,不要建立庞大臃肿的接口
3.尽量细化接口,接口中的方法尽量少
例子
一个动物接口,存在吃、飞、游等行为
public interface Animal {
void eat();
void fly();
void swim();
}
public class Bird implements Animal{
@Override
public void eat() {}
@Override
public void fly() {}
@Override
public void swim() {}
}
public class Dog implements Animal{
@Override
public void eat() {}
@Override
public void fly() {}
@Override
public void swim() {}
}
此时,Bird的swim()无法实现,而Dog的fly()也无法实现。故采取接口隔离原则,来设计不同的接口诠释不同动物的行为。
public interface AnimalEat {
void eat();
}
public interface AnimalFly {
void fly();
}
public interface AnimalSwim {
void swim();
}
此时Dog和Brild的类便更加完善
public class Dog implements AnimalSwim,AnimalEat{
@Override
public void eat() {}
@Override
public void swim() {}
}
public class Bird implements AnimalFly,AnimalEat{
@Override
public void eat() {}
@Override
public void fly() {}
}
迪米特法则
定义
迪米特法则(Law of Demeter LoD),又叫最少知道原则,指一个对象应该对其他对象保持最少的了解,尽量降低类之间的耦合。
强调只和朋友交流(成员变量、方法的输入,输出参数),不和陌生人(方法体内部)说话。
例子
设计一个权限系统,TeamLeader需要查看目前发布到线上的课程数量。此时TeamLeader要让Employee去统计。
Course.class
public class Course {
}
Emploee.class
public class Employee {
public void checkNumberOfCourses(List<Course> coureseList){
System.out.println("目前已发布的课程数量为"+coureseList.size());
}
}
TeamLeader.class
public class TeamLeader {
public void commandCheckNumber(Employee employee){
List<Course> courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
employee.checkNumberOfCourses(courseList);
}
}
此时Employee统计需要引用Course对象,TeamLeader和Course并不是朋友。
改造Employee
public class Employee {
public void checkNumberOfCourses(){
List<Course> courseList = new ArrayList<>();
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();
}
}
里氏替换原则
定义
里氏替换原则(Liskov Substitution Principle LSP)指如果对每一个类型为T1的对象O1,都有T2的对象O2,使得T1定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么T2是类型T1的字类型。
即一个软件实体如果适用于一个父类,则一定适用于子类。“所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。也可以理解为,子类可以扩展父类的功能,但不能改变父类原有的功能。
1.子类可以实现父类的抽象方法,但不能覆盖非抽象方法
2.子类可以增加自己特有的方法
3.当子类的方法重载父类的方法时,方法对的前置条件(即方法的输入参数)要比父类的方法更宽松。
4.当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的方法更严格或相等。
优点:
约束继承泛滥,是开闭原则的一种体现
加强程序的健壮性,同时变更可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。
合成复用原则
定义
合成复用(Composite/Aggregate Reuse Principle,CARP)指尽量使用对象组合(has-a)或者对象聚合(contains-a)的方式实现代码复用,而不是用继承关系达成代码复用的目的。