设计模式02 - 面向对象 - 面向对象理论

        提到面向对象,我相信很多人都不陌生,随口都可以说出面向对象的四大特性:封装、抽象、继承、多态。本小结主要介绍面向对象的以下知识点

 

       1、面向对象 vs 面向过程 基本概念

       2、面向对象的四大特性

  3、接口vs抽象类的区别

       4、两个编程原则 - 基于接口而非实现编程原则、多用组合少用继承编程原则

 

一、面向对象 vs 面向过程 基本概念

1.1 面向对象编程

  面向对象编程英文缩写 OOP,全称是 Object Oriented Programming

  面向对象编程中有两个非常重要的概念,那就是类(class)和对象(object)。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。面向对象的方法主要是把事物给对象化,包括其属性和行为。

1.2 面向过程编程

  它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。

  面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程,比如C语言。

面向对象 和面向过程 的两个重要区别:

1、数据和逻辑是分开的(面向过程),还是合在一起的(面向对象)

2、面向对象符合封装、抽象、继承、多态四个特性

1.3 面向对象编程(OOP)相比面向过程编程有哪些优势

  1、OOP 更加能够应对大规模复杂程序的开发

    对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发。

  2、OOP 风格的代码更易复用、易扩展、易维护

    面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。

  3、OOP 语言更加人性化、更加高级、更加智能

  从编程语言跟机器打交道的方式的演进规律中,我们可以总结出:面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能。(越高级的语言越和人更亲和)

1.4 什么是 UML? - 用处不大,了解

  UML(Unified Model Language),统一建模语言。常用它来画图表达面向对象或设计模式的设计思路。实际上,UML 是一种非常复杂的东西。它不仅仅包含我们常提到类图,还有用例图、顺序图、活动图、状态图、组件图等。在我看来,即便仅仅使用类图,学习成本也是很高的。就单说类之间的关系,UML 就定义了很多种,比如泛化、实现、组合、依赖等。

  UML 在互联网公司的项目开发中,用处可能并不大。为了文档化软件设计或者方便讨论软件设计,大部分情况下,我们随手画个不那么规范的草图,能够达意,方便沟通就够了,而完全按照 UML 规范来将草图标准化,所付出的代价是不值得的。

        泛化(Generalization)可以简单理解为继承关系;

1 public class A { ... }
2 public class B extends A { ... }

  实现(Realization)一般是指接口和实现类之间的关系。具体到 Java 代码就是下面这样:

1 public interface A {...}
2 public class B implements A { ... }

  组合(Composition)也是一种包含关系。A 类对象包含 B 类对象,B 类对象的生命周期依赖 A 类对象的生命周期,B 类对象不可单独存在,比如鸟与翅膀之间的关系。具体到 Java 代码就是下面这样:

public class A {
  private B b;
  public A() {
    this.b = new B();
  }
}

  依赖(Dependency)是表明A和B有关系。不管是 B 类对象是 A 类对象的成员变量,还是 A 类的方法使用 B 类对象作为参数或者返回值、局部变量,只要 B 类对象和 A 类对象有任何使用关系,我们都称它们有依赖关系。具体到 Java 代码就是下面这样:

复制代码
public class A {
  private B b;
  public A(B b) {
    this.b = b;
  }
}
或者
public class A {
  private B b;
  public A() {
    this.b = new B();
  }
}
或者
public class A {
  public void func(B b) { ... }
}
复制代码

 

1.5 OOP中的面向过程还在使用(Java中的面向过程代码)

  ①滥用 getter、setter 方法

    面向对象设计风格:定义完类的属性之后,就顺手把这些属性的 getter、setter 方法都定义上。破坏了封装的特性,面向对象退化为面向过程。编码建议:

把比如对类初始化的方法封装起来,供外部调用。

编码建议: 对类的操作全部用方法封装起来,真正的封装,当然,实际上,没这么用的

  ②常量类和Utils类

    常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。静态方法一般用来操作静态变量或者外部数据。你可以联想一下我们常用的各种 Utils 类,里面的方法一般都会定义成静态方法,可以在不用创建对象的情况下,直接拿来使用。静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。我们可以适当的使用。

  但是,不能吧所有的常量放在一个类中,这样如果其他项目引用,不得不引用类,导致编译无用的静态变量,增加代码的编译时间,同时也不方便维护。

