软件架构设计原则-开闭、依赖倒置、单一职责、接口隔离、迪米特、里氏替换、合成复用,附Java语言示例讲解

场景

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

是指一个软件实体(如类、模块和函数)应该对外扩展开放,对修改关闭。所谓的关闭,也正是对扩展和修改两个行为的一个原则。

它强调的是用抽象构建框架,用实现扩展细节,可以提高软件系统的可复用性和可维护性。

开闭原则是面向对象设计的最基本原则,例如版本更新,可以实现尽量不修改源代码的前提下增加新功能。

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

是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。

抽象不应该依赖细节,细节应该依赖抽象。可以减少类与类之间的耦合性,提高系统的稳定性,提高代码可读性和可维护性,

降低修改程序的风险。

3、单一职责原则(Simple Responsibility Pinciple,SRP)

是指不要存在多于一个导致变更的原因。如果一个类负责两个职责,

修改其中一个,则可能导致另一个出现问题。所以将多个职责用多个类进行解耦。

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

是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。

①一个类对另一个类的依赖应该建立在最小的接口之上。

②建立单一接口,不要建立庞大臃肿的接口。

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

接口隔离原则符合高内聚低耦合的设计思想,可以使类具有很好的可读性、可扩展性。

5、迪米特原则(Law of Demeter LoD)

是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。

迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称为成员朋友类,

而出现在方法体内部的类不属于朋友类,也就是说陌生的类不要以局部变量的形式出现在类的内部。

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

是指如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都替换为O2时,程序P

的行为没有发生变化,那么类型T2是类型T1的子类型。

可以理解为一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,

子类对象能够替换父类对象,而程序逻辑不变。

或者说子类可以扩展父类的功能,但不能改变父类原有的功能:

①子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

②子类可以增加自己特有的方法。

③当子类的方法重载父类的方法时,方法的前置条件(方法的输入/入参)要比父类方法的输入参数更宽松。

④当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更严格或与父类一样。

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

是指尽量使用对象组合/聚合而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,

一个类的变化对其他类造成的影响相对较少。

继承叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合称为黑箱复用,我们是无法获取到类以外的对象的实现细节的。

虽然我们要根据具体的业务场景来做代码设计,但也需要遵循OOP模型。

注:

博客:
霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。

实现

开闭原则示例

1、以商城商品为例,新建一个商品接口Goods

package com.ruoyi.demo.designPrinc.ocp;

/**
 * 商品接口
 */
public interface IGoods {
    Integer getId();
    String getName();
    Double getPrice();
}

2、商品有很多种类,这里新建一个电脑的类Computer

package com.ruoyi.demo.designPrinc.ocp;

public class Computer implements IGoods{

    private Integer Id;
    private String name;
    private Double price;

    public Computer(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;
    }
}

3、现在要给电脑做活动,价格打88折。

如果修改Computer中的getPrice()方法,则存在一定风险,可能会影响另外调用获取价格的地方。既要不修改原有代码,又

要实现价格优惠这个功能,可以写一个处理优惠逻辑的类ComputerDiscounts

package com.ruoyi.demo.designPrinc.ocp;

/**
 * 要给电脑类做活动,搞优惠。如果修改Computer的getPrice方法,则存在一定的风险,可能影响其他地方的调用结果
 */
public class ComputerDiscounts extends Computer{
    public ComputerDiscounts(Integer id, String name, Double price) {
        super(id, name, price);
    }

    public Double getOrginPrice(){
        return super.getPrice();
    }

    public Double getPrice(){
        return super.getPrice()*0.88;
    }
}

依赖倒置原则示例

1、还是以商品类为例,先创建学生类

package com.ruoyi.demo.designPrinc.dip;

public class Student {

    public void buyComputer(){
        System.out.println("学生购买了电脑");
    }

    public void buyBook(){
        System.out.println("学生购买了书籍");
    }
}

调用学生类的购买电脑和购买书籍的方法

package com.ruoyi.demo.designPrinc.dip;

public class dipTest {
    public static void main(String[] args) {
        Student student = new Student();
        student.buyComputer();
        student.buyBook();
    }
}

学生购买了电脑和书籍,如果还要购买别的商品。这时候因为业务扩展,要从低层到高层(调用层)依次修改代码。

在Student类中添加buyPen()方法,在高层调用方也要追加调用。这样一来,系统发布之后很不稳定,可能导致

意想不到的风险。

下面优化代码

创建一个商品的抽象IGoods接口,抽离出购买方法

package com.ruoyi.demo.designPrinc.dip;

public interface IGoods {
    void buy();
}

然后编写购买电脑类

package com.ruoyi.demo.designPrinc.dip;

public class BuyComputer implements IGoods{

    @Override
    public void buy() {
        System.out.println("学生买了电脑");
    }
}

再编写购买书籍类

package com.ruoyi.demo.designPrinc.dip;

