由“为什么VO不能继承PO?” 引出的为什么组合优于继承?

2024-09-02 20:20

本文主要是介绍由“为什么VO不能继承PO?” 引出的为什么组合优于继承?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

简述VO、DTO、PO的概念。

如下概念是我个人的理解:

  • VO(View Object)视图对象,用于展示,这很好理解,就是前端页面所需数据封装,一般所需要的属性比 PO 多并且。
  • DTO(Data Transfer Object)数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载。(个人理解:原先需要获取用户信息和用户订单需要调用俩个接口,将返回数据整合为一个 DTO,调用一次就可以获取所有数据)
  • PO实体类,一般与持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。

对于这些概念,网上众说纷纭。没要必要纠结具体的定义,不要纠结DTO与VO的区别是什么。在实际编码过程中,按照每个人自己的规范来做就好了。在实际的编码当中,Service层处理数据的时候,我们使用DTO来进行数据的传输,然后再包装成 VO 返回页面所需数据就可以了

关于 VO 层的一次设计

在工作中编码过程中的感受, 在写 VO 和 DTO 的时候,发现总是在重复写代码,不断的拷贝对象字段。

这其实是一种组合方式的 VO 设计,在这个例子中,我们创建了 GoodsVO 类,通过组合实体类和定制属性,实现了前端展示所需的信息。

组合方式的VO设计

// 实体类
public class Goods {private Long id;private String name;private BigDecimal price;private String description;// 其他属性和方法...
}// VO类通过组合实现
public class GoodsVO {private Long id;private String name;private BigDecimal price;// 构造方法或工厂方法,将实体类转换为VO类public GoodsVO(Goods goods) {this.id = goods.getId();this.name = goods.getName();this.price = goods.getPrice();}// Getter 和 Setter 方法...
}

通过组合,我们实现了 VO 类对实体类的定制化展示,同时保留了灵活性,使得 VO 类的设计不受实体类的限制。但会重复编写大量的 setget 代码,对象之间频繁复制。

继承方式的VO设计

VO 类继承 PO 实体类,转而调用实体类的部分属性,可以达到复用相同属性的效果。

如果你决定使用继承,下面是一个简单的示例。在这个例子中,GoodsVO 类继承自 Goods 类,通过继承,GoodsVO 类拥有了 Goods 类的所有属性和方法。

// VO类通过继承实现
public class GoodsVO extends Goods {// 新增或覆盖需要展示的属性private String displayInfo;// 构造方法或工厂方法,将实体类转换为VO类public GoodsVO(Goods goods) {// 调用父类构造方法,复制基本属性super.setId(goods.getId());super.setName(goods.getName());super.setPrice(goods.getPrice());super.setDescription(goods.getDescription());// 初始化VO类特有的属性this.displayInfo = "Additional information for display";}// Getter 和 Setter 方法...
}

通过继承,我们可以在 GoodsVO 类中新增或覆盖需要展示的属性,实现对特定场景的定制化。但需要注意的是,继承关系通常带来类之间的紧密耦合,可能会限制类的扩展性和灵活性。

这种方式生成的文档很不清晰,每个接口都有一堆没返回给前端的字段;业务中的额外字段会污染VOPOJO 承担的责任太重了,

为什么组合优先于继承

我在个人项目中喜欢直接使用各种 model 来继承 po,单纯的因为可以直接省去写各种重复 gettersetter 的步骤,并且不需要使用各种拷贝工具。但是,搞vodopo,还有其他各种 o,是为了解藕它们之间的联系,而继承却是建立它们之间的耦合关系,确实是会限制类的扩展性和灵活性。

比如:当表结构做了增减,do 变了 vo 也变了,你的接口返回项也会变化,线上的项目容易翻车(虽然我觉得表结构不会那么轻易变化),不利于节流,会返回很多不必要的字段给前端,容易被不怀好意的人推测出数据库的字段。

面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。同样地,在《阿里巴巴Java开发手册》中有一条规定:谨慎使用继承的方式进行扩展,优先使用组合的方式实现

在这里插入图片描述
在编程中,继承组合是用于在面向对象语言中设计和构建类和对象的两种基本技术。

继承,它允许一个类(称为派生类或子类)从另一个类(称为基类或超类)继承属性和行为。换句话说,子类“是”超类的一种类型。它建立了一种“是”关系。例如,如果我们有一个类 Animal 和一个类 Dog ,则 Dog 类继承自 Animal ,因为狗是一种动物。