建议用,分类使用,Redis的用RedisConstants类等

  ③定义数据和方法分离的类

  数据定义在一个类中,方法定义在另一个类中。你可能会觉得,这么明显的面向过程风格的代码(面向对象讲究的是数据和方法封装在一起),谁会这么写呢?实际上,如果你是基于 MVC 三层结构做 Web 方面的后端开发,这样的代码你可能天天都在写,便于维护等。Controller 层负责暴露接口给前端调用,Service 层负责核心业务逻辑,Repository 层负责数据读写。而在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格。这种开发模式叫作基于贫血模型的开发模式。针对大型项目,便于多人维护等

二、面向对象的特性解决了那些问题

2.1 封装(Encapsulation)

  定义:封装也叫作信息隐藏或者数据访问保护。

  类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。对于封装这个特性,java语言通过比如 private、public 等关键字访问权限控制语法。private 关键字修饰的属性只能类本身访问,可以保护其不被类之外的代码直接访问,达到隐藏信息和保护数据的目的。

  意义: 保护数据只被暴露的方法修改,暴露方法要适中

  如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。

  除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。这就好比,如果一个冰箱有很多按钮,你就要研究很长时间,还不一定能操作正确。相反,如果只有几个必要的按钮,比如开、停、调节温度,你一眼就能知道该如何来操作,而且操作出错的概率也会降低很多。

2.2 抽象

  定义:如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

  java语言提供的接口类(比如 Java 中的 interface 关键字语法)或者抽象类(比如 Java 中的 abstract 关键字语法)这两种语法机制,来实现抽象这一特性。

  意义:调用者关心调用的效果,而不需要关心具体实现

  在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节。抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等

2.3 继承(Inheritance)

  定义:继承是用来表示类之间的 is-a 关系

  继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。java属于是单继承。

  意义:代码复用,继承中发展,但是继承需要适度,不要深度过深,建议多组合少继承(多实现,少继承)

  继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。

  继承:子类和父类高度耦合,修改父类的代码,会直接影响到子类。这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。关于这个问题,在后面讲到“多用组合少用继承”这种设计思想。

2.4 多态(Polymorphism)

  定义:使用父类的地方,可以使用子类替换,最终调用了子类的实现方法,实现多态有两种方式:

  方式一:继承+重写; 方式二:接口特性

  在实际的代码运行过程中,调用子类的方法实现。对于多态这种特性,纯文字解释不好理解,我们还是看一个具体的例子。

复制代码
public class Fu {
  public void add(Integer e) {
   ...
  }
}
public class Zi extends Fu {
  @Override
  public void add(Integer e) {
   。。。
  }
} 
  public static void main(String args[]) {
    Fu zi = new Zi();
   zi.add(); // 执行的是子类的方法
  }
}
复制代码

 

   对于多态特性的实现方式,除了利用“继承加方法重写”这种实现方式之外,还有就是利用接口类语法,接下来,我们先来看如何利用接口类来实现多态特性。代码如下:

我一般用在接口调用日志中

复制代码
public interface IFu {
  String print();
}
public class Zi01 implements IFu {
  public String print() { ... }
}
public class Zi02 implements IFu {
  public String print() { ... }
}
public class Demo {
  private static void print(Fu fu) {
     fu.print();
    }
  }
 public static void main(String[] args) {
    IFu zi01 = new Zi01();
    print(zi01);
  }
}
复制代码

       多态特性能提高代码的可扩展性和复用性。我们利用多态的特性,仅用一个 print() 函数就可以实现遍历打印不同类型集合的数据。如果我们不使用多态特性,我们就无法将不同的类型(子类)传递给相同的函数(print函数)。提高了代码的复用性。

  除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。关于这点,在学习后面的章节中,你慢慢会有更深的体会。

三、接口vs抽象类的区别

  本质区别: 抽象类需要被继承,而接口需要实现,java语言是单继承,多实现的机制。抽象类可以定义属性,接口不可以