public class BuyBook implements IGoods{
    @Override
    public void buy() {
        System.out.println("学生买了书籍");
    }
}

修改Student

package com.ruoyi.demo.designPrinc.dip;

public class Student {
    public void buy(IGoods goods){
        goods.buy();
    }
}

最后修改调用方代码

package com.ruoyi.demo.designPrinc.dip;

public class dipTest {
    public static void main(String[] args) {
        Student student = new Student();
        student.buy(new BuyComputer());
        student.buy(new BuyBook());
    }
}

这时,当再有新的业务,比如购买手机等扩展时,只需要新建一个类,通过传参的方式告诉Student,

而不需要修改底层代码。这种方式也叫依赖注入。注入的方式还有构造器注入和Setter方法。

单一职责原则示例

用商品举例,自营商品可以无条件退货,代理商品需要协商退货。创建商品类

package com.ruoyi.demo.designPrinc.srp;

public class Goods {
    public void buy(String goodsType){
        if("自营".equals(goodsType)){
            System.out.println(goodsType+"商品可以无条件退货");
        }else {
            System.out.println(goodsType+"商品需要协商退货");
        }
    }
}

调用代码

package com.ruoyi.demo.designPrinc.srp;

public class srpTest {
    public static void main(String[] args) {
        Goods goods = new Goods();
        goods.buy("自营");
        goods.buy("代理");
    }
}

上面逻辑中,商品Goods类处理两种逻辑,假如需要对商品进行活动促销,两种类型商品的活动逻辑不一样,

必须修改代码,而修改代码势必会相互影响。所以对职责进行解耦。

分别创建两个类

proprietaryGoods:

package com.ruoyi.demo.designPrinc.srp;

/**
 * 自营商品
 */
public class proprietaryGoods {
    public void buy(String goodsType){
        System.out.println(goodsType+"商品需要协商退货");
    }
}

agentGoods :

package com.ruoyi.demo.designPrinc.srp;

/**
 * 代理商品
 */
public class agentGoods {
    public void buy(String goodsType){
        System.out.println(goodsType+"商品可以无条件退货");
    }
}

调用代码修改

package com.ruoyi.demo.designPrinc.srp;

public class srpTest {
    public static void main(String[] args) {
        proprietaryGoods proprietaryGoods = new proprietaryGoods();
        proprietaryGoods.buy("自营");
        agentGoods agentGoods = new agentGoods();
        agentGoods.buy("代理");
    }
}

业务发展,要增加会员VIP业务。VIP可以获取商品优惠券,普通会员只能获取商品基本价格。

所以在控制商品上有两个职责,可以将展示的职责和管理的职责分开,实现同一个抽象依赖。

设计顶层接口IGoods

package com.ruoyi.demo.designPrinc.srp;

import java.util.List;

public interface IGoods {
    //获取商品价格
    String getGoodsPrice();
    //获取商品优惠券
    List getCoupons();
    //购买商品
    void buyGoods();
    //商品退货
    void returnGoods();
}

将这个接口拆分成两个接口

IGoodsInfo

package com.ruoyi.demo.designPrinc.srp;

import java.util.List;

public interface IGoodsInfo {
    //获取商品价格
    String getGoodsPrice();
    //获取商品优惠券
    List getCoupons();
}

IGoodsManager

package com.ruoyi.demo.designPrinc.srp;

public interface IGoodsManager {
    //购买商品
    void buyGoods();
    //商品退货
    void returnGoods();
}

接口隔离原则示例

商品类接口

package com.ruoyi.demo.designPrinc.isp;

public interface IGoods {
    void eat();
    void drink();
    void wear();
}

有能吃的、能喝的、能穿的。

食品类

package com.ruoyi.demo.designPrinc.isp;

public class FoodGoods implements IGoods{
    @Override
    public void eat() {

    }

    @Override
    public void drink() {

    }

    @Override
    public void wear() {

    }
}

衣物类

package com.ruoyi.demo.designPrinc.isp;

public class ClothesGoods implements IGoods{
    @Override
    public void eat() {

    }

    @Override
    public void drink() {

    }

    @Override
    public void wear() {

    }
}

所以,如果按照上面的进行设计,那么食品类的wear只能空着,衣物类的eat和drink也只能空着。

所以分别设计吃、喝、穿三个接口

package com.ruoyi.demo.designPrinc.isp;

public interface IEatGoods {
    void eat();
}

package com.ruoyi.demo.designPrinc.isp;

public interface IDrinkGoods {
    void drink();
}

package com.ruoyi.demo.designPrinc.isp;

public interface IWearGoods {
    void wear();
}

然后食品类是需要实现吃、喝接口即可

package com.ruoyi.demo.designPrinc.isp;

public class FoodGoodsNew implements IEatGoods,IDrinkGoods{

    @Override
    public void drink() {

    }

    @Override
    public void eat() {

    }
}

迪米特原则示例

