欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

软件设计的七大原则

程序员文章站 2024-02-10 08:15:10
...

软件设计原则

一、开闭原则

对扩展开放,对修改关闭。在程序需要进行扩展的时候,不能去修改原有的代码,想要达到这样的效果,需要使用接口和抽象类。

软件中易变的细节可以从抽象派生的实现类中进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类即可。

举例:

【例】搜狗输入法的皮肤设计

分析:皮肤是搜狗输入法(SouGouInput)的属性,用户可以根据自己的喜爱更换输入法的皮肤。这些皮肤有共同的特点,比如都由图片、输入窗口等组成。可以为其定义一个抽象类的皮肤(AbstractSkin),而每个具体的皮肤(DefaultSpecificSkin 和 HeimaSpecificSkin)是其子类。如下图所示:

软件设计的七大原则

如果有新的皮肤,只需要定义新的实现类即可,无需修改抽象皮肤类。

二、里氏代换原则

里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法

如果通过重写父类的方法来完成新的功能,会导致整个继承体系的可复用性降低

举例:

【例】正方形属于长方形

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形,如下图所示:

RectangleDemo 类是软件系统中的一个组件,它有一个 resize 方法依赖基类Rectangle,resize方法是RectandleDemo类中的一个方法,用来实现宽度逐渐增长直到大于长度的效果。

软件设计的七大原则

代码如下:

正方形(Square):

由于正方形的长和宽相同,所以在方法 setLengthsetWidth 中,对长度和宽度都需要赋相同值:

public class Square extends Rectangle {

    @Override
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }

    @Override
    public void setWidth(double width) {
        super.setLength(width);
        super.setWidth(width);
    }
}

RectangleDemo 类:

resize() 方法如下:

public static void resize(Rectangle rectangle) {
    while (rectangle.getWidth() <= rectangle.getLength()) {
        rectangle.setWidth(rectangle.getWidth() + 1);
    }
}

运行一下这段代码就会发现,假如把一个长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合预期。

假如把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长(正方形重写的 setWidth 方法将会使长宽一致),代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。

进行改进:

创建一个接口 Quadrilateral ,定义四边形通用的获取长宽的抽象方法,长方形和正方形分别实现这个接口,定义各自具体的设置长宽的方法,如下图:

软件设计的七大原则

此时,resize方法只可以计算长方形,无法计算正方形,解决了之前的问题。

三、依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

依赖:A类依赖B类,也就是在A类中声明了B类型的成员变量。

举例:

【例】组装电脑

现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等,类图如下:

Computer 类中直接依赖了具体的硬件:

软件设计的七大原则

这种方式最大的缺陷是,电脑的 cpu 只能是 Intel 的,内存条只能是金士顿的,硬盘只能是希捷的。

根据依赖倒转原则进行改进:

让Computer类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类,类图如下:

软件设计的七大原则

后期如果有不同品牌的产品,只需要实现对应的接口即可,无需修改Computer类。

四、接口隔离原则

  • 一个类不应该*依赖于它不使用的方法:
软件设计的七大原则
  • 一个类对另一个类的依赖应该建立在最小的接口上:
软件设计的七大原则

举例:

【例】安全门案例

有一个 HeiMa 品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:

软件设计的七大原则

现在如果还需要创建一个其他品牌的安全门,该安全门只具有防盗、防水功能,很显然如果实现 SafetyDoor 接口就违背了接口隔离原则,需要改进,将不同的功能提取到不同的接口,类图如下:

软件设计的七大原则

这样,不同的安全门,需要什么样的功能实现对应的接口即可。

五、迪米特法则

迪米特法则又叫最少知识原则

如果两个软件实体之间无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性:

软件设计的七大原则

可以理解为,只和朋友交谈,不和陌生人交谈。

迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

举例:

【例】明星与经纪人的关系实例

明星的许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是明星的陌生人,所以适合使用迪米特法则,类图如下:

软件设计的七大原则

六、合成复用原则

尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

继承的缺点:

  1. 继承破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的。
  2. 子类与父类的耦合度高。父类的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。

组合或聚合的优点:

  1. 对象间的耦合度低。可以在类的成员位置声明。

举例:

【例】汽车分类管理程序

汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、红色汽车等。如果同时考虑这两种分类,其组合就很多。类图如下(继承复用)

软件设计的七大原则

从上面的类图可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。可以试着将继承复用改为聚合复用,如下图:

软件设计的七大原则

二者的对比,假设要新添加一种光能汽车类:

  • 对于继承复用(添加了三个类)

    软件设计的七大原则
  • 对于聚合复用(仅添加了一个类)

    软件设计的七大原则

七、单一职责原则

不要存在多于一个导致类变更的原因。

比如,Class 类有两个职责 a 和 b,a变更会导致类的变更,进而可能导致对职责b功能的影响;同理,b变更会导致类的变更,进而可能导致对职责a功能的影响。也就是说职责 a 和 b 都可以对类进行变更,不满足单一职责原则。

解决方案:一个类或接口只负责一项职责。

好处:降低类的复杂度,提高类的可读性、可维护性,降低变更带来的风险。

举例:

用一个类描述动物呼吸这个场景:

class Animal{
    public void breathe(String animal){
        System.out.println(animal+"呼吸空气");
    }
}
public class Client{
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("牛");
    }
}

运行结果:牛呼吸空气。

此时出现一个问题,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将 Animal 类细分为陆生动物类 Terrestrial ,水生动物 Aquatic ,代码如下:

class Terrestrial{
    public void breathe(String animal){
        System.out.println(animal+"呼吸空气");
    }
}
class Aquatic{
    public void breathe(String animal){
        System.out.println(animal+"呼吸水");
    }
}

public class Client{
    public static void main(String[] args){
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");

        Aquatic aquatic = new Aquatic();
        aquatic.breathe("鱼");
    }
}

运行结果:牛呼吸空气,鱼呼吸水。

如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类 Animal 来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:

class Animal{
    public void breathe(String animal){
        if("鱼".equals(animal)){
            System.out.println(animal+"呼吸水");
        }else{
            System.out.println(animal+"呼吸空气");
        }
    }
}

但是这种方式有一个缺点,动物类型越多,if 判断越多。

所以,是否使用单一职责原则,需要根据具体的情况来决定,比如上述,逻辑比较简单,可以违背单一职责原则。