一起聊聊设计原则(下)

简介: 一起聊聊设计原则(下)

里氏替换原则



我认为,里氏替换原则更多是体现在了父子类继承方面,强调的是子类在继承了父类对象的时候不应该破坏这个父类对象的设计初衷。


举个例子来说:


我们定义了一个提款的服务:


/**
 * @Author linhao
 * @Date created in 11:21 上午 2021/9/4
 */
public interface DrawMoneyService {
    /**
     * 提款函数
     *
     * @param drawMoneyInputParam
     */
    void drawMoney(DrawMoneyInputParam drawMoneyInputParam);
}
复制代码


对应的是一个抽象实现父类:


/**
 * @Author linhao
 * @Date created in 11:25 上午 2021/9/4
 */
public abstract class AbstractDrawMoneyServiceImpl implements DrawMoneyService{
    /**
     * 设计初衷,需要对提现金额进行参数校验
     * 
     * @param drawMoneyInputParam
     */
    @Override
    public abstract void drawMoney(DrawMoneyInputParam drawMoneyInputParam);
}
复制代码


正常的子类继承对应父类都应该是对入参进行一个校验判断,如果金额数值小于0,自然就不允许提现了。


/**
 * @Author linhao
 * @Date created in 11:22 上午 2021/9/4
 */
public class AppDrawMoneyServiceImpl extends AbstractDrawMoneyServiceImpl{
    @Override
    public void drawMoney(DrawMoneyInputParam drawMoneyInputParam) {
        if(drawMoneyInputParam.getMoney()>0){
            //执行提款程序
        }
        System.out.println("app提款业务");
    }
}
复制代码


但是如果某个实现的子类当中违背了这一设计原则,例如下边这种:


public class GZHDrawMoneyServiceImpl implements DrawMoneyService {
    @Override
    public void drawMoney(DrawMoneyInputParam drawMoneyInputParam) {
        if(drawMoneyInputParam.getMoney()<0){
            //执行提款程序
        }
        System.out.println("公众号提款业务");
    }
}
复制代码


那么这种情况下,子类的实现就违背了最初父类设计的初衷,此时就违背了里氏替换原则的思想。此时就容易给阅读代码的人感觉,不同的子类虽然都继承了同一个父类,但是在转账的参数校验逻辑上完全是东一套,西一套,没有特定的规矩,逻辑比较乱。


所以较好的做法是在父类中就将需要满足的基本逻辑定义好,保证子类在进行扩展的时候不会轻易造成修改。


另外说说多态和里氏替换原则两个名词:


从案例代码来看,你会发现似乎 多态 和 里氏替换 长得很相似。但是我个人认为这是两个不同领域的东西,前者是代码特有的属性,后者则是一种设计思想,正因为类有了多态的这种特性,人们才会重视在代码设计过程中需要遵守里氏替换原则。这一项原则在设计的过程中保证了代码设计的正确性,它更像是一种思路在指导着开发者如何设计出更加好维护和理解的程序。


接口隔离原则



关于接口隔离原则这部分,我们可以通过一个具体的实战案例来学习。


在和第三方服务进行对接的时候,通常我们需要接入一些密钥之类的相关信息,例如和支付宝的支付接口对接,和微信支付接口做对接,和银联支付做对接等等。


那么我们可以将这些不同场景下关于支付相关的信息的储存放在一个Config相关的对象中,如下所示:


/**
 * 基本的支付配置接口
 * 
 * @Author linhao
 * @Date created in 9:59 上午 2021/9/5
 */
public interface BasePayConfig {
}
复制代码


然后对每类支付配置都有对应的一个实现方式:


public class BankPayConfig implements BasePayConfig{
    private String secretKey;
    private String appId;
    private String randomNumber;
    //getter和setter省略
}
public class AliPayConfig implements BasePayConfig{
    private String secretKey;
    private String appId;
    private String randomNumber;
}
public class WXPayConfig implements BasePayConfig{
    private String secretKey;
    private String appId;
    private String randomNumber;
}
复制代码


然后呢,实际场景中我们需要将这些配置信息给展示到一个后台管理系统的某个模块当中,所以后续我便在已有的BasePayConfig接口中定义了一个专门展示支付配置的函数:


public interface BasePayConfig {
    /**
     * 展示配置
     */
    Map<String,Object> showConfig();
}
复制代码


展示配置之后,需要在各个子类中去对不同的信息进行组装,最后返回一个Map的格式给到调用方。


但是随着业务的变动,某天需要对微信支付的配置信息实现可以替换更新的功能,但是额外的支付宝支付,银联支付不允许对外暴露这一权限。那么此时就需要对代码进行调整了。


调整思路一:


直接在BasePayConfig接口中进行扩展,代码案例如下:


public interface BasePayConfig {
    /**
     * 展示配置
     */
    Map<String,Object> showConfig(int code);
    /**
     * 更新配置信息
     * 
     * @return
     */
    Map<String,Object> updateConfig();
}
复制代码


然后各个子类依旧是实现这些接口,并且即使不需要实现更新功能的支付宝配置类,银联配置类都必须强制实现。从这样的设计角度来思考就会发现,对于代码实现方面不是太友好,接口内部定义的函数粒度还可以再分细一些。


调整思路二:


将读取配置和更新配置分成两个接口,需要实现更新配置功能的类才需要去实现该接口。代码如下所示:


支付配置展示


/**
 * @Author linhao
 * @Date created in 10:19 上午 2021/9/5
 */