老板要采购员统计符合条件的商品数量。

商品类

package com.ruoyi.demo.designPrinc.lod;

public class Goods {
}

采购员

package com.ruoyi.demo.designPrinc.lod;

import java.util.List;

public class Buyer {
    public void checkNumberOfGoods(List goodsList){
        System.out.println("符合条件的商品数量为:"+goodsList.size());
    }
}

老板

package com.ruoyi.demo.designPrinc.lod;

import java.util.ArrayList;
import java.util.List;

public class Boss {
    public void commandCheckNumber(Buyer buyer){
        //模拟老板一页一页往下翻页,采购员实时统计
        List goodsList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
           goodsList.add(new Goods());
        }
        buyer.checkNumberOfGoods(goodsList);
    }
}

测试代码

package com.ruoyi.demo.designPrinc.lod;

public class lodTest {
    public static void main(String[] args) {
        Boss boss = new Boss();
        Buyer buyer = new Buyer();
        boss.commandCheckNumber(buyer);
    }
}

根据迪米特原则,Boss只想要结果,不需要跟商品直接交流。但是采购员统计需要引用商品对象。

所以在Boss类中就出现了Goods商品类。

只需要将Boss类中的Goods类交给采购员,使Boss与Goods不再有关联。

Buyer:

package com.ruoyi.demo.designPrinc.lod;

import java.util.ArrayList;
import java.util.List;

public class Buyer {
    public void checkNumberOfGoods(){
        List goodsList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
           goodsList.add(new Goods());
        }
        System.out.println("符合条件的商品数量为:"+goodsList.size());
    }
}

Boss:

package com.ruoyi.demo.designPrinc.lod;

public class Boss {
    public void commandCheckNumber(Buyer buyer){
        buyer.checkNumberOfGoods();
    }
}

里氏替换原则示例

新建鸟类,有飞行速度属性,可以根据飞行距离和飞行速度计算飞行时间

package com.ruoyi.demo.designPrinc.lsp;

/**
 * 鸟类
 */
public class Bird {
    //飞行速度
    double flySpeed;

    public double getFlySpeed() {
        return flySpeed;
    }

    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }

    public double getFlyTime(double distance){
        return (distance/flySpeed);
    }
}

新建燕子类,继承自鸟类

package com.ruoyi.demo.designPrinc.lsp;

/**
 * 燕子类
 */
public class Swallow extends Bird{

}

新建企鹅类,继承自鸟类

package com.ruoyi.demo.designPrinc.lsp;

/**
 * 企鹅类
 */
public class Penguin extends Bird{
    public void setFlySpeed(double speed){
        flySpeed = 0;
    }
}

因为企鹅不会飞,所以设置其飞行速度为0

计算飞行时间业务类

package com.ruoyi.demo.designPrinc.lsp;

public class lspTest {
    public static void main(String[] args) {
        Bird swallow = new Swallow();
        swallow.setFlySpeed(100);

        Bird penguin = new Penguin();
        penguin.setFlySpeed(100);

        try {
            System.out.println("燕子飞行50公里耗时:"+swallow.getFlyTime(50));
            System.out.println("企鹅飞行50公里耗时:"+penguin.getFlyTime(50));
        }catch (Exception e){
            System.out.println("出错:"+e.getMessage());
        }

    }
}

上面的示例中,因为企鹅不具备飞行能力,重写了鸟类的速度方法,违背了里氏替换原则,所以当计算

企鹅的飞行时间时出现了除数不能为0的错误。

将上面的继续关系修改为,增加一个动物类,描述动物的普遍通用行为,比如计算时间和速度,鸟类和企鹅分别继承动物类,

燕子继承鸟类。这样燕子能计算飞行时间,企鹅能计算奔跑时间,同时又避免了重写父类的方法,符合里氏替换原则。

合成复用原则示例

以数据库操作添加商品为例,创建数据库连接类

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

然后创建GoodsDao

package com.ruoyi.demo.designPrinc.carp;

public class GoodsDao {
    private DBConnection dbConnection;

    public void setDbConnection(DBConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    public void addGoods(){
        String connection = dbConnection.getConnection();
        System.out.println("使用连接"+connection+"增加商品成功");
    }
}

上面的DBConnection还不是一种抽象,不便于系统扩展。目前的系统支持Mysql数据库连接。

后续业务扩展,需要支持Oracle数据库连接。

如果直接在DBConnection中增加对Oracle数据库的支持,会违背开闭原则。

可以在不修改Dao代码的前提下,而将DBConnection修改为abstract的。

package com.ruoyi.demo.designPrinc.carp;

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

然后将Mysql的逻辑抽离

package com.ruoyi.demo.designPrinc.carp;

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

再创建Oracle连接

package com.ruoyi.demo.designPrinc.carp;

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

具体选择交给应用层。

你可能感兴趣的:(架构之路,java,javascript,前端)