《盘点软件设计中的七大原则》

说在前头:本人为大二在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,能力有限,文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正。若在阅读时有任何的问题,也可通过评论提出,本人将根据自身能力对问题进行一定的解答。

前言

在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据 7 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

一、开闭原则

定义:当项目的需求需要做出更改或者增加的时候,在不修改源代码的前提下,可以扩展模块的功能,使其功能得到实现。即:一个软件实体,对扩展开放,对修改关闭。

作用:

  • 方便软件的测试 :因为开闭原则是在不修改原来的代码基础上进行功能的修改,因此,当我们测试新修改的功能是否可用时,我们只需测试修改部分的代码即可。
  • 提高代码的复用性:开闭原则在一定的程度上,引导着代码的编写往颗粒度小的方向发展,根据原子和抽象编程可以提高代码的复用性。
  • 提高代码的可维护性:遵守了开闭原则的代码,其稳定性和延续性增强,易于日后的维护。

示例:我们先来创建一个具体的对象类Tom,Tom是一个人类,他拥有人类的各种基本技能(吃饭,走路,跑步,跳跃),如下:

package com.bosen.www;
​
/**
 * <p>Tom实体类</p>
 * @author Bosen 2021/5/16 21:49
 */
public class Tom {
    public void eat() {
        System.out.println("吃饭");
    }
    public void walk() {
        System.out.println("走路");
    }
    public void run() {
        System.out.println("跑步");
    }   
    public void jump() {
        System.out.println("跳跃");
    }
}
​

我们来假设:Tom的家庭经济状况良好,Tom的父母给他报了一个钢琴培训班,因此Tom获得了弹奏钢琴的技能。那我们需要将新的技能点给Tom加上,如果使用最传统的实现方式,我们将在Tom类中新添加playThePiano()方法,以达到给Tom加上新的技能的要求(如下)

package com.bosen.www;
​
/**
 * <p>Tom实体类</p>
 * @author Bosen 2021/5/16 21:49
 */
public class Tom {
    public void eat() {
        System.out.println("吃饭");
    }
    public void walk() {
        System.out.println("走路");
    }
    public void run() {
        System.out.println("跑步");
    }
    public void jump() {
        System.out.println("跳跃");
    }
    public void playThePiano() {
        System.out.println("弹钢琴");
    }
}

但这种实现方式,我们对源代码进行了修改,明显不符合我们的开闭原则的要求。因此我们需要用拓展的方式让Tom的新技能优雅的添加上。

我们需要新建一个培训班类TrainingCourse,并设置了playThePiano()方法,这样下来我们的Tom只需要通过参加(继承)培训班,就学会了弹钢琴。

package com.bosen.www;
​
/**
 * <p>培训班类</p>
 * @author Bosen 2021/5/16 22:07
 */
public class TrainingCourse {
    public void playThePiano() {
        System.out.println("弹钢琴");
    }
} 

package com.bosen.www;
​
/**
 * <p>Tom实体类</p>
 * @author Bosen 2021/5/16 21:49
 */
public class Tom extends TrainingCourse{
    public void eat() {
        System.out.println("吃饭");
    }
    public void walk() {
        System.out.println("走路");
    }
    public void run() {
        System.out.println("跑步");
    }
    public void jump() {
        System.out.println("跳跃");
    }
}

这样,我们就无需修改Tom内部的代码,只需要通过继承的方式,即可达到新方法的添加。

二、依赖倒置原则

定义:

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

作用:

  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

示例:我们获取新闻时事方式有很多种,比如通过看电视获取,看报纸获取等等。我们来试试实现这一个功能。先创建一个TV类和Newspaper类,这两个类都有获取时事的功能getCurrentAffairs(),接下来我们继续请出Tom同学帮我们完成测试,创建Tom类,并实现从电视获取时事和从报纸获取时事的两个方法getCurrentAffairsByTV(),getCurrentAffairsByNewspaper()。代码如下:

package com.bosen.www;
​
/**
 * <p>电视类</p>
 * @author Bosen 2021/5/16 23:03
 */
