写点什么

【设计模式】Java 语言不同的编程范式 - 第 1 章

  • 2022-10-20
    北京
  • 本文字数:4032 字

    阅读完需:约 1 分钟

【设计模式】Java 语言不同的编程范式-第1章

持续创作,加速成长!这是我参与「10 月更文挑战」的第 2 篇,https://xie.infoq.cn/article/56044fe8a4c2f9fe16c11f857



前言


1)设计模式(design pattern)是前辈的经验积累,是软件开发人员解决软件开发过程中的一般问题的通用方案,能够帮助开发人员提高代码的可重用性,增强系统的可维护性,快速地解决开发过程中常见的诸多难题。


2)本章节主要介绍面向对象编程的基本概念和设计模式的基本原则。


3)适用于每一位有意愿编写高质量代码的 Java 开发人员。

1、Java 简介

1995 年,一个新的编程语言发布了,它从广为人知的 C++ 语言以及鲜为人知的 Smalltalk 语言继承而来。就这样,凭借着 "Write Once,run Anywhere(一次编写,到处运行)"的经典宣言横空问世,你所需要的仅仅是 JVM(Java Virtual Machine,Java 虚拟机)。


Java 会被新兴语言取代吗?

2、Java 编程范式

什么是编程范式呢?对于不同的编程语言,我们都有一系列的概念、原则和规定。这些概念、原则和规定就被称为编程范式。从理论上来讲,我们希望编程语言只遵从一个编程范式。但是实际上,一个语言往往拥有多个编程范式。


这里我主要介绍 Java 语言的编程范式,包括命令式面向对象声明式函数式编程,以及用来描叙这些编程范式的主要概念。

2.1、命令式编程

命令式编程是这样一种编程范式:用语句更改程序的状态。


“命令”这个名称,顾名思义,指令的执行就是程序的运行。目前大多数流行的编程语言或多或少都基于命令式编程发展而来。最典型的示例就是我们所熟知的 C 语言。


命令式编程示例


为了更好地理解,我们举个通俗易懂的示例:比如你朋友要从西二旗过来找你(你在回龙观),他不知道怎么走,那么你需要给他指令,让他按照你的指令来走,类似百度地图导航。


1)先从西二旗进站


2)乘坐 13 号线东直门方向的地铁


3)回龙观地铁站出站


4)出站左拐,直行到第二个红绿灯路口


5)结束,找到你了。

2.2、面向对象编程

对象是面向对象编程(OOP)语言的主要元素,它包括状态和行为。


面向对象基于四个基本原则:


封装、抽象、继承、多态。


此处详细内容我就不做过多介绍了,这个属于我们学习 Java 的基础内容。

2.3、声明式编程

声明式编程跟我们前面提到的命令式编程的区别就是,这次我们不用告诉你的朋友如何找到你住的地儿,只需要告诉他你的住址即可,至于他过程如何实现的,我们不关心。


所以声明式编程是这样一种编程范式:它指定程序应该做什么,而不具体说明怎么做。


纯粹的声明式语言包括数据库查询语言(如 SQL 和 XPath)以及正则表达式。与命令式编程语言相比,声明式编程语言更为抽象。


通常,非命令式的编程范式都被认为是声明式类别。比如函数式编程其实就属于声明式编程范式。

2.4、函数式编程

函数式编程是声明式编程的子范式。函数式编程不会改变程序的内部状态。


在函数式编程术语中,函数类似于数学函数,函数的输出仅依赖于其参数,而不管程序的状态如何,完全不受函数是何时执行的影响。大多数函数式语言都是基于 lambda 演算而来,这是由数学家 Alonzo Church 于 20 世纪 30 年代创建的一种形式化数学逻辑系统。

3、流以及集合的使用

1)命令式编程如下示例:创建数字集合,过滤奇数,打印结果。


/** * 命令式编程  * 1.实例化10个整数的集合,从1到10 * 2.过滤掉奇数 * 3.最后把结果打印出来 */List<Integer> list = new ArrayList<Integer>();for (int i = 0; i < 10; i++) {    list.add(i);}//创建另一个集合,过滤掉奇数List<Integer> odds = new ArrayList<Integer>();for (Integer integer : list) {    if (integer % 2 == 0)        odds.add(integer);}    //打印结果for (Integer integer : odds) {    System.out.println(integer);}
复制代码


2)从 Java 8 开始,我们可以使用流在一行代码中执行相同的操作:


/** * 使用流在一行代码中执行相同的操作 */IntStream.range(0, 10).filter(i -> i % 2 ==0).forEachOrdered(System.out::print);
复制代码


这里注意流在 java.util.stream 包中定义,用于管理可以对其执行功能式操作的对象流,流是集合的功能对应物。后续我会详细分享关于流式编程的相关内容。

4、设计模式和原则

1)创建软件应用程序是为了满足不断变化和发展的需求。一个成功的应用程序还应该提供一种简单的方法来扩展它以满足不断变化的期望。如果在设计和开发软件时应用一组面向对象的设计原则和模式,则可以避免或解决这些常见问题。


2)面向对象的设计原则也被称为 SOLID 。在设计和开发软件时可以应用这些原则,以便创建易于维护和开发的程序。


SOLID 原则包括,单一职责原则开闭原则里氏替换原则接口隔离原则依赖倒置原则

4.1、单一职责原则

该原则指出软件模块应该只有一个被更改的理由。多数情况下,编写 Java 代码时都会将单一职责原则应用于类。


通俗点儿就是,专业的人做专业的事。每个类只负责做一件事,修改当前类不会对其他类有所影响。


示例:使用数据库持久化保存对象。假设对 Car 类添加方法处理 CRUD 操作。


