面向对象的设计原则你不要了解一下么?

简介: 之前我们看了单一职责原则和开闭原则,今天我们再来看里式替换原则和依赖倒置原则,千万别小看这些设计原则,他在设计模式中会有很多体现,所以理解好设计原则之后,那么设计模式,也会让你更加的好理解一点。

前言

在面向对象的软件设计中,只有尽量降低各个模块之间的耦合度,才能提高代码的复用率,系统的可维护性、可扩展性才能提高。面向对象的软件设计中,有23种经典的设计模式,是一套前人代码设计经验的总结,如果把设计模式比作武功招式,那么设计原则就好比是内功心法。常用的设计原则有七个,下文将具体介绍。

设计原则简介

  • 单一职责原则:专注降低类的复杂度,实现类要职责单一;
  • 开放关闭原则:所有面向对象原则的核心,设计要对扩展开发,对修改关闭;
  • 里式替换原则:实现开放关闭原则的重要方式之一,设计不要破坏继承关系;
  • 依赖倒置原则:系统抽象化的具体实现,要求面向接口编程,是面向对象设计的主要实现机制之一;
  • 接口隔离原则:要求接口的方法尽量少,接口尽量细化;
  • 迪米特法则:降低系统的耦合度,使一个模块的修改尽量少的影响其他模块,扩展会相对容易;
  • 组合复用原则:在软件设计中,尽量使用组合/聚合而不是继承达到代码复用的目的。

这些设计原则并不说我们一定要遵循他们来进行设计,而是根据我们的实际情况去怎么去选择使用他们,来让我们的程序做的更加的完善。

里式替换原则

定义

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

换句话来说,一个软件实体如果使用一个基类的话,那么一定适用于其子类,而且它根本不会察觉出基类对象和子类对象的区别。

比如说,假设有两个类,一个是Base类,另一个是Derived类,并且Derived类是Base的子类,那么一个方法如果可以接受一个基类对象b的话:method(Base b) ,那么它必然可以接受一个子类对象d,可以有 method1(d)

里式替换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不会受到影响的时候,基类才能真正被复用,而衍生类也才能够在基类的基础上增加新的行为。

我们通过一个例子来理解一下:

《西游记》中,美猴王下地府桥段,个位应该有印象把,到达阎王殿之后,拿到生死簿,把生死簿上所有的包括自己,还有其他的猕猴,所有的猴子猴算都给划了,这也是导致之后真假美猴王桥段的前序。

画个图理解

46.jpg

很显然,地府管理一切生灵的生死的方法都是通过类来进行区分的,比如孙悟空就是石猴,之后出现的那个六耳猕猴就是猕猴,但是他们都是属于同一个类,猴类,就像下图中。

47.jpg


因此,孙悟空把猴类中有姓名的都从生死簿勾掉之后,显然是因为勾魂小鬼们并不区分石猴类与猕猴类,就像下图:

48.jpg

换句话来说,只要是猴类适用的,猕猴和石猴都适用,这其实就是里式替换原则。

这是第一种解释,还有第二个更加通俗易懂的解释:所有引用基类的地方必须能透明地使用其子类的对象。

第二种定义比较通俗,容易理解:只要有父类出现的地方,都可以用子类来替代,而且不会出现任何错误和异常。但是反过来则不行,有子类出现的地方,不能用其父类替代。

实例代码

public class TestA {
    public void fun(int a,int b){
        System.out.println(a+"+"+b+"="+(a+b));
    }
    public static void main(String[] args) {
        System.out.println("父类的运行结果");
        TestA a=new TestA();
        a.fun(1,2);
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.println("子类替代父类后的运行结果");
        TestB b=new TestB();
        b.fun(1,2);
    }
}
class TestB extends TestA{
    @Override
    public void fun(int a, int b) {
        System.out.println(a+"-"+b+"="+(a-b));
    }
}

大家肯定也都能猜出来结果是什么样子的

父类的运行结果
1+2=3
子类替代父类后的运行结果
1-2=-1
Process finished with exit code 0