public class TV {
    public void getCurrentAffairs() {
        System.out.println("通过电视获取时事");
    }
}
package com.bosen.www;
​
/**
 * <p>报纸类</p>
 * @author Bosen 2021/5/16 23:04
 */
public class Newspaper {
    public void getCurrentAffairs() {
        System.out.println("通过报纸获取时事");
    }
}
package com.bosen.www;
​
/**
 * <p>Tom实体类</p>
 * @author Bosen 2021/5/16 21:49
 */
public class Tom {
    public void getCurrentAffairsByTV() {
        new TV().getCurrentAffairs();
    }
​
    public void getCurrentAffairsByNewspaper() {
        new Newspaper().getCurrentAffairs();
    }
}

从如上代码我们可以发现,Tom对于TV和Newspaper类的依赖严重,内嵌进了Tom类内部,这样设计会使得代码不便于维护。根据依赖倒置原则,我们可以这样设计,修改如下:

package com.bosen.www;
​
/**
 * <p>发现时事的接口,定义了获取时事的方法</p>
 * @author Bosen 2021/5/16 23:10
 */
public interface IWatch {
    void getCurrentAffairs();
}
package com.bosen.www;
​
/**
 * <p>报纸类</p>
 * @author Bosen 2021/5/16 23:04
 */
public class Newspaper implements IWatch {
    @Override
    public void getCurrentAffairs() {
        System.out.println("通过报纸获取时事");
    }
}
package com.bosen.www;
​
/**
 * <p>电视类</p>
 * @author Bosen 2021/5/16 23:03
 */
public class TV implements IWatch {
    @Override
    public void getCurrentAffairs() {
        System.out.println("通过电视获取时事");
    }
}

package com.bosen.www;
​
/**
 * <p>Tom实体类</p>
 * @author Bosen 2021/5/16 21:49
 */
public class Tom {
​
    private IWatch watch;
​
    public Tom(IWatch watch) {
        this.watch = watch;
    }
​
    public void getCurrentAffairs() {
        watch.getCurrentAffairs();
    }
}
 

可以看到,我们在原有的基础上,增加了一个接口类,TV和Newspaper类实现了该接口,并且,Tom也无需关心具体获取时事的方法,由外部传入对应的类,Tom即可成功获取时事。这样的实现方式,不单止抽象了类之间的依赖关系,也提高了代码的维护性。

三、单一职责

定义:一个对象不应该承担过多的责任,当一个类或对象承担了过多的指责或者方法,容易消弱对象或类对其他职责的能力。并且,当客户端调用此类时,容易加入许多不必要的代码段,造成代码冗余。

作用:单一职责主要降低了代码的复杂度,提高了代码的可读性,随着代码的可读性加强以及复杂度的降低,代码的可维护性也跟着提升,与此同时,当需求变更时,需要修改某一功能的实现,只需修改该功能的代码即可,无需变更负责其他职责的代码。

四、接口隔离原则

定义:要求将庞大的接口拆分成细粒度小的接口,使接口只包含调用端感兴趣或者需要的接口。

作用:将臃肿庞大的接口拆分成细粒度更小的接口,可以预防外来的需求变更,提高代码可维护性,降低了系统的耦合性。

单一职责和接口隔离的区别:从大体上看,接口隔离原则与单一职责原则非常相像,但从细节上看又有着许多不同。单一职责主要关注的是职责的隔离,接口隔离关注的是接口依赖的隔离。单一职责主要约束类,针对程序的实现和细节,接口隔离注重整个项目的抽象体系的构建。

接下来我们使用具体代码来模仿一下:创建一个培训班接口TrainingCourse,定义钢琴课(playThePiano)和奥数课(mathematicalOlympiad),Tom去参加该培训班(实现接口),代码如下:

package com.bosen.www;
​
/**
 * <p>培训班类</p>
 * @author Bosen 2021/5/16 22:07
 */
