一、软件架构设计的七大原则简介

1.1 开闭原则(Open-Closed Principle,OCP)

开闭原则是一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。所谓的开闭也是对扩展和修改两个行为的一个原则。强调是用抽象扩展架构,用实现扩展细节。可以提高软件系统的可复用性及可维护性。开闭原则,是面向对象设计中最基础的设计原则。它指导我们如何建立稳定灵活的系统,例如:版本更新,我们尽可能不修改代码,但是可以增加新功能。

在现实生活中也有很多开闭原则的例子,比如,很多互联网公司都实行弹性的工作时间,规定每天工作8小时。意思就是说,对于工作日每天工作8小时这个规定是关闭的,但是你什么时候来,什么时候走是开放的。早来早走,晚来晚走。

实现开闭原则的核心思想就是面向抽象编程,以某新华书店的图书为例,首先创建一个课程接口类:

public class IBook {    Integer getId();    String getName();    Double getPrice();}

我们来创建一个计算机图书的类ComputerBook:

public class ComputerBook implements IBook {    private Integer id;    private String name;    protected Double price;    public ComputerBook(Integer id, String name, Double price) {        this.id = id;        this.name = name;        this.price = price;    }        public Integer getId() {        return this.id;    }        public String getName() {        return this.name;    }        public Double getPrice() {        return this.price;    }}

现在我们要给计算机类的图书做活动,价格优惠。如果修改ComputerBook类的getPrice()方法,则会存在一定的风险,可能会影响其它地方的调用结果。我们如何在不修改原有代码的前提下,实现价格优惠这个功能呢?现在,我们再写一个处理优惠逻辑的类,ComputerDiscountBook类:

public class ComputerDiscountBook extends ComputerBook {    public ComputerDiscountBook(Integer id, String name, Double price) {        super(id, name, price);    }        public Double getOriginPrice() {        return super.price;    }        public Double getPrice() {        return super.price * 0.8;    }}

1.2 依赖倒置原则(Dependence Inversion Principle,DIP)

依赖倒置原则是指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。通过依赖倒置,可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所造成的风险。

还是以课程为例,创建一个类Kevin:

public class Kevin {    public void readComputerBook() {        System.out.println("Kevin正在阅读电脑书籍");    }    public void readScienceBook() {        System.out.println("Kevin正在阅读科学书籍");    }    public static void main(String[] args) {        Kevin kevin = new Kevin();        kevin.readComputerBook();        kevin.readScienceBook();    }    }

Kevin正在阅读电脑书籍和科学书籍。大家知道电脑书籍的种类很多,现在人工智能AI这么火热,Kevin想学习关于人工智能方面的电脑书籍。这个时候,业务需要扩展,我们需要从底层到调用层一次修改代码来满足业务要求。在Kevin中增加readAIBook()的方法,在调用层也要追加调用。这样一来,系统发布的时候,实际上不是很稳定。如何优化我们的代码,创建一个书籍的抽象IBook接口:

public interface IBook {    void read();}

然后写ComputerBook类:

public class ComputerBook implements IBook {    @Override    public void read() {        System.out.println("Kevin正在阅读电脑书籍");    }}

再写ScienceBook类:

public class ScienceBook implements IBook {    @Override    public void read() {        System.out.printf("Kevin正在阅读科学书籍");    }}

修改Kevin类:

public class Kevin {    public void read(IBook book) {        book.read();    }    public static void main(String[] args) {        Kevin kevin = new Kevin();        kevin.read(new ComputerBook());        kevin.read(new ScienceBook());    }}

我们来看main方法调用,无论Kevin想读哪些类的新书,只需要创建一个类,通过传参的方式告诉Kevin,而不需要修改底层代码来实现。这种实现方式叫做依赖注入。注入的方式有构造器注入和setter注入两种方式。

构造器注入实现方式:

public class Kevin {    public IBook book;    public Kevin(IBook book) {        this.book = book;    }    public void read() {        book.read();    }    public static void main(String[] args) {        Kevin kevin = new Kevin(new ComputerBook());        kevin.read();    }}

根据构造器注入,在调用时,每次都需要创建实例。如果Kevin是全局单例的话,则只能选择Setter方式来注入,继续修改Kevin类的代码:

public class Kevin {    public IBook book;    public void setBook(IBook book) {        this.book = book;    }    public void read() {        book.read();    }    public static void main(String[] args) {        Kevin kevin = new Kevin();        kevin.setBook(new ComputerBook());        kevin.read();        kevin.setBook(new ScienceBook());        kevin.read();    }}

以抽象为基准比以细节为基准搭建起来的代码架构要稳定得多,因此拿到需求任务后,要面向接口编程,先设计顶层再琢磨细节来设计代码结构。

1.3 单一职责原则(Simple Responsibility Principle,SRP)

单一职责原则是值不要存在多于一个导致类变更的原因。假设我们有一个类要负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能会导致另一个职责的功能发生故障。这样一来,这个类存在两个导致类变更的原因。怎么来解决这个问题呢?就需要给两个职责分别用两个类来实现,进行解耦。后期需求维护相互不受影响。这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说就是一个class/interface/mothod只负责一项职责。 这里用在线直播课程的案例来举例,课程有在线直播课和录播课。直播课不能快进和后退,录播课程可以任意的反复观看,功能职责不一样。创建一个Course类:

public class Course {    public static final String COURSE_NAME = "直播课";    public void study(String courseName) {        if(COURSE_NAME.equals(courseName)) {            System.out.println(courseName + "不能快进和快退");        }else {            System.out.println(courseName + "可以反复观看");        }    }    public static void main(String[] args) {        Course course = new Course();        course.study("直播课");        course.study("录播课");    }}

从上面的代码看,Course类承担了两种逻辑。现在需要对课程进行加密,那么直播课和录播课的加密逻辑是不一样的,必须要修改代码。而修改代码逻辑必然会相互影响并且容易造成风险。我们对职责进行解耦,分别创建LiveCourse和ReplayCourse两个类:

LiveCourse类:

public class LiveCourse {    public void study(String courseName) {        System.out.println(courseName + "不能快进和快退");    }}

ReplayCourse类:

public class ReplayCourse {    public void study(String courseName) {        System.out.println(courseName + "可以反复观看");    }}

调用main方法:

   public static void main(String[] args) {        LiveCourse liveCourse = new LiveCourse();        liveCourse.study("直播课");        ReplayCourse replayCourse = new ReplayCourse();        replayCourse.study("录播课");   }

随着业务的发展,课程也要做权限。没有付费的vip会员可以获取课程基本信息,已经付费的vip会员可以获得视频流,即获得视频观看权限。对于控制课程层面上至少有两个职责。我们可以把展示职责和管理职责分离出来,实现同一个抽象依赖。设计一个顶层接口,创建ICourse接口:

public interface ICourse {    /**     * 获得课程信息     * @return     */    String getCourseName();    /**     * 获得视频流     * @return     */    byte[] getCourseVideo();    /**     * 学习课程     */    void studyCourse();    /**     * 退款     */    void refundCourse();}

可以把这个接口拆成两个接口,创建一个接口ICourseInfo和ICourseManager:

接口ICourseInfo:

public interface ICourseInfo {    /**     * 获得课程信息     * @return     */    String getCourseName();    /**     * 获得视频流     * @return     */    byte[] getCourseVideo();}

接口ICourseManager:

public interface ICourseManager {    /**     * 学习课程     */    void studyCourse();    /**     * 退款     */    void refundCourse();}

下面来看下方法层面的单一职责的代码设计。有时候我们为了偷懒,通常会把方法写成下面这样子的:

private void mofifyUserInfo(String userName, String address) {    userName = "Kevin";    address = "Beijing";}

还可能写成这样子:

private void modifyUserInfo(String userName, String... fields) {    userName = "Kevin";    }private void modifyUserInfo(String userName, String address, boolean bool) {    if(bool) {            }else {            }    userName = "Kevin";    address = "Beijing";}

显然上面的modifyUserInfo()方法中承担了多个职责,既可以修改userName,也可以修改address,甚至更多的值,明显不符合单一职责。这样我们需要对代码进行修改,把这个方法拆分成两个方法:

private void modifyUserName(String userName) {    userName = "Kevin";}private void modifyAddress(String address) {    address = "Beijing";}

修改之后的代码看起来简单,且维护起来更加容易。但是,我们在实际开发项目过程中,项目直接会相互依赖,组合、聚和这些关系,还有项目的规模,周期,技术人员的水平,对进度的把控,很多类都不符合单一职责。但是,我们在编码的过程中尽量做到单一职责,这样对我们项目的后期维护是有很大的帮助的。

1.4 接口隔离原则 (Interface Segregation Principle,ISP)

接口隔离原则是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。这个原则指导我们在设计接口的时候应该注意以下几点:

1、一个类对一类的依赖应该建立在最小的接口之上;

2、建立单一接口,不要建立庞大臃肿的接口;

3、尽量细化接口,接口中的方法尽量少(不是越少越好,要适度)。

接口隔离原则符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性以及可维护性。在设计接口的时候,要多花时间去思考,要考虑业务模型,包括以后有可能发生变更的地方还要做一些预判。所以对于抽象,对于业务模型的理解是非常重要的。下面举例来看一个动物行为的抽象接口:

IAnimal接口:

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

Bird 类实现:

public class Bird implements IAnimal {    @Override    public void eat() {    }    @Override    public void fly() {    }    @Override    public void swim() {    }}

Dog 类实现:

public class Dog implements IAnimal {    @Override    public void eat() {    }    @Override    public void fly() {    }    @Override    public void swim() {    }}

可以看出,Bird 的 swim()方法可能只能空着,Dog 的fly()方法显然不可能的。这时候,我们针对不同动物行为来设计不同的接口,分别设计 IEatAnimal,IFlyAnimal 和ISwimAnimal 接口,来看代码:

IEatAnimal接口:

public interface IEatAnimal {    void eat();}

IFlyAnimal接口:

public interface IFlyAnimal {    void fly();}

ISwimAnimal接口:

public interface ISwimAnimal {    void swim();}

Dog 只实现 IEatAnimal 和 ISwimAnimal 接口:

public class Dog implements IEatAnimal, ISwimAnimal {    @Override    public void eat() {            }    @Override    public void swim() {    }}

1.5 迪米特法则(Law of Demeter, LoD)

迪米特法则是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle, LKP),尽量降低类与类之间的耦合。主要强调只和朋友说话,不和陌生人说话。出现在成员变量、方法输入、输出参数中的类都可以称之为成员朋友类,而出现在方法体内部的类不属于朋友类。

现在来设计一个权限系统,老板需要查看目前发布到线上的课程数量。这个老板找到项目负责人去进行统计,项目负责人再把统计结果告诉老板。来看一下代码示例:

Course类:

public class Course {}

TeamLeader类:

public class TeamLeader {    public void checkNumberOfCourses(List courseList){        System.out.println("目前已发布的课程数量是:"+courseList.size());    }}

Boss类:

public class Boss {    public void commandCheckNumber(TeamLeader teamLeader){        //模拟 Boss 一页一页往下翻页,TeamLeader 实时统计        List courseList = new ArrayList();        for (int i= 0; i < 10 ;i ++){            courseList.add(new Course());        }        teamLeader.checkNumberOfCourses(courseList);    }    public static void main(String[] args) {        Boss boss = new Boss();        TeamLeader teamLeader = new TeamLeader();        boss.commandCheckNumber(teamLeader);    }}

上面的代码根据迪米特原则,老板Boss只想要结果,不需要跟Course产生直接交流。而课程负责人统计要引用Course对象。Boss和Course并不是朋友,我们需要修改代码:

TeamLeader类:

public class TeamLeader {    public void checkNumberOfCourses(){        List courseList = new ArrayList();        for(int i = 0 ;i < 10;i++){            courseList.add(new Course());        }        System.out.println("目前已发布的课程数量是:"+courseList.size());    }}

Boss类:

public class Boss {    public void commandCheckNumber(TeamLeader teamLeader){        teamLeader.checkNumberOfCourses();    }    public static void main(String[] args) {        Boss boss = new Boss();        TeamLeader teamLeader = new TeamLeader();        boss.commandCheckNumber(teamLeader);    }}

得到的:学习软件设计原则,千万不能形成强迫症。碰到业务复杂的场景,我们需要随机应变。

1.6 里氏替换原则(Liskov Substitution Principle,LSP)

里氏替换原则是指如果对每一个类型T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。

定义看上去还是比较抽象,可以理解为如果适应一个父类的话,那一定是适用于子类,所有的引用父类的地方必须能透明的使用子类的对象,子类对象能够替换父类对象,而程序逻辑不变。 总结一下:

引申含义:子类可以扩展父类的功能,但不能改变父类原有的功能。

1、子类可以实现父类的抽象方法,但不能覆盖父类的抽象方法;

2、子类中可以增加自己特有的方法;

3、当子类的方法覆盖父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松;

4、当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或者相等。

我们在开闭原则的代码案例中在获取折后时覆盖了父类的getPrice()方法,增加了一个获取优惠价格后的方法getOriginPrice(),显然就违背了里氏替换原则。接下来我们不覆盖getPrice()方法,增加getDiscountBook()方法:

public class ComputerDiscountBook extends ComputerBook {    public ComputerDiscountBook(Integer id, String name, Double price) {        super(id, name, price);    }    public Double getDiscountBook() {        return super.getPrice() * 0.8;    }}

使用里氏替换原则有以下几个优点:

1、约束继承泛滥,开闭原则的一种体现;

2、加强程序的健壮性,同时变更时可以做到很好的兼容性,提高程序的维护性、扩展性。降低需求变更时引入的风险。

现在用正方形、矩形和四边形来说明里氏替换原则,我们都知道正方形是一个特殊的长方形,创建一个长方形父类Rectangle:

public class Rectangle {    private long width;    private long height;    public long getWidth() {        return width;    }    public void setWidth(long width) {        this.width = width;    }    public long getHeight() {        return height;    }    public void setHeight(long height) {        this.height = height;    }}

创建正方形Square类继承Rectangle长方形类,

public class Square extends Rectangle {    private long length;    public long getLength() {        return length;    }    public void setLength(long length) {        this.length = length;    }    @Override    public long getWidth() {        return getLength();    }    @Override    public long getHeight() {        return getLength();    }    @Override    public void setHeight(long height) {        setLength(height);    }    @Override    public void setWidth(long width) {        setLength(width);    }}

在测试类中创建 resize()方法,根据逻辑长方形的宽应该大于等于高,我们让高一直自增,知道高等于宽变成正方形:

public static void resize(Rectangle rectangle){        while (rectangle.getWidth() >= rectangle.getHeight()){            rectangle.setHeight(rectangle.getHeight() + 1);            System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());        }        System.out.println("resize 方法结束" +                "width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());}

测试代码:

 public static void main(String[] args) {        Rectangle rectangle = new Rectangle();        rectangle.setWidth(20);        rectangle.setHeight(10);        resize(rectangle);}

运行结果:

修改代码把长方形Rectangle替换成它的子类正方形Square:

public static void main(String[] args) {        Square square = new Square();        square.setLength(10);        resize(square);}

这时候我们运行的时候就出现了死循环,违背了里氏替换原则,将父类替换为子类后,程序运行结果没有达到预期。因此,我们的代码设计是存在一定风险的。里氏替换原则只存在父类与子类之间,约束继承泛滥。再来创建一个基于长方形与正方形共同抽象的四边形Quadrangle接口:

public interface Quadrangle {        long getWidth();        long getHeight();}

修改长方形 Rectangle 类:

public class Rectangle implements Quadrangle {    private long height;    private long width;    @Override    public long getWidth() {        return width;    }    public long getHeight() {        return height;    }    public void setHeight(long height) {        this.height = height;    }    public void setWidth(long width) {        this.width = width;    }}

修改正方形类 Square 类:

public class Square implements Quadrangle {    private long length;    public long getLength() {        return length;    }    public void setLength(long length) {        this.length = length;    }    @Override    public long getWidth() {        return length;    }    @Override    public long getHeight() {        return length;    }}

此时,如果我们把 resize()方法的参数换成四边形 Quadrangle类,方法内部就会报错。因为正方形 Square 已经没有了 setWidth()和 setHeight()方法了。因此,为了约束继承泛滥,resize()的方法参数只能用 Rectangle 长方形。

1.7 合成复用原则 (Composite/Aggregate Reuse Principle,CARP)

合成复用原则是指尽量使用对象组合/聚和,而不是用继承关系达到对象复用的目的。这样可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其它类造成的影响也较少。

继承又叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚和也成为黑箱复用,对类以外的对象是无法获取到实现细节的。 需要根据具体的业务场景来设计代码,其实也要遵循OOP模型。这里以数据库操作为例,先创建DBConnection类:

public class DBConnection {    public String getConnection(){        return "MySQL 数据库连接";    }}

创建 ProductDao 类:

public class ProductDao {    private DBConnection dbConnection;    public void setDbConnection(DBConnection dbConnection) {        this.dbConnection = dbConnection;    }    public void addProduct(){        String conn = dbConnection.getConnection();        System.out.println("使用"+conn+"增加产品");    }}

这就是非常典型的合成复用原则的应用场景。但是DBConnection还不是一种抽象,不便于系统扩展。目前系统只支持Mysql数据库连接,假设业务发生变化,数据库操作层要支持Oralce数据库。当然,可以在DBConnection中增加对Oracle数据库支持的方法。但是违背了开闭原则。我们可以不修改Dao的代码,将DBConnection改为abstract,代码如下:

public abstract class DBConnection {    public abstract String getConnection();}

然后,将 MySQL 的逻辑抽离:

public class MySqlConnection extends DBConnection {    @Override    public String getConnection() {        return "MySQL 数据库连接";    }}

再创建 Oracle 支持的逻辑:

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

二、 设计原则总结

学习设计原则,学习设计模式的基础。在实际开发过程中,并不是所有的代码都要遵循设计原则。我们需要考虑人力、时间、成本、质量,不是刻意的追求完美,要在适当的场景下遵循设计原则,体现的是一种平衡的取舍,帮助我们设计出更加优美的代码结构。

软件架构设计_架构师内功心法,软件架构设计的七大原则精选案例相关推荐

  1. 架构师内功心法之设计原则

    一.架构师内功心法之设计原则 1.为什么要学习软件架构设计原则 1.1.课程目标 通过对节课内容的学习,了解设计原则的重要性. 掌握七大设计原则的具体内容. 1.2.内容定位 学习设计原则,学习设计模 ...

  2. 读书_w3c架构师01通用设计与方法论

    原文:读书_w3c架构师01通用设计与方法论 读书_w3c架构师 架构 秒杀系统优化思路 基本思路 (1)将请求尽量拦截在系统上游(不要让锁冲突落到数据库上去) (2)充分利用缓存,秒杀买票,这是一个 ...

  3. 软件架构解读与架构师角色培养——希赛嘉宾聊天实录

    软件架构解读与架构师角色培养--希赛嘉宾聊天实录[1]                                  [url]http://www.csai.cn[/url] 作者:温昱 来源: ...

  4. 解决方案架构师我需要懂代码吗_架构师真正要学会的事情

    本文引自周爱民老师[就职于南潮(Ruff),现任架构师一职]写给<程序员必读之软件架构>一书的推荐序. 编者按:软件架构师是在一个软件项目开发过程中,将客户的需求转换为规范的开发计划及文本 ...

  5. 【系统架构师】软件架构设计——2需求与质量属性

    软件架构设计--需求与质量 软件的属性包括功能属性和质量属性,但是,软件架构重点关注的是质量属性.因为,在大量可能的结构中,可以使用不同的结构来实现同样的功能性,即功能性在很大程度上是独立于结构的,架 ...

  6. IT架构师介绍-软件架构设计学习第一天(非原创)

    文章大纲 一.架构师定义 二.架构师分类与具备能力 三.研发人员发展的技术路线 四.架构师知识体系 五.参考文章 一.架构师定义   什么是架构师,这个聊架构话题时永恒的问题.每个公司对架构师的定位也 ...

  7. 软件体系结构设计文档_一个java架构师是如何设计出一个好的架构的

    一.架构的定义 所谓一千个架构师中有一千种"最好的架构"模式. "架构"是我们行业中非常普遍的词,表示它也必须是经过长时间磨合后形成的词. 架构一词的含义是什么 ...

  8. 微服务架构 接口交互问题_架构师的故事:设计微服务架构

    架构师在软件项目中的作用是提供待解决问题的工作模型.架构师的工作是提供脚手架,开发人员将根据这些脚手架构建他们的代码,使应用程序所有部件都组合在一起. 在构建微服务架构时,项目的架构师主要关注以下3个 ...

  9. java的string访问某个元素_架构师必懂的——RBAC基于角色的访问权限设计

    RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限.这就极大地简化了权限的管理.这 ...

最新文章

  1. python 怎样让 print 打印的结果不换行
  2. 这个机器狗引起网友争议,「持枪机器狗」射程达1200米
  3. BZOJ 2137 submultiple(约数,拉格朗日插值求自然数k次幂和)【BZOJ 修复工程】
  4. 为什么有的人知道很多却一事无成?
  5. Linux下全局安装composer方法
  6. SQL Server Insert 操作效率(堆表 VS 聚集索引表)
  7. linux实验目的能学会什么意思,Linux实验.doc
  8. 第三章 Joomla!扩展开发:后端开发
  9. 【英语学习】【English L06】U06 Banking L5 I'd like to change Chinese *yuan* into pounds
  10. python list remove_python list有关remove的问题
  11. aes js 加盐值 解密_cryptoJS AES 加解密简单使用
  12. 关于计算机软件的使用 正确的认识应该是,计算机二级考试单选题训练题库
  13. C语言系列(5) --- C语言文件的操作
  14. java osm pbf_OSM PBF 文件格式说明
  15. 将UTC(字符串包含TZ的时间)时间转换成本地时间 python
  16. centos7 安装 oceanbase 单机版测试
  17. 从隐函数存在定理到隐函数定理
  18. [转]IDA + GDBServer实现iPhone程序远程调试
  19. oracle natural join qualifier,自然连接(natural join)
  20. 眼图形成理论研究(关于眼图的原理、基础及测量)

热门文章

  1. c#实现ajax通信:向后台发送JSON字符串,接收响应字符串,并转换为对象
  2. java实现图片裁剪
  3. CSS样式设置语法全解,样式优先级、值和单位、字体、文本、块级元素,行内元素,替换元素、非替换元素、display、float、position、table、li、光标、边距边框、轮廓、颜色背景
  4. zynq文档阅读之GPIO的中断
  5. 终端短路和终端开路的无耗传输线的输入阻抗和导纳
  6. linux常见术语示意
  7. [RN] React Native 定义全局变量
  8. laravel5.4中验证与错误提示设置
  9. 并发编程之多线程基础-Thread和Runnable的区别及联系(二)
  10. Mysql自动设置时间(自动获取时间,填充时间)