这种封装方法将导致 Car 逻辑和数据库持久化操作耦合性太高,将来要修改数据持久操作必须更改 Car 类代码,这可能会在 Car 逻辑中产生错误。


解决方式是创建两个类:一个用于封装 Car 逻辑,另一个用于负债持久化。如下图所示:


4.2、开闭原则

这个原则就是:“模块、类和函数应该对扩展开放,对修改关闭。”


开闭原则在 Java 中最典型的案例就是接口,接口我们都知道,它是对外开放,对内封闭。


开闭原则是最重要的设计原则之一,是大多数设计模式的基础。


所以当我们完成一个模块功能后需要修改或增加新功能时,最好保持原有模块不变,然后在此基础上通过继承和多态扩展来添加新功能。

4.3、里氏替换原则

Barbara Liskov 指出,派生类型必须完全可替代其基类型。里氏替换原则(LSP)与子类型多态密切相关。


里氏替换原则声明,在设计模块和类时,必须确保派生类型从行为的角度来看是可替代的。


里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。


例如:燕子 和 鸵鸟 都属于鸟类,但是鸵鸟不具备飞的功能。


这里列举一个违反此原则的示例:


我们定义一个汽车 Car 类,并添加两个方法,用钥匙 Key 锁门(lock)和 开锁(unlock)


但是有一种车它是没有车门的,巴吉赛车(BUGGY ,一种没有车门的沙漠赛车,后轮驱动)。


我们创建一个继承自 Car 类的 Buggy 类,这时,我们调用 lock 锁门方法时,发现无法锁定车门,因为没有车门。


所以我们在设计软件要用于车时,无论它们是否是巴吉赛车,都应该考虑到汽车锁门和开锁的一系列问题。我们设计的程序一定要能扩展到其他类型的车上。

4.4、接口隔离原则

客户端不应该依赖于它所不需要的接口。


通俗点儿就是:别人不需要的东西不要强加给人家


比如:汽车 ICar 接口提供了 修理汽车 repair()方法和 销售汽车 sell()方法。修理厂 Mechanic 类实现了 ICar 接口,此时,Mechanic 类需要实现 汽车 ICar 接口中所有方法,但是 修理厂不需要 销售汽车 sell() 方法。


这里我们应该都知道有两种解决方式


1)把修理厂 Mechanic 类定义为 Abstract 抽象类


2)把 ICar 接口拆分为两个接口,一个修理汽车的 IRepairable 接口(此接口只有 repair()方法),一个销售汽车 ISellable 的接口(此接口只有 sell()方法)。这样 Mechanic 类只需实现 IRepairable 接口即可。


这里就是用这个示例说明,修理厂不需要汽车厂接口的销售汽车方法,但是汽车接口提供的方法超出了 Mechanic 类所需要的。接口隔离原则是为了约束接口,降低类对接口的依赖。


接口隔离原则的优点

1)灵活性,可维护性增强

2)高内聚,低耦合

3)减少代码冗余(减少了方法的实现)

4)体现对象层次

4.5、依赖倒置原则

1)高级模块不应该依赖于低级模块,两者都应该依赖抽象。

2)抽象不应该依赖于细节,细节应该依赖于抽象。

3)依赖倒置的中心思想是面向接口编程。

4)依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类。

5)使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

4.5.1、案例分享

完成 Person 接收消息的功能,接收消息者可以通过微信,短信等等方式接收。引入一个抽象的接口 IReceiver, 表示接收者。这样 Person 类与接口 IReceiver 发生依赖因为 Email, WeiXin 等等属于接收的范围,他们各自实现 IReceiver 接口就 ok, 这样我们就符号依赖倒转原则。​


示例代码


  • 定义接口 IReceiver:


public interface IReceiver {    public String getInfo();  }
复制代码


  • 定义两个不同的实现类 Email 和 WeiXin :


public class Email implements IReceiver {  public String getInfo() {    return "电子邮件信息: hello,world";  }}
复制代码


public class WeiXin implements IReceiver {  public String getInfo() {    return "微信信息: hello,ok";  }}
复制代码


  • 定义人 Person 类,这里我们依赖接口 IReceiver,这样我们就可以实现解耦,提高代码的可扩展性,之后我们想用其他方式接收消息,只需要传入具体的实现类即可。


public class Person {  /**   * 入参传入接口   * @param receiver   */  public void receive(IReceiver receiver ) {    System.out.println(receiver.getInfo());  }  }
复制代码


  • 客户端调用我们的接收消息方法。


public class DependecyInversion {
public static void main(String[] args) { //客户端无需改变,只需要定义不同的实现类即可 Person person = new Person(); person.receive(new Email()); person.receive(new WeiXin()); } }
复制代码

4.5.2、分析

  • 高层模块 Person 没有依赖底层模块 Email 和 WeiXin,而是依赖抽象(IReciver)

  • 细节(Email、Weixin)依赖抽象(IReciver)

4.5.3、依赖传递的三种方式

  • 接口传递

  • 构造器传递

  • setter 方法传递

发布于: 刚刚阅读数: 3
用户头像

人生苦短,拒绝内卷。一心向阳,向阳而生。 2021-08-17 加入

知名互联网公司高级开发工程师,专注于后端Java开发的全栈工程师,一名技术狂热追求者,一直在奔跑从未停止过。 人生苦短,拒绝内卷。我是跟着飞哥学编程,一个一心向阳,向阳而生,努力向上生长的年轻人。

评论

发布
暂无评论
【设计模式】Java 语言不同的编程范式-第1章_设计模式_跟着飞哥学编程_InfoQ写作社区