public interface BasePayConfigViewer {
    /**
     * 展示配置
     */
    Map<String,Object> showConfig(int code);
}
复制代码


支付配置更新


public interface BasePayConfigUpdater {
    /**
     * 更新配置信息
     *
     * @return
     */
    Map<String,Object> updateConfig();
}
复制代码


这样的设计能够保证,不同的接口专门负责不同的领域,只有当实现类确实需要使用该功能的时候才去实现该接口。写到这里的时候,你可以不妨再回过头去理解下我在文章上半部分中提及的接口隔离原则,相信你会有新的体会。


或许你也会有所疑惑,接口隔离原则好像和单一责任原则有些类似呀,都是各自专一地负责自己所管理的部分。但是我个人认为,接口隔离原则关注的是接口,而单一责任原则关注的目标可以是对象,接口,类,所涉及的领域更加广阔一些。


依赖反转原则



在介绍依赖反转原则之前,我们先来理解一个相似的名词,控制反转。

单纯的从Java程序来进行理解:


例如我们定义个BeanObject对象:


public interface BeanObject {
    void run();
}
复制代码


然后再定义相关的实现类,如消息发送:


public class MessageNotify implements BeanObject{
    @Override
    public void run() {
        System.out.println("消息发送");
    }
}
复制代码


最后是一个Context上下文环境:


public class BeanContext {
    private static List<BeanObject> beanObjectList = new ArrayList<>();
    static {
        beanObjectList.add(new MessageNotify());
    }
    public static void main(String[] args) {
        beanObjectList.get(0).run();
    }
}
复制代码


从代码来看,可以发现对于MessageNotify的调用均是通过一个BeanContext组件调用来实现的,而并不是直接通过new MessageNotify的方式去显示调用。通过封装一个基础骨架容器BeanContext来管控每个BeanObject的run方法执行,这样就将该函数的调用权转交给了BeanContext对象管理。


控制反转


现在我们再来理解 控制反转 这个名词,“控制”主要是指对程序执行流程的控制,例如bean的调用方式。“反转”则是指程序调用权限的转变,例如从bean的调用方转变为了基础容器。


依赖注入


再来聊下依赖注入这个名词。


依赖注入强调的是将依赖属性不要通过显式的new方式来创建注入,而是将其交给了基础框架去管理。这方面的代表框架除了我们熟悉的Spring之外,其实还有很多,例如Pico Contanier等。


最后再来品味下官方对于依赖反转的介绍:


High-level modules shouldn’t depend on low-level modules. Both

modules should depend on abstractions. In addition, abstractions

shouldn’t depend on details. Details depend on abstractions.


高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。


依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。


最后,希望这篇文章能够对你有所启发。

目录
相关文章
|
设计模式 关系型数据库 数据安全/隐私保护
软件架构设计原则之单一职责原则
单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。假设我们有一个类负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能导致另一个职责的功能发生故障。这样一来,这个类就存在两个导致类变更的原因。如何解决这个问题呢?将两个职责用两个类来实现,进行解耦。后期需求变更维护互不影响。这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说,就是一个类、接口或方法只负责一项职责。
111 0
软件架构设计原则之单一职责原则
|
设计模式 人工智能 Java
软件架构设计原则之依赖倒置原则
依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。通过依赖倒置,可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并且能够降低修改程序所造成的风险。接下来看一个案例,还是以Course(课程)为例,先来创建一个类Tom:
91 0
浅谈设计原则
什么是单一职责原则,在我理解看来就是一个东西如果发生问题那么就有且仅有一个原因导致它发生问题。它的准确解释就是,就一个类而言,应该仅有一个引起它变化的原因。如果一个类承担的职责过多,就等于耦合度加大,当变化发生时,设计会受到破坏。最好的例子就是将界面和业务进行分离。做设计应该让类只有一个职责。
|
设计模式 关系型数据库
软件架构设计原则之迪米特法则
迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称为成员朋友类,而出现在方法体内部的类不属于朋友类。
104 1
|
设计模式 人工智能 前端开发
软件架构设计原则之开闭原则
开闭原则(Open-Closed Principle,OCP)是指一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。它强调的是用抽象构建框架,用实现扩展细节,可以提高软件系统的可复用性及可维护性。开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定、灵活的系统。例如版本更新,我们尽可能不修改源代码,但是可以增加新功能。
130 0
|
设计模式 关系型数据库
软件架构设计原则之接口隔离原则
接口隔离原则符合我们常说的高内聚、低耦合的设计思想,可以使类具有很好的可读性、可扩展性和可维护性。我们在设计接口的时候,要多花时间去思考,要考虑业务模型,包括对以后有可能发生变更的地方还要做一些预判。所以,对于抽象、对于业务模型的理解是非常重要的。下面我们来看一段代码,对一个动物行为进行抽象描述。
97 0
|
设计模式 关系型数据库
软件架构设计原则之里氏替换原则
里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为T1的对象o1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
77 0
|
设计模式 算法 Java
六大原则之外的设计原则|设计原则
在前面的几篇设计原则文章中,我们分别讲述了经典的六大设计原则。但是事实上,我们在开发中还有几个重要的设计原则,在这篇文章中,一并给大家讲述。
|
设计模式 关系型数据库
|
数据安全/隐私保护
软件架构设计原则--单一职责原则
> 本专栏内容参考自:咕泡学院Tom老师的《Spring5核心原理与30个类手写实战》,仅作个人学习记录使用,如有侵权,联系速删
软件架构设计原则--单一职责原则