public interface TrainingCourse {
    void playThePiano();
​
    void mathematicalOlympiad();
}
package com.bosen.www;
​
/**
 * <p>Tom实体类</p>
 * @author Bosen 2021/5/16 21:49
 */
public class Tom implements TrainingCourse {
    @Override
    public void playThePiano() {
        System.out.println("弹钢琴");
    }
    @Override
    public void mathematicalOlympiad() {
        System.out.println("奥数");
    }
}

通过上面的代码我们可以看出一点不合理的地方,就是我们Tom同学如果只想要去学习钢琴课的时候,Tom同学必须也要报奥数班,如果Tom不报奥数班(不实现mathematicalOlympiad方法),培训班就会告诉你一定要将奥数班也报了才可以正式上课(因为,Java的接口定义,实现该接口的类必须实现接口下的所有方法,否则编译无法通过),这种捆绑消费行为明显是不合理的。

接下来我们使用接口隔离的实现,对上面的代码进行修改。将培训班拆分成数学班(MathCourse)和钢琴班(PianoCourse),数学班教奥数(mathematicalOlympiad),钢琴班教弹钢琴(playThePiano),代码如下:

package com.bosen.www;
​
/**
 * <p>数学班接口</p>
 * @author Bosen 2021/5/17 13:40
 */
public interface MathCourse {
    void mathematicalOlympiad();
}
package com.bosen.www;
​
/**
 * <p>钢琴班接口</p>
 * @author Bosen 2021/5/17 13:40
 */
public interface PianoCourse {
    void playThePiano();
}
package com.bosen.www;
​
/**
 * <p>Tom实体类</p>
 * @author Bosen 2021/5/16 21:49
 */
public class Tom implements PianoCourse {
    @Override
    public void playThePiano() {
        System.out.println("弹钢琴");
    }
}

这样一来,我们的Tom同学就可以根据自己自身的需求,报自己感兴趣的培训班即可,无需被捆绑消费。

五、迪米特原则(最少知道原则)

定义:一个对象对其他对象保持最少的了解,尽量降低类与类之间的耦合,强调只与相关类交流。相关类指的是出现在成员变量、方法的输入、输出参数中的类。

作用:降低类之间的耦合度,提高了模块之间的独立性。提高了类的可复用性和扩展性。

示例:我们先创建四个对象(明星类Idol,经纪人类Agent,粉丝类Fans,媒体类Media)。我们通过这四个对象实现两个功能(明星与粉丝见面会、明星与媒体公司业务洽谈),由于明星更专注于专业领域的工作,开见面会,和媒体洽谈的事务都会交由经纪人完成。在这个场景中,明星与经纪人是朋友,而与粉丝和媒体是陌生人,明星不需要关注开粉丝见面会的具体流程,也不需要关注媒体商谈的过程,这些都交由经纪人完成即可,经纪人完成后告诉明星结果。这种设计完全符合我们的迪米特原则。下面我们通过代码来实现:

package com.bosen.www.test5;
​
/**
 * <p>明星类</p>
 * @author Bosen 2021/5/17 13:52
 */
public class Idol {
    private String name;
​
    public Idol(String name) {
        this.name = name;
    }
​
    public String getName() {
        return name;
    }
}
package com.bosen.www.test5;
​
/**
 * <p>粉丝类</p>
 * @author Bosen 2021/5/17 13:55
 */
public class Fans {
    private String name;
​
    public Fans(String name) {
        this.name = name;
    }
​
    public String getName() {
        return name;
    }
}
package com.bosen.www.test5;
​
/**
 * <p>媒体类</p>
 * @author Bosen 2021/5/17 13:55
 */
public class Media {
    private String name;
​
    public Media(String name) {
        this.name = name;
    }
​
    public String getName() {
        return name;
    }
}
package com.bosen.www.test5;
​
/**
 * <p>经纪人类</p>
 * @author Bosen 2021/5/17 13:54
 */
