软件设计七大原则

开闭原则

  • 一个软件实体如:类、模块和函数应该对扩展开放,对修改关闭
  • 用抽象构建框架,用实现扩展细节
  • 提高软件系统的可复用性及可维护性
接口类
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());
    }
}

UML

有新的需要,只需要创建新的接口实现类即可,而选择权在最高层的方法,降低了实现类和具体接口实现类的耦合度

单一职责原则
  • 不要存在多于一个导致类变更的原因
  • 一个类/接口/方法只负责一项职责
  • 优点 降低类的复杂度、提高类的可读性、提高系统的可维护性,降低变更引起的风险
    示例

    
    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() {  }
}

UML

接下来,版本进行演进
创建吃的接口

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。

UML

因为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不变

UML

里氏替换原则

定义

如果对每一个类型为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();
    }
}

结果

使用数据库连接增加产品

UML

如果业务修改,需要增加别的数据库连接,我们就需要做以下更改

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数据库连接增加产品

UML

可以看出减少了ProductDao与DBConnection的继承关系,增加了1对1的组合关系,PostgreSQLConnection和MySQLConnection继承了DBConnection,而且抽象了DBConnection,在选择连接类的时候可以输入任意子类,并且程序执行逻辑没有改变,也达到了结果预期。符合里氏替换原则。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注