我们想要的结果是“1+2=3”。可以看到,方法重写后结果就不是了我们想要的结果了,也就是这个程序中子类B不能替代父类A。这违反了里氏替换原则原则,从而给程序造成了错误。

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

这个很容易理解,子类继承了父类,拥有了父类和方法,同时还可以定义自己有,而父类没有的方法。这是在继承父类方法的基础上进行功能的扩展,符合里氏替换原则。

public class TestA {
    public void fun(int a,int b){
        System.out.println(a+"+"+b+"="+(a+b));
    }
    public static void main(String[] args) {
        System.out.println("父类的运行结果");
        TestA a=new TestA();
        a.fun(1,2);
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.println("子类替代父类后的运行结果");
        TestB b=new TestB();
        b.fun(1,2);
        b.newFun();
    }
}
class TestB extends TestA{
    public void newFun(){
        System.out.println("这是子类的新方法...");
    }
}

这次运行出来的代码结果就是我们意料中的内容了

父类的运行结果
1+2=3
子类替代父类后的运行结果
1+2=3
这是子类的新方法...
Process finished with exit code 0

AVA语言对里式替换原则支持的局限

JAVA编译器的检查是有局限性的,为什么呢?举个例子来说,描述一个物体大小的量有精度和准确度两种属性。所谓的精度,就是这个量的有效数字有多少位;而所谓的精准度,是这个量与真实的物体大小相符合到什么程度。

一个量可以有很高的精度,但是却无法与真实物体的情况相吻合,JAVA语言编译器能够检查的,仅仅是相当于精度的属性而已,它没有办法去检查这个量与真实物体的差距。

换一句话来说,JAVA编译器不能检查一个系统在实现和商业逻辑上是否满足里式替换原则。

而里式替换原则在设计模式中也有体现,请关注我们的知识星球,链接在文末,我们将每周更新一篇关于设计模式的文章。

依赖倒置原则

如果说实现开闭原则的关键事抽象化,是面向对象设计的目标的话,依赖倒置原则就是这个面向对象设计的主要机制。

定义

抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

为什么要实现倒置?这也是我们看这个定义的时候产生的一些问题,那么我们就来说说。

简单的来说,传统的过程性系统的设计办法倾向于使高层次的模块依赖于低层次的模块,抽象层依赖于具体层次,倒置原则是要把这个错误的依赖关系倒转过来,这就是依赖倒置原则的由来。也是为什么要进行依赖倒置。

依赖倒置原则的实现方法

依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:

  • 每个类尽量提供接口或抽象类,或者两者都具备。
  • 变量的声明类型尽量是接口或者是抽象类。
  • 任何类都不应该从具体类派生。
  • 使用继承时尽量遵循里氏替换原则。


下面我们通过一些代码实例(商品售卖)来进行理解:

class BeijingShop implements Shop{
        public String sell(){
            return "北京商店售卖:北京烤鸭,稻香村月饼";
        }
    }
    class ShanDongShop implements  Shop{
        @Override
        public String sell() {
            return "山东商店售卖:德州扒鸡,烟台苹果";
        }
    }
    //如果说顾客去购买商品