public class Agent {
    private Idol idol;      // 与明星打交道
    private Fans fans;      // 与粉丝打交道
    private Media media;    // 与媒体打交道
​
    public Agent(Idol idol, Fans fans, Media media) {
        this.idol = idol;
        this.fans = fans;
        this.media = media;
    }
​
    /*
     * 组织明星与粉丝的见面会
     */
    public void meeting() {
        System.out.println(
                this.idol.getName() + "与" + this.fans.getName() + "见面了!"
        );
    }
​
    /*
     * 组织明星与媒体进行商务洽谈
     */
    public void business() {
        System.out.println(
                this.idol.getName() + "与" + this.media.getName() + "进行洽谈!"
        );
    }
}
package com.bosen.www.test5;
​
/**
 * <p>测试类</p>
 * @author Bosen 2021/5/17 16:49
 */
public class Test {
    public static void main(String[] args) {
        Idol idol = new Idol("明星");
        Fans fans = new Fans("粉丝");
        Media media = new Media("媒体");
​
        Agent agent = new Agent(idol, fans, media);
​
        // 组织明星与粉丝的见面会
        agent.meeting();
        // 组织明星与媒体进行商务洽谈
        agent.business();
    }
}

测试程序输出结果

从上述测试的程序我们可以看到,明星无需知道粉丝和媒体的存在,具体的业务交由经纪人全权代理完成即可。类和类之间都相对独立,降低了耦合度,当项目规模增大时,便于维护。

六、里氏替换原则

定义:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

作用:约束子类对父类代码的修改,避免修改父类方法时引入新的错误。

示例:我们都知道正方形是特殊的矩形,正方形可以继承矩形。

又因为正方形的长宽必须相等,所以正方形类需要对长方形类设置宽高的方法中进行重写,此时违反了我们里氏替换原则,从而导致继承泛滥的问题,先让结合具体的代码看看这样做会出现什么样的弊端?

package com.bosen.www.test6;
​
/**
 * <p>矩形类</p>
 * @author Bosen 2021/5/17 17:22
 */
public class Rectangle {
    private int height;
    private int width;
​
    public int getHeight() {
        return height;
    }
​
    public void setHeight(int height) {
        this.height = height;
    }
​
    public int getWidth() {
        return width;
    }
​
    public void setWidth(int width) {
        this.width = width;
    }
}
package com.bosen.www.test6;
​
/**
 * <p>正方形类,继承矩形类,并重写父类方法</p>
 * @author Bosen 2021/5/17 17:22
 */
public class Square extends Rectangle {
​
    private int length;
​
    public void setLength(int length) {
        this.length = length;
    }
​
    public int getLength() {
        return length;
    }
​
    @Override
    public void setWidth(int width) {
        super.setWidth(length);
    }
​
    @Override
    public int getWidth() {
        return getLength();
    }
​
    @Override
    public void setHeight(int height) {
        super.setHeight(length);
    }
​
    @Override
    public int getHeight() {
        return getLength();
    }
}

假设我们规定,矩形的宽应该大于高,当我们用户设置的数值是高大于宽时,我们需要将矩形的宽高进行调整,因此定义一个方法resize(),使宽一直增大,直到宽大于高,后程序才停止。代码如下

package com.bosen.www.test6;
​
public class Test {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(20);
        rectangle.setWidth(15);
​
        resize(rectangle);
    }
    
    /*
     * 调整宽高,使宽大于等于高
     */
    public static void resize(Rectangle rectangle) {
        while (rectangle.getHeight() >= rectangle.getWidth()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
            System.out.println(
                    "width:"+rectangle.getWidth()+",height:"+rectangle.getHeight()
            );
        }
        System.out.println("方法结束!");
    }
}

执行结果如下:

将测试的方法改为子类如下:

package com.bosen.www.test6;
​
public class Test {
    public static void main(String[] args) {
        Square square = new Square();
        square.setLength(20);
​
        resize(square);
    }
    
    /*
     * 调整宽高,使宽大于等于高
     */
    public static void resize(Rectangle rectangle) {
        while (rectangle.getHeight() >= rectangle.getWidth()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
            System.out.println(
                    "width:"+rectangle.getWidth()+",height:"+rectangle.getHeight()
            );
        }
        System.out.println("方法结束!");
    }
}