组合,涉及使用其他对象作为组件来构建对象。类不是继承属性和行为,而是使用其他类的实例来实现其功能。它建立了“有”关系。例如,Car 类可以具有 Engine 类和 Wheel 类的组合。

有一张图可以很形象的表示他们两者之间的关系

在这里插入图片描述

为什么不推荐使用继承

降低类之间的耦合度: 在继承关系中,子类与父类之间存在紧密的耦合关系,子类对父类的任何修改都可能产生影响。通过组合,类之间的关系更为松散,一个类的改变通常不会影响到其他类,除非它们共享相同的成员变量。

假设我们要设计一个关于鸟的类。我们将“鸟”这样一个抽象的事物概念,定义为一个抽象类AbstractBird 。所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。我们知道,大部分鸟都会飞,那我们可不可以在 AbstractBird抽象类中,定义一个 fly() 方法呢?

答案是否定的。尽管大部分鸟都会飞,但也有特例,比如鸵鸟就不会飞。鸵鸟继承具有 fly() 方法的父类,那鸵鸟就具有“飞”这样的行为,这显然不对。如果在鸵鸟这个子类中重写 fly() 方法,让它抛出UnSupportedMethodException异常呢?具体的代码实现如下所示:


public class AbstractBird {//...省略其他属性和方法...public void fly() { //... }
}public class Ostrich extends AbstractBird { //鸵鸟//...省略其他属性和方法...public void fly() {throw new UnSupportedMethodException("I can't fly.'");}
}

这种写法虽然可以解决问题,但不优雅。因为除了法师之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,全部都去重写 fly() 方法,抛出异常,完全属于代码重复。理论上这些不会飞的鸟根本就不应该拥有 fly() 方法,让不会飞的鸟暴露 fly() 接口给外部,增加了被误用的概率。

要解决上面的问题,就得让 AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird ,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird ,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类。具体的继承关系如下图所示:
在这里插入图片描述
这样一来,继承关系变成了三层。但是如果我们不只关注“鸟会不会飞”,还要继续关注“鸟会不会叫”,将鸟划分得更加细致时呢?两个关注行为自由搭配起来会产生四种情况:会飞会叫、不会飞会叫、会飞不会叫、不会飞不会叫。如果继续沿用刚才的设计思路,继承层次会再次加深。
在这里插入图片描述
如果继续增加“鸟会不会下蛋”这样的行为,类的继承层次会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。

继承最大的问题就在于:继承层次过深、继承关系过于复杂时会影响到代码的可读性和可维护性。

组合相比继承有哪些优势

复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。作为一门面向对象开发的语言,代码复用是 Java 引人注意的功能之一。Java代码的复用有继承、组合以及委托三种具体的实现形式。

对于上面提到的继承带来的问题,可以利用**组合(composition)、接口、委托(delegation)**三个技术手段一块儿来解决。

接口表示具有某种行为特性。针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、 EggLayable 接口。我们将这个设计思路翻译成 Java 代码的话,就是下面这个样子:


public interface Flyable {void fly();
}
public interface Tweetable {void tweet();
}
public interface EggLayable {void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鸵鸟//... 省略其他属性和方法...@Overridepublic void tweet() { //... }@Overridepublic void layEgg() { //... }
}
public class Sparrow implements Flayable, Tweetable, EggLayable {//麻雀//... 省略其他属性和方法...@Overridepublic void fly() { //... }@Overridepublic void tweet() { //... }@Overridepublic void layEgg() { //... }
}

不过,接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑几乎是一样的(可能极少场景下会不一样),这就会导致代码重复的问题。那这个问题又该如何解决呢?有以下两种方法。

1.使用委托

针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托技术来消除代码重复。


