软件设计的七大设计原则

一、前言

      七大设计原则是23种设计模式的基础,体现了软件设计的思想,但并不是所有设计模式都遵循这七大设计原则,有些设计模式只遵循一部分设计原则,是对一些实际情况做的一些取舍。在我们项目中也并不一定完全遵循所有设计模式,因为受一些因素如时间、人力、成本等,如果一开始将扩展性做的很完美,那么成本就上来了。所以遵循设计模式不要过度,一定要适度。
      本文将讲解每一种设计原则,从定义开始进行分析,理解,然后使用代码示例和UML图,分析存在的问题,逐步演进,力求将设计原则讲透,使读者理解。后续的设计模式文章也将采用这种方式。

二、开闭原则

      开闭原则的定义是:一个软件实体(如类、模块、函数)应该对扩展开放、对修改关闭。生活中也有很多开闭原则的体现,比如公司的8小时弹性上班制度,对8小时的上班时间是的修改是关闭的,但是什么时候来什么时候走是开放的。
      开闭原则的含义其实是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现。在项目中需求变更是非常常见的,如果我们频繁修改原有的代码会增加系统的复杂度,增加项目的风险。使用开闭原则可以提高系统的可复用性及可维护性,具体做法就是用抽象构建框架,用实现扩展细节。
      下面用一个例子来讲解开闭原则,有一个在线书店卖书的场景,每本书有价格和名称,我们新建一个书的接口IBook,和该接口的一个实现类JavaBook。

public interface IBook {
    String getName();
    double getPrice();
}
public class JavaBook implements IBook {
    @Override
    public String getName() {
        return "Java入门到精通";
    }
    @Override
    public double getPrice() {
        return 68.99;
    }
}

      此时的UML图是这样的(图有点错误,IDEA生成UML时get开头的方法它会认为是属性)
在这里插入图片描述
      现在有一个需求,就是有些书需要打折销售,我们需要获取打折后的价格,一种思路是修改IBook,新增一个打折价格的方法,但是这种改动影响很大,实现了该接口的类都要实现这个方法;另一种思路就是将JavaBook中的getPrice() 方法修改成打折后的价格,但是这样就获取不到原价了;还一种思路是在JavaBook中新增一个获取打折后价格的方法,这样既能获取原价又能获取打折后的价格,但是这三种种做法都违背了开闭原则,也就是对JavaBook进行了修改来实现变化,正确的做法是新增一个JavaBook的打折类,继承自JavaBook,新增一个获取打折后价格的方法。

public class JavaDiscountBook extends JavaBook {
    double getDiscountPrice(){
        return super.getPrice() * 0.8;
    }
}

      此时的UML图是:
在这里插入图片描述
      这样在不修改原来代码的基础上,实现了需求变更,其实实现开闭原则的核心是面向抽象编程,后面一些设计原则也是如此。

三、依赖倒置原则

      依赖倒置原则定义:高层模块不应该依赖底层模块,二者都应该依赖其抽象。也就是说针对接口编程,不要针对实现编程,针对接口编程包括使用接口或抽象类,这样可以使得各个模块彼此独立,降低模块间的耦合性。而且在实现类中尽量不发生直接的依赖关系,依赖关系通过接口或抽象类产生。
      有一个场景,司机可以开车,我们新建一个Driver类和一个Benz类,司机可以开奔驰车,代码如下:

public class Driver {
    public void driver(Benz benz){
        benz.run();
    }
}
public class Benz {
    public void run(){
        System.out.println("奔驰车可以跑!");
    }
}

此时的UML图是这样的
在这里插入图片描述
      Driver类就依赖Benz类,显然违反了依赖倒置原则,如果我们司机想开其他车,就必须修改Driver类,我们进一步修改,增加IDriver的接口和ICar接口,IDriver的driver类参数是ICar,这样使得依赖关系发生在这两个接口上,不同的司机实现IDriver接口,不同的车实现ICar接口就可以了,代码如下:

public interface IDriver {
    void driver(ICar car);
}
public interface ICar {
    void run();
}
public class Benz implements ICar{
    @Override
    public void run(){
        System.out.println("奔驰车可以跑!");
    }
}
public class Driver implements IDriver{
    @Override
    public void driver(ICar car){
        car.run();
    }
}

      此时的UML图是这样的
在这里插入图片描述
      这样不论什么类型的车都可以传入Driver的driver()方法里面,进行调用。

四、 单一职责原则

      单一职责原则的定义:不要存在多于一个导致类变更的原因。如果我们一个类有两个职责:职责1和职责2,当我们需求变更的时候,职责1需要改变,变更的时候很可能会导致原本正常的职责2出问题。所以一个类、接口方法只负责一项职责,这样能降低类的复杂度,提高类的可读性,提高可维护性,降低修改带来的风险。在实际项目中,很多类不遵循单一职责原则,但是接口和方法要做到单一职责。单一职责原则还有一个很重要的点就是职责的划分,有些需求正常情况下有多个职责,但是某些特殊情况下又是一个职责,职责划分也需要视实际情况而定。

五、接口隔离原则

      接口隔离原则定义:用多个专门的接口而不使用单一的总接口,客户端不应该依赖它不需要的接口。也就是说一个类对另一个类的依赖应该建立在最小的接口上,尽量细化接口,减少接口中的方法,但是一定要注意适度的原则,过分细化接口会带来复杂度。
      比如我们有个接口IAnimalAction,描述动物的行为,代码如下:

public interface IAnimalAction {
    void eat();
    void fly();
    void swim();
}

      这个接口中,我们定义了三个行为,后面需要的动物类实现这个接口,但是这就存在一个问题,如果某些动物不具备接口里面的三个行为中的某一个,但是它必须要实现那个方法,这就违背了接口隔离原则,正确的做法是将接口中的三个方法隔离开,分成三个接口,这样有具体行为的动物实现具体的接口,减少了耦合,将代码演进:

public interface IEatAnimal {
    void eat();
}
public interface IFlyAnimal {
    void fly();
}
public interface ISwimAnimal {
    void swim();
}

      接口隔离原则强调的是接口依赖隔离,单一职责原则强调的是职责单一。单一职责是对实现的约束,接口隔离原则是对抽象的约束。

六、迪米特原则

      迪米特原则又叫最少知道原则,定义是:一个对象应该对其他对象保持最少的了解。简单讲就是只和朋友交流,不和陌生人说话,朋友指的是出现在成员变量、方法输入、方法输出中的类,但是出现在方法内部的类不属于朋友。不应该和这样的类发生关系。使用迪米特原则可以降低类与类之间的耦合,提高类的复用率,但是还是要强调适度的原则,过分使用迪米特原则会产生大量的中介类,使系统变复杂。
      现在有一个场景,学校里面有多个班级,班级里面有多个学生,我们现在要打印所有班级的所有学生,一种实现方式如下(这里将成员变量设置成public省略了get、set方法):

public class School {
    public int id;
    public String schoolName;
    public List<Class> classes;
    public void print(){
        for(Class c : classes){
            for(Student s : c.students){
                System.out.println(s.studentName);
            }
        }
    }
}
class Class{
    public int id;
    public String className;
    public List<Student> students;
}
class Student{
    public int id;
    public String studentName;
}

      在School类中,Class类是它的属性,也就是它的朋友,但是Student类既不是成员变量,方法入参,也不是方法返回值,它不是School类的朋友,不应爱出现在方法内部,应该让Class类打印本班级的学生,代码演进如下:

public class School {
    public int id;
    public String schoolName;
    public List<Class> classes;
    public void print(){
        for(Class c : classes){
            c.print();
        }
    }
}
class Class{
    public int id;
    public String className;
    public List<Student> students;
    public void print(){
        for(Student s : students){
            System.out.println(s.studentName);
        }
    }
}
class Student{
    public int id;
    public String studentName;
}

      这样School类和Student就没有耦合了,逻辑也很清晰,符合迪米特原则。

七、里氏替换原则

      里氏替换原则的定义是:对于每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有对象O1替换为O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。也就是子类替换父类,程序逻辑不变。里氏替换原则约束了继承,继承在程序设计中能够复用代码但是对程序是有入侵的,因为子类默认就拥有父类的行为,而且增加了耦合。
      在继承中如何遵守里氏替换原则?首先子类可以扩展父类的功能,但不能改变原有的功能,子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,如果重写了,那么用子类替换父类时,程序会调用子类的方法(否则重写就没有意义了),也就导致程序的行为发生变化。还有就是子类重载(不是重写)父类方法时,方法的前置条件(方法入参)要比父类更宽松比如父类的参数是HashMap,子类的话参数可以是Map,更宽松的话替换或不替换程序都会调用父类的方法,程序的行为也就不会改变,符合里氏替换原则。同理,方法的后置(方法返回值)条件要比父类更严格。使用里氏替换原则,可以避免子类重写父类的方法,降低代码出错的可能性。

八、合成复用原则

      合成复用原则的定义是:尽量使用对象的组合/聚合,而不是继承关系达到软件复用的目的。
      组合是contains-A的关系,比如一个人的手、脚就是组合关系,这是一种强关系,其中一部分不存在了,所有的都不存在了,聚合是has-A的关系,是一种弱的关系,人群中的人就是聚合关系,其中一个人离开了,人群还是存在的,通过组合聚合也可以达到复用的目的,但是这种复用是黑箱复用,不需要知道细节,继承的复用是白箱复用,父类的细节会暴露给子类。尽量使用组合/聚合来实现软件复用并不是说抛弃继承,如果两个实体是is-A的关系时,可以使用继承。
      现在有个场景就是数据访问层要操作数据,需要先获得数据库连接,我们新建两个类:

public class DBConnection {
    public String getConnection() {
        return "获得数据库连接";
    }
}
public class ProductDao extends DBConnection {
    private void addProduct(){
        getConnection();
        System.out.println("操作数据库!");
    }
}

      通过继承我们实现了获取数据库连接,但是如果我们要更换一种数据库,那么就要修改DBConnection类,违背了开闭原则,现在我们可以通过抽象加组合的方式来实现变更后的需求,代码演进如下:

public abstract class DBConnection {
    public abstract String getConnection();
}
public class MySQLConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "MySQL的数据库连接";
    }
}
public class OracleConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "Oracle数据库连接";
    }
}
public class ProductDao{

    private DBConnection dbConnection;

    ProductDao(DBConnection dbConnection){
        this.dbConnection = dbConnection;
    }
    public void addProduct(){
        String con = dbConnection.getConnection();
        System.out.println("使用"+con + "增加一个产品");
    }
}

      这样,当我们新增一种数据库连接的时候,只要继承DBConnection这个抽象类就可以了,按我们实际传入的类型,ProductDao会调用对应的数据库连接来操作数据库,此时的UML是这样的。
在这里插入图片描述

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AE86Jag

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值