执行后为一个死循环,一直重复输出

上面代码的死循环正是因为子类重写了父类的方法导致的,也正是因为这种继承泛滥的问题,才出现了里氏替换原则的约束思想。

七、合成复用原则

定义:要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

作用:

  • 维持类的封装性
  • 降低新旧类之间的耦合度
  • 提高代码复用的灵活性    

示例:一辆汽车,它可以分为许多类型,我们假设从类型上分有汽油车和新能源车,颜色上分有黑色和白色。在不运用合成服用原则思想的前提下,当我们要生产一辆黑色的汽油车时,我需要做以下步骤,首先定义一个顶层父类汽车类Car,然后是汽车的类型类(汽油类型GasolineCar、新能源类型NewEnergyCar),最后是颜色类(BlackGasolineCar),实现代码如下:

package com.bosen.www.test7;
​
/**
 * <p>顶层汽车类</p>
 * @author Bosen 2021/5/17 20:05
 */
public class Car {
    public String i = "车";
}

package com.bosen.www.test7;
​
/**
 * <p>汽油车类,继承Car</p>
 * @author Bosen 2021/5/17 20:05
 */
public class GasolineCar extends Car {
    public String type = "汽油";
}
package com.bosen.www.test7;
​
/**
 * <p>新能源汽车类,继承Car</p>
 * @author Bosen 2021/5/17 20:06
 */
public class NewEnergyCar extends Car {
    public String type = "新能源";
}
package com.bosen.www.test7;
​
/**
 * <p>黑色汽油车类</p>
 * @author Bosen 2021/5/17 20:06
 */
public class BlackGasolineCar extends GasolineCar {
    public String color = "黑色";
}
package com.bosen.www.test7;
​
/**
 * <p>测试类</p>
 * @author Bosen 2021/5/17 20:10
 */
public class Test {
    public static void main(String[] args) {
        BlackGasolineCar car = new BlackGasolineCar();
        System.out.println(car.color+car.type+car.i);
    }
}

程序运行结果如下:

这样我们就完成了对黑色汽油车的创建,但这样实现,类和类之继承关系太过复杂,不便于维护。所以我们引入合成复用的思想对代码进行修改,修改如下:

package com.bosen.www.test7;
​
/**
 * <p>汽车类</p>
 * @author Bosen 2021/5/17 20:05
 */
public class Car {
    private String color;
    private String type;
​
    @Override
    public String toString() {
        return color + type;
    }
​
    public void setColor(String color) {
        this.color = color;
    }
​
    public void setType(String type) {
        this.type = type;
    }
}
package com.bosen.www.test7;
​
/**
 * <p>汽车颜色类</p>
 * @author Bosen 2021/5/17 20:20
 */
public class Color {
    public static String BLACK = "黑色";
    public static String WHITE = "白色";
}
package com.bosen.www.test7;
​
public class Type {
    public static String GASOLINE = "汽油车";
    public static String NEW_ENERGY = "新能源车";
}
package com.bosen.www.test7;
​
/**
 * <p>测试类</p>
 * @author Bosen 2021/5/17 20:10
 */
public class Test {
    public static void main(String[] args) {
        Car car = new Car();
        car.setColor(Color.BLACK);
        car.setType(Type.GASOLINE);
​
        System.out.println(car);
    }
}

程序运行结果如下:

经过修改,我们不难发现,运用了合成复用思想的程序代码,类与类之间的关系更抽象化了,对于代码的复用率,和灵活性也随之提高。


总结

学习设计原则是学习设计模式的基础。在实际开发的过程中,我们并不需要要求所有的代码都遵循设计原则,我们还要考虑人力、时间、成本、质量,不能刻意或“强迫”追求完美,但要在适当的场景下遵循设计原则。

   👇扫描二维码关注

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云丶言

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

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

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

打赏作者

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

抵扣说明:

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

余额充值