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

日期: 2025-03-16 10:11:09 |浏览: 3|编号: 80477

友情提醒:信息内容由网友发布,本站并不对内容真实性负责,请自鉴内容真实性。

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

简述VO、DTO、PO的概念。

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

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

关于 VO 层的一次设计

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

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

组合方式的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 类的设计不受实体类的限制。但会重复编写大量的 set 和 get 代码,对象之间频繁复制。

继承方式的VO设计

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

如果你决定使用继承,下面是一个简单的示例。在这个例子中, 类继承自 Goods 类,通过继承, 类拥有了 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 方法...
}

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

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

为什么组合优先于继承

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

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

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

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

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

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

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

为什么不推荐使用继承

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

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

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


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

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

要解决上面的问题,就得让 类派生出两个更加细分的抽象类:会飞的鸟类 和不会飞的鸟类 d ,让麻雀、乌鸦这些会飞的鸟都继承 ,让鸵鸟、企鹅这些不会飞的鸟,都继承 d 类。具体的继承关系如下图所示:

这样一来,继承关系变成了三层。但是如果我们不只关注“鸟会不会飞”,还要继续关注“鸟会不会叫”,将鸟划分得更加细致时呢?两个关注行为自由搭配起来会产生四种情况:会飞会叫、不会飞会叫、会飞不会叫、不会飞不会叫。如果继续沿用刚才的设计思路,继承层次会再次加深。

如果继续增加“鸟会不会下蛋”这样的行为,类的继承层次会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。

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

组合相比继承有哪些优势

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

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

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


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

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

1.使用委托

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


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

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

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

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关系来替代;多态特性我们也可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。

参考文章:

提醒:请联系我时一定说明是从旅游网上看到的!