class Customer{
    public void shopping(ShanDongShop shop){
        //购物
        System.out.println(shop.sell());
    }
//这是在山东商店购买,如果说是在北京商店购买就会这样
class Customer{
    public void shopping(BeijingShop shop) {
        //购物
        System.out.println(shop.sell());
    }

这也是这种设计的存在缺陷,顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。解决方式我们可以定义一个共同的接口Shop,就可以这样了。

public class TestSale {
    public static void main(String[] args) {
        Customer c = new Customer();
        System.out.println("---顾客购买商品如下---");
        c.shopping(new ShanDongShop());
        c.shopping(new BeijingShop());
    }
}
interface Shop{
    //售卖方法
    public String sell();
}
class BeijingShop implements Shop{
    public String sell(){
        return "北京商店售卖:北京烤鸭,稻香村月饼";
    }
}
class ShanDongShop implements  Shop{
    @Override
    public String sell() {
        return "山东商店售卖:德州扒鸡,烟台苹果";
    }
}
class Customer{
    public void shopping(Shop shop) {
        System.out.println(shop.sell());//购物
    }
}

程序运行结果

---顾客购买商品如下---
山东商店售卖:德州扒鸡,烟台苹果
北京商店售卖:北京烤鸭,稻香村月饼
Process finished with exit code 0

这样,不管顾客类 Customer 访问什么商店,或者增加新的商店,都不需要修改原有代码了。

依赖倒置原则是OO设计的核心原则,设计模式的研究和应用是以依赖导致原则为知道原则的,在知识星球中的设计模式中我们将会一一给大家体现。

我是懿,一个正在被打击还在努力前进的码农。欢迎大家关注我们的公众号,加入我们的知识星球,我们在知识星球中等着你的加入。

相关文章
|
6月前
|
SQL 人工智能 数据处理
《AI赋能SQL Server,数据处理“狂飙”之路》
在数据爆炸的时代,SQL Server作为主流关系型数据库管理系统面临复杂查询与海量数据的挑战。引入人工智能(AI)为优化查询性能提供了全新路径。AI能精准洞察查询瓶颈,优化执行计划;通过预测性维护提前预防性能隐患;智能管理索引以提升查询效率;并基于持续学习实现动态优化。这些优势不仅提高数据处理效率、降低运营成本,还助力企业在数字化竞争中抢占先机,推动SQL Server与AI深度融合,为企业可持续发展注入新动能。
160 4
|
11月前
|
Serverless
MATLAB中的矩阵与向量运算
【10月更文挑战第2天】本文全面介绍了MATLAB中的矩阵与向量运算,包括基本操作、加减乘除、转置、逆矩阵、行列式及各种矩阵分解方法。通过丰富的代码示例,展示了如何利用矩阵运算解决线性方程组、最小二乘法拟合、动态系统模拟和电路分析等问题。掌握这些运算不仅提升编程效率,还能在工程计算和科学研究中发挥重要作用。
|
安全 网络安全 网络虚拟化
配置通过管理VLAN实现远程管理设备示例
用户可以通过管理网口或管理VLAN的VLANIF接口远程登录到设备进行管理。设备上没有空闲的管理网口,需要采用管理VLAN实现。同时,为了用户安全登录,采用STelnet登录方式。配置思路如下: 1. 在Switch上配置管理VLAN,并将接口加入VLAN。 2. 在Switch上配置VLANIF并指定IP地址。 3. 在Switch上开启STelnet服务功能,并配置SSH用户。 4. 用户PC以STelnet方式登录Switch。
|
程序员 C#
C#财务管理系统(C#课程设计)
C#财务管理系统(C#课程设计)
208 0
【Leetcode 2583】二叉树中的第K大层和 —— 优先队列 + BFS
解题思路: - 使用队列保存节点,按层序依次保存该层节点 - 使用优先队列保存每层节点值的总和,最后剔除前k个大数即可得到
|
缓存 NoSQL Redis
【后端面经】【缓存】36|Redis 单线程:为什么 Redis 用单线程而 Memcached 用多线程?-- Redis多线程
【5月更文挑战第21天】Redis启用多线程后,主线程负责接收事件和命令执行,IO线程处理读写数据。请求处理流程中,主线程接收客户端请求,IO线程读取并解析命令,主线程执行后写回响应。业界普遍认为,除非必要,否则不建议启用多线程模式,因单线程性能已能满足多数需求。公司实际场景中,启用多线程使QPS提升约50%,或选择使用Redis Cluster以提升性能和可用性。
120 0
EMQ
|
运维 Kubernetes 监控
在阿里云 ACK 上部署 EMQX MQTT 服务器集群
本文章将以EMQX企业版为例,详细讲解如何使用EMQX Kubernetes Operator在阿里云ACK公有云平台上创建部署MQTT服务集群,并实现自动化管理与监控。
EMQ
826 0
在阿里云 ACK 上部署 EMQX MQTT 服务器集群
数据结构-栈和队列
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
数据结构-栈和队列
|
消息中间件 存储 Java
这一篇让你彻底搞懂 JAVA 内部类
这一篇让你彻底搞懂 JAVA 内部类
175 0