3.1 抽象类abstract

  抽象类具有哪些特性。总结了下面三点。

  • 抽象类不允许被实例化,只能被继承。也就是说,你不能 new 一个抽象类的对象出来。

  • 抽象类可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现(比如 Logger 中的 doLog() 方法)。不包含代码实现的方法叫作抽象方法。jdk1.8以后,接口也可以实现方法的具体逻辑了。

  • 子类继承抽象类,必须实现抽象类中的所有抽象方法。

  问题:不用抽象类,直接用父类,继承也可以达到效果,那抽象类和普通的父类有啥区别呢?

  第一,抽象类设计,强制子类实现抽象类的抽象方法,应用场景广泛,比如:Logger日志抽象类存在abstract doLog()记录日志方法,不同的子类继承抽象类必须要实现 doLog()方法。相对于普通父类,更加具有规范性

  第二,抽象类可以使用多态,而父类要想使用多态的话,公用的方法父类必须实现一个空方法,影响可读性,没有抽象类容易读懂。

  第三,抽象类不能实例化,而父类可以实例化,影响本身设计的初衷。

3.2 接口interface

  接口也是对行为的一种抽象,接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。实际上,接口是一个比抽象类应用更加广泛。

  接口的定义:

  • 接口中没有成员变量,只有方法声明,没有方法实现(JDK1.7),

  • 实现接口的类必须实现接口中的所有方法。

  在java8之前,定义的接口不能有具体实现,这会导致在后续维护的时候如果想要在接口中新增一个方法,必须在所有实现类中都实现一遍,并且只有几个新的实现类可能要去具体实现,其他的都只是加上默认实现,这样比较麻烦。在java8中接口可以用使用关键字default(可以多个),来实现默认方法,这样就解决了上述的麻烦。

3.3 如何用抽象类还是接口

  从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。

四、两个编程原则

4.1 基于接口而非实现编程原则

  “基于接口而非实现编程” 《=》“基于抽象而非实现编程”。在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。

在编写代码的时候,要遵从“基于接口而非实现编程”的原则,具体来讲,我们需要做到下面这 3 点。

  第一、函数的命名不能暴露任何实现细节。

  比如,下载的方法, uploadToAliyun() 就不符合要求,应该改为去掉 aliyun 这样的字眼,改为更加抽象的命名方式,比如:upload()。

封装具体的实现细节。比如,跟阿里云相关的特殊上传(或下载)流程不应该暴露给调用者。我们对上传(或下载)流程进行封装,对外提供一个包裹所有上传(或下载)细节的方法,给调用者使用。(实现接口的方法中在调用具体的方法)

代码如下:

复制代码
public interface ImageStore {
  //方法不要写具体
  String upload(Image image, String bucketName);
}
//类名做到见名知意,一看就知道是阿里云的方式
public class AliyunImageStore implements ImageStore {
  //...省略属性、构造函数等...
  public String upload(Image image, String bucketName) {
    //在方法中写具体逻辑
    createBucketIfNotExisting(bucketName);
    。。。
  }
  private void createBucketIfNotExisting(String bucketName) {
    // ...创建bucket...
  }
​
}
// 上传下载流程改变:私有云不需要支持access token
public class PrivateImageStore implements ImageStore  {
  public String upload(Image image, String bucketName) {
    createBucketIfNotExisting(bucketName);
  }
  private void createBucketIfNotExisting(String bucketName) {
    // ...创建bucket...
  }
}
// ImageStore的使用举例
1 public class Test {
2  private static final String BUCKET_NAME = "ai_images_bucket";
3  public void process() {
4    Image image = ...;//处理图片,并封装为Image对象
5    ImageStore imageStore = new PrivateImageStore(...);
6    imagestore.upload(image, BUCKET_NAME);
  }
}
复制代码

 ☆思考题

  正如上边代码一样,如果需要改变上传方式,那么需要改变第五行代码,有其他解决方法吗?

答:

1、简单工厂方法 ImageStore imageStore = ImageStoreFactory.newInstance(SOTRE_TYPE_CONFIG); config文件可以写类似properties的文件,使用key-value存储。

缺点:再新增另一种存储手段时,需要修改工厂类和添加新的类。修改工厂类,违反了开放-封闭原则。

那有没有更好一点的方法呢?

2、使用反射。 在配置文件中定义需要的image store类型。 ImageStore store = (ImageStore) Class.forName(STORE_CLASS).newInstance();

缺点:使用反射,在大量创建对象时会有性能损失。

4.2、多用组合少用继承原则

  在面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。

1、为什么不推荐使用继承?

  继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。在这种情况下,我们应该尽量少用,甚至不用继承。除此之外,一些特殊的场景要求我们必须使用继承。如果你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现。比如接口日志。

2、如何判断该用组合还是继承?

  在实际的项目开发中,我们还是要根据具体的情况,来选择该用继承还是组合。如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。

posted @   云执  阅读(152)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示