public interface Flyable {void fly()}
public class FlyAbility implements Flyable {@Overridepublic void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbilitypublic class Ostrich implements Tweetable, EggLayable {//鸵鸟private TweetAbility tweetAbility = new TweetAbility(); //组合private EggLayAbility eggLayAbility = new EggLayAbility(); //组合//... 省略其他属性和方法...@Overridepublic void tweet() {tweetAbility.tweet(); // 委托}@Overridepublic void layEgg() {eggLayAbility.layEgg(); // 委托}
}

2.使用Java8的接口默认方法

Java8 中,我们可以在接口中写默认实现方法。使用关键字 default 定义默认接口实现,当然这个默认的方法也可以重写。

public interface Flyable {default void fly() {//默认实现... }
}public interface Flyable {default void fly() {//默认实现... }
}public interface Tweetable {default void tweet() {//默认实现... }
}public interface EggLayable {default void layEgg() {//默认实现... }
}public class Ostrich implements Tweetable, EggLayable {//鸵鸟//... 省略其他属性和方法...
}
public class Sparrow implements Flayable, Tweetable, EggLayable {//麻雀//... 省略其他属性和方法...
}

继承主要有三个作用:表示is-a关系、支持多态特性、代码复用。而这三个作用都可以通过其他技术手段来达成。比如is-a关系,我们可以通过组合和接口的has-a关系来替代;多态特性我们也可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。

参考文章:
https://blog.csdn.net/fuzhongmin05/article/details/108646872

https://blog.csdn.net/Mr_YanMingXin/article/details/139540085

这篇关于由“为什么VO不能继承PO?” 引出的为什么组合优于继承?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1130970

相关文章

java中VO PO DTO POJO BO DO对象的应用场景及使用方式

《java中VOPODTOPOJOBODO对象的应用场景及使用方式》文章介绍了Java开发中常用的几种对象类型及其应用场景,包括VO、PO、DTO、POJO、BO和DO等,并通过示例说明了它... 目录Java中VO PO DTO POJO BO DO对象的应用VO (View Object) - 视图对象

hdu4869(逆元+求组合数)

//输入n,m,n表示翻牌的次数,m表示牌的数目,求经过n次操作后共有几种状态#include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<queue>#include<set>#include<map>#include<stdio.h>#include<stdlib.h>#includ

JavaSE——封装、继承和多态

1. 封装 1.1 概念      面向对象程序三大特性:封装、继承、多态 。而类和对象阶段,主要研究的就是封装特性。何为封装呢?简单来说就是套壳屏蔽细节 。     比如:对于电脑这样一个复杂的设备,提供给用户的就只是:开关机、通过键盘输入,显示器, USB 插孔等,让用户来和计算机进行交互,完成日常事务。但实际上:电脑真正工作的却是CPU 、显卡、内存等一些硬件元件。

解决Office Word不能切换中文输入

我们在使用WORD的时可能会经常碰到WORD中无法输入中文的情况。因为,虽然我们安装了搜狗输入法,但是到我们在WORD中使用搜狗的输入法的切换中英文的按键的时候会发现根本没有效果,无法将输入法切换成中文的。下面我就介绍一下如何在WORD中把搜狗输入法切换到中文。

【经验交流】修复系统事件查看器启动不能时出现的4201错误

方法1,取得『%SystemRoot%\LogFiles』文件夹和『%SystemRoot%\System32\wbem』文件夹的权限(包括这两个文件夹的所有子文件夹的权限),简单点说,就是使你当前的帐户拥有这两个文件夹以及它们的子文件夹的绝对控制权限。这是最简单的方法,不少老外说,这样一弄,倒是解决了问题。不过对我的系统,没用; 方法2,以不带网络的安全模式启动,运行命令行,输入“ne

Go组合

摘要 golang并非完全面向对象的程序语言,为了实现面向对象的继承这一神奇的功能,golang允许struct间使用匿名引入的方式实现对象属性方法的组合 组合使用注意项 使用匿名引入的方式来组合其他struct 默认优先调用外层方法 可以指定匿名struct以调用内层方法 代码 package mainimport ("fmt")type People struct{}type Pe

组合c(m,n)的计算方法

问题:求解组合数C(n,m),即从n个相同物品中取出m个的方案数,由于结果可能非常大,对结果模10007即可。       共四种方案。ps:注意使用限制。 方案1: 暴力求解,C(n,m)=n*(n-1)*...*(n-m+1)/m!,n<=15 ; int Combination(int n, int m) { const int M = 10007; int

代码随想录训练营day37|52. 携带研究材料,518.零钱兑换II,377. 组合总和 Ⅳ,70. 爬楼梯

52. 携带研究材料 这是一个完全背包问题,就是每个物品可以无限放。 在一维滚动数组的时候规定了遍历顺序是要从后往前的,就是因为不能多次放物体。 所以这里能多次放物体只需要把遍历顺序改改就好了 # include<iostream># include<vector>using namespace std;int main(){int n,m;cin>>n>>m;std::vector<i

为什么构造函数不能为虚函数

1,从存储空间角度     虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。 2,从使用角度         虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调

七、Maven继承和聚合关系、及Maven的仓库及查找顺序

1.继承   2.聚合   3.Maven的仓库及查找顺序