软件设计七大原则
最后更新于:2022-08-13 13:06:33
开闭原则
- 一个软件实体如:类、模块和函数应该对扩展开放,对修改关闭
- 用抽象构建框架,用实现扩展细节
- 提高软件系统的可复用性及可维护性
接口类
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) {
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 DiscountCourse extends JavaCourse {
public DiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
@Override
public Double getPrice() {
return super.getPrice() * 0.8;
}
public Double getOriginPrice() {
return super.getPrice();
}
}
UML
依赖倒置原则
- 高层模块不应该依赖底层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 针对接口编程,不要针对实现编程
public class Geely {
public void studyJavaCourse() {
System.out.println("学习Java课程");
}
public void studyFECourse() {
System.out.println("学习FE课程");
}
// 如果想要再学习别的,就需要添加新的方法
public static void main(String[] args) {
Geely geely = new Geely();
geely.studyJavaCourse();
geely.studyFECourse();
}
}
每当需要新的课程出现,都需要实现新的方法。然后在高层模块才能使用
引入抽象
创建学习接口
public interface ICourse {
void studyCourse();
}
实现接口
public class JavaCourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("学习Java课程");
}
}
public class FECourse implements ICourse {
@Override
public void studyCourse() {
System.out.println("学习FE课程");
}
}
public class Geely {
// 将选择权交给高层接口,高层选择哪个,就使用哪个
public void studyCourse(ICourse iCourse) {
geely.studyCourse(new JavaCourse());
geely.studyCourse(new FECourse());
}
}
有新的需要,只需要创建新的接口实现类即可,而选择权在最高层的方法,降低了实现类和具体接口实现类的耦合度
单一职责原则
- 不要存在多于一个导致类变更的原因
- 一个类/接口/方法只负责一项职责
- 优点 降低类的复杂度、提高类的可读性、提高系统的可维护性,降低变更引起的风险
示例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("鸵鸟");
}
}
结果
``` XML
大雁用翅膀飞
鸵鸟用翅膀飞
因为鸵鸟不用翅膀飞,应该用脚走,所以我们应该在代码中做判断:
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 WalkBrid {
public void mainMoveMode(String birdName) {
System.out.println(birdName + "用脚走");
}
}
public class Test {
// 具体的使用,在应用层进行判断,就是类的单一性的体现
public static void main(String[] args) {
FlyBird flyBird = new FlyBird();
flyBird.mainMoveMode("大雁");
WalkBrid walkBrid = new WalkBrid();
walkBrid.mainMoveMode("鸵鸟");
}
}
接口隔离原则
- 用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
- 一个类对一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
- 注意适度原则,一定要适度
优点:符合常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性,可扩展性和可维护性
示例
// 建立一个动物接口
// 包含吃、飞行、游泳
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() { }
}
接下来,版本进行演进
创建吃的接口
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() {
}
}
UML
平级接口增加,实现接口隔离,因为细粒度可以组装,但是粗粒度是不能拆分的
接口隔离原则和单一职责原则看起来区别不大,但是单一职责原则指的是类、接口、方法的职责是单一的,强调的是职责,只要职责是单一的,有多少个无所谓,比如接口eat,有多少中吃法都没有问题。注重的是方法中的实现和细节。接口隔离原则,主要是接口的隔离主要约束的是针对抽象,针对程序整体框架的构建。同时接口隔离要适度,以免接口过多,在实际使用中结合项目规模做出平衡。
迪米特原则
- 一个对象应该对其他对象保持最少的了解。又叫最少知道原则
- 尽量降低类与类之间的耦合
- 强调只和朋友交流,不和陌生人说话
朋友:出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类
代码
场景:boss问teamlader当前有多少课程
public class Boss {
// 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 TeamLeader {
// 输出课程的数量
public void checkNumberOfCourses(List courseList) {
System.out.println("课程数量为" + courseList.size());
}
}
public class Course {
}
测试
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
boss.commandCheckNumber(teamLeader);
}
}
结果
课程数量为20
迪米特原则讲究只跟朋友交流,Boss代码中,teamleader作为入参,属于朋友。而方法体内部的类,不算朋友。例如course。
因为course与boss无关,所以,不应该有创建关系。更改代码
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());
}
}
public void commandCheckNumber(TeamLeader teamLeader) {
teamLeader.checkNumberOfCourses();
}
//course不变
里氏替换原则
定义
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的字类型
扩展
一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明的使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变
引申
子类可以扩展父类的功能,但不能改变父类原有的功能
含义
1:子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
2:子类中可以增加自己特有的方法
3:当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
4:当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的更严格或相等。
优点
约束继承泛滥,开闭原则的一种体现
加强程序的健壮性,同时变更时也可以做到非常好的兼容性提高程序的维护性、扩展性。降低需求变更时引入的风险
代码
创建一个矩形
public class Rectangles {
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 Rectangles {
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) {
setSideLength(width);
}
}
创建测试类
public class Test {
public static void resize(Rectangles rectangles) {
while (rectangles.getWidth() <= rectangles.getLength()) {
rectangles.setWidth(rectangles.getWidth() + 1);
System.out.println(rectangles);
}
System.out.println("resize方法结束" + rectangles);
}
public static void main(String[] args) {
Rectangles rectangles = new Rectangles();
rectangles.setLength(20);
rectangles.setWidth(10);
resize(rectangles);
}
}
结果
Rectangles{length=20, width=11}
Rectangles{length=20, width=12}
Rectangles{length=20, width=13}
Rectangles{length=20, width=14}
Rectangles{length=20, width=15}
Rectangles{length=20, width=16}
Rectangles{length=20, width=17}
Rectangles{length=20, width=18}
Rectangles{length=20, width=19}
Rectangles{length=20, width=20}
resize方法结束Rectangles{length=20, width=20}
修改测试类
public class Test {
public static void resize(Rectangles rectangles) {
while (rectangles.getWidth() <= rectangles.getLength()) {
rectangles.setWidth(rectangles.getWidth() + 1);
System.out.println(rectangles);
}
System.out.println("resize方法结束" + rectangles);
}
public static void main(String[] args) {
Square square = new Square();
square.setLength(20);
resize(square);
}
}
结果
Square{sideLength=607844}
Square{sideLength=607845}
Square{sideLength=607846}
Square{sideLength=607847}
Square{sideLength=607848}
Square{sideLength=607849}
Square{sideLength=607850}
Square{sideLength=607851}
Square{sideLength=607852}
Square{sideLength=607853}
Square{sideLength=607854}
Square{sideLength=607855}
Square{sideLength=607856}
......
程序一直执行,直到内存溢出
所以在这个使用resize的业务场景下,正方形是不可以当作矩形的子类,这样就违反了里氏替换原则,根据要求做出更改
创建一个更上层的类,四边形。因为矩形和正方形都是四边形
public interface Quadrangle {
long getWidth();
long getLength();
}
修改Rectangles类
public class Rectangles implements Quadrangle {
private long length;
private long width;
@Override
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public void setWidth(long width) {
this.width = width;
}
@Override
public long getWidth() {
return width;
}
}
修改Square类
public class Square implements Quadrangle {
private long sideLength;
public long getSideLength() {
return sideLength;
}
public void setSideLength(long sideLength) {
this.sideLength = sideLength;
}
@Override
public long getLength() {
return sideLength;
}
@Override
public long getWidth() {
return sideLength;
}
}
进入Test中,发现代码报错
因为有get方法,没有set方法,所以如果传Rectangles进来是可以的,但是传Square进来是不行的,约束了接口的实现,禁止了继承泛滥。在这个业务场景中,里氏替换原则也没有被破坏。
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
public class Father {
public void method(HashMap map){
System.out.println("执行了父类");
}
}
// 子类的输入比父类更宽泛
public class Son extends Father {
public void method(Map map) {
System.out.println("执行了子类");
}
}
public class Test {
public static void main(String[] args) {
HashMap o = new HashMap();
Father father = new Father();
father.method(o);
Son son = new Son();
//父类存在的地方,可以用子类替代
//子类替代父类
son.method(o);
}
}
结果
执行了父类
执行了父类
子类可以直接替换父类,不影响程序执行逻辑和运行结果
public class Father {
public void method(Map map){
System.out.println("执行了父类");
}
}
// 子类的输入比父类更宽泛
public class Son extends Father {
public void method(HashMap map) {
System.out.println("执行了子类");
}
}
public class Test {
public static void main(String[] args) {
HashMap o = new HashMap();
Father father = new Father();
father.method(o);
Son son = new Son();
//父类存在的地方,可以用子类替代
//子类替代父类
son.method(o);
}
}
结果
执行了父类
执行了子类
子类可以直接替换父类,影响程序执行逻辑和运行结果,不符合里氏替换原则
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
public abstract class Base {
public abstract Map method();
}
public class Child extends Base {
@Override
public HashMap method() {
HashMap map = new HashMap();
map.put("message", "执行了child");
return map;
}
}
public class Test {
public static void main(String[] args) {
Child child = new Child();
System.out.println(child.method());
}
}
结果
{message=执行了child}
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更宽泛时
public abstract class Base {
public abstract HashMap method();
}
public class Child extends Base {
@Override
public Map method() {
HashMap map = new HashMap();
map.put("message", "执行了child");
return map;
}
}
合成(组合)/聚合复用原则
尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的
聚合 has-a 组合 contains-a
优点
可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
缺点
会有较多的对象需要管理
代码
public class DBConnection {
public String getConnection(){
return "数据库连接";
}
}
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();
}
}
结果
使用数据库连接增加产品
如果业务修改,需要增加别的数据库连接,我们就需要做以下更改
public class DBConnection {
public String getConnection(){
return "数据库连接";
}
public String getPostgreSQLConnection(){
return "数据库连接";
}
}
这样就违反了开闭原则,所以应该把基础类做成抽象类
public abstract class DBConnection {
public abstract String getConnection();
}
public class MySQLConnection extends DBConnection{
@Override
public String getConnection() {
return "MySQL数据库连接";
}
}
public class PostgreSQLConnection extends DBConnection{
@Override
public String getConnection() {
return "PostgreSQL数据库连接";
}
}
public class ProductDao {
private DBConnection connection;
public ProductDao(DBConnection connection) {
this.connection = connection;
}
public void addProduct() {
String conn = connection.getConnection();
System.out.println("使用" + conn + "增加产品");
}
}
public class Test {
public static void main(String[] args) {
ProductDao productDao = new ProductDao(new MySQLConnection());
productDao.addProduct();
productDao = new ProductDao(new PostgreSQLConnection());
productDao.addProduct();
}
}
结果
使用MySQL数据库连接增加产品
使用PostgreSQL数据库连接增加产品
可以看出减少了ProductDao与DBConnection的继承关系,增加了1对1的组合关系,PostgreSQLConnection和MySQLConnection继承了DBConnection,而且抽象了DBConnection,在选择连接类的时候可以输入任意子类,并且程序执行逻辑没有改变,也达到了结果预期。符合里氏替换原则。