DTO、存储库和数据映射器在DDD中的作用 | Khalil Stemmler

栏目: 后端 · 发布时间: 5年前

内容简介:在领域驱动设计中,对于在对象建模系统的开发中需要发生的每一件事情都有一个正确的工具。负责处理验证逻辑的是什么?值对象。你在哪里处理领域逻辑?尽可能使用实体,否则领域服务。

在领域驱动设计中,对于在对象建模系统的开发中需要发生的每一件事情都有一个正确的工具。

负责处理验证逻辑的是什么?值对象。

你在哪里处理领域逻辑?尽可能使用实体,否则领域服务。

也许学习DDD最困难的方面之一就是能够确定特定任务所需的工具。

在DDD中,存储库,数据映射器和DTO是 实体生命周期关键部分 ,使我们能够存储,重建和删除域实体。这种类型的逻辑称为“ 数据访问逻辑 ”。

对于 使用MVC 构建 REST-ful CRUD API 而不太关注封装ORM数据访问逻辑的开发人员,您将学习:

  • 当我们不封装ORM数据访问逻辑时发生的问题
  • 如何使用DTO来稳定API
  • 存储库如何充当复杂ORM查询的外观
  • 创建存储库和方法的方法
  • 数据映射器如何用于与DTO,域实体和ORM模型进行转换

我们如何在MVC应用程序中使用ORM模型?

我们看看一个MVC控制器的代码:

<b>class</b> UserController <b>extends</b> BaseController {
  async exec (req, res) => {
    <b>try</b> {
      <b>const</b> { User } = models;
      <b>const</b> { username, email, password } = req.body;
      <b>const</b> user = await User.create({ username, email, password });
      <b>return</b> res.status(201);
    } <b>catch</b> (err) {
      <b>return</b> res.status(500);
    }
  }
}

我们同意这种方法的好处是:

  • 这段代码非常容易阅读
  • 在小型项目中,这种方法可以很容易地快速提高工作效率

但是,随着我们的应用程序不断发展并变得越来越复杂,这种方法会带来一些缺点,可能会引入错误。

主要原因是因为缺乏关注点分离。这段代码负责太多事情:

  • 处理API请求( 控制器 责任)
  • 对域对象执行验证(此处不存在,但 域实体值对象 责任)
  • 将域实体持久化到数据库(存储库责任)

当我们为项目添加越来越多的代码时,我们注意为我们的类分配 单一的责任 变得非常重要。

场景:在3个单独的API调用中返回相同的视图模型

这是一个示例,其中缺乏对从ORM检索数据的封装可能导致引入错误。

假设我们正在开发我们的 乙烯基交易应用程序 ,我们的任务是创建3个不同的API调用。

GET /vinyl?recent=6         - GET the 6 newest listed vinyl
GET /vinly/:vinylId/        - GET a particular vinyl by it's id
GET /vinyl/owner/:userId/   - GET all vinyl owned by a particular user

在每个API调用中,我们都需要返回Vinyl视图模型,所以让我们做第一个控制器:返回最新的乙烯基。

export <b>class</b> GetRecentVinylController <b>extends</b> BaseController {
  <b>private</b> models: any;

  <b>public</b> constructor (models: any) {
    <b>super</b>();
    <b>this</b>.models = models;
  }

  <b>public</b> async executeImpl(): Promise<any> {
    <b>try</b> {
      <b>const</b> { Vinyl, Track, Genre } = <b>this</b>.models;
      <b>const</b> count: number = <b>this</b>.req.query.count;

      <b>const</b> result = await Vinyl.findAll({
        where: {},
        include: [
          { owner: User, as: 'Owner', attributes: ['user_id', 'display_name'] },
          { model: Genre, as: 'Genres' },
          { model: Track, as: 'TrackList' },
        ],
        limit: count ? count : 12,
        order: [
          ['created_at', 'DESC']
        ],
      })

      <b>return</b> <b>this</b>.ok(<b>this</b>.res, result);
    } <b>catch</b> (err) {
      <b>return</b> <b>this</b>.fail(err);
    }
  }
}

如果您熟悉Sequelize,这可能是您的标准。

再实现一个根据Id获得实体的控制器:

export <b>class</b> GetVinylById <b>extends</b> BaseController {
  <b>private</b> models: any;

  <b>public</b> constructor (models: any) {
    <b>super</b>();
    <b>this</b>.models = models;
  }

  <b>public</b> async executeImpl(): Promise<any> {
    <b>try</b> {
      <b>const</b> { Vinyl, Track, Genre } = <b>this</b>.models;
      <b>const</b> vinylId: string = <b>this</b>.req.params.vinylId;

      <b>const</b> result = await Vinyl.findOne({
        where: {},
        include: [
          { model: User, as: 'Owner', attributes: ['user_id', 'display_name'] },
          { model: Genre, as: 'Genres' },
          { model: Track, as: 'TrackList' },
        ]
      })

      <b>return</b> <b>this</b>.ok(<b>this</b>.res, result);
    } <b>catch</b> (err) {
      <b>return</b> <b>this</b>.fail(err);
    }
  }
}

这两个类之间没有太大的不同,呃?

所以这绝对不遵循DRY原则,因为我们在这里重复了很多。

并且您可以预期第三个API调用将与此类似。

到目前为止,我们注意到的主要问题是:代码重复;另一个问题是......缺乏数据一致性!

请注意我们如何直接传回ORM查询结果?

<b>return</b> <b>this</b>.ok(<b>this</b>.res, result);

这就是为了响应API调用而返回给客户端的内容。

那么,当我们 在数据库上执行迁移并添加新列时会发生什么?更糟糕的是 - 当我们删除列或更改列的名称时会发生什么?

我们刚刚破坏了依赖它的每个客户端的API。

嗯......我们需要一个工具。

让我们进入我们的企业 工具 箱,看看我们发现了什么......

啊,DTO(数据传输对象)。

数据传输对象

数据传输对象是在两个独立系统之间传输数据的对象的(奇特)术语。

当我们关注Web开发时,我们认为DTO是View Models,因为它们是虚假模型。它们不是真正的域模型,但它们包含视图需要了解的尽可能多的数据。

例如,Vinyl视图模型/ DTO可以构建为如下所示:

type Genre = 'Post-punk' | 'Trip-hop' | 'Rock' | 'Rap' | 'Electronic' | 'Pop';

<b>interface</b> TrackDTO {
  number: number;
  name: string;
  length: string;
}

type TrackCollectionDTO = TrackDTO[];

<font><i>// Vinyl view model / DTO, this is the format of the response</i></font><font>
<b>interface</b> VinylDTO {
  albumName: string;
  label: string;
  country: string;
  yearReleased: number;
  genres: Genre[];
  artistName: string;
  trackList: TrackCollectionDTO;
}
</font>

之所以如此强大是因为我们只是标准化了我们的API响应结构。

我们的DTO是一份数据合同。我们告诉任何使用此API的人,“嘿,这将是您始终期望从此API调用中看到的格式”。

这就是我的意思。让我们看一下如何在通过id检索Vinyl的例子中使用它。

export <b>class</b> GetVinylById <b>extends</b> BaseController {
  <b>private</b> models: any;

  <b>public</b> constructor (models: any) {
    <b>super</b>();
    <b>this</b>.models = models;
  }

  <b>public</b> async executeImpl(): Promise<any> {
    <b>try</b> {
      <b>const</b> { Vinyl, Track, Genre, Label } = <b>this</b>.models;
      <b>const</b> vinylId: string = <b>this</b>.req.params.vinylId;

      <b>const</b> result = await Vinyl.findOne({
        where: {},
        include: [
          { model: User, as: 'Owner', attributes: ['user_id', 'display_name'] },
          { model: Label, as: 'Label' },
          { model: Genre, as: 'Genres' }
          { model: Track, as: 'TrackList' },
        ]
      });

      <font><i>// Map the ORM object to our DTO</i></font><font>
      <b>const</b> dto: VinylDTO = {
        albumName: result.album_name,
        label: result.Label.name,
        country: result.Label.country,
        yearReleased: <b>new</b> Date(result.release_date).getFullYear(),
        genres: result.Genres.map((g) => g.name),
        artistName: result.artist_name,
        trackList: result.TrackList.map((t) => ({
          number: t.album_track_number,
          name: t.track_name,
          length: t.track_length,
        }))
      }

      </font><font><i>// Using our baseController, we can specify the return type</i></font><font>
      </font><font><i>// for readability.</i></font><font>
      <b>return</b> <b>this</b>.ok<VinylDTO>(<b>this</b>.res, dto)
    } <b>catch</b> (err) {
      <b>return</b> <b>this</b>.fail(err);
    }
  }
}
</font>

那很好,但现在让我们考虑一下这类的责任。

这是一个controller,但它负责:

  • 定义如何坚持ORM模型映射到VinylDTO,TrackDTO和Genres。
  • 定义需要从Sequelize ORM调用中检索多少数据才能成功创建DTO。

这比controllers应该做的要多得多。

我们来看看Repositories和Data Mappers。

我们将从存储库开始。

存储库Repositories

存储库是持久性技术的外观(例如ORM),Repository是 实体生命周期关键部分, 它使我们能够存储,重建和删除域实体。Facade是一种 设计模式 术语,指的是为更大的代码体提供简化界面的对象。在我们的例子中,更大的代码体是域实体持久性和 域实体检索逻辑。

存储库在DDD和清洁架构中的作用:

在DDD和清洁体系结构中,存储库是 基础架构层 关注的问题。

一般来说,我们说repos 持久化并检索域实体。

1. 保存持久化

  • 跨越 交叉点 和关系表的脚手架复杂持久性逻辑。
  • 回滚失败的事务
  • 单击save(),检查实体是否已存在,然后执行创建或更新。

关于“创建如果不存在,否则更新”,这就是我们不希望我们域中的任何其他构造必须知道的复杂数据访问逻辑的类型:只有repos应该关心它。

2.检索

检索创建域实体所需的全部数据,我们已经看到了这一点,选择了include: []Sequelize的内容,以便创建DTO和域对象。将实体重建的责任委托给一个映射器。

编写存储库的方法

在应用程序中创建存储库有几种不同的方法。

1. 通用存储库接口

您可以创建一个通用存储库接口,定义您必须对模型执行的各种常见操作getById(id: string),save(t: T)或者delete(t: T)。

<b>interface</b> Repo<T> {
  exists(t: T): Promise<<b>boolean</b>>;
  delete(t: T): Promise<any>;
  getById(id: string): Promise<T>;
  save(t: T): Promise<any>;
}

从某种意义上说,这是一种很好的方法,我们已经定义了创建存储库的通用方法,但我们最终可能会看到数据访问层的细节泄漏到调用代码中。

原因是因为getById感觉就像感冒一样。如果我正在处理一个VinylRepo,我宁愿任务,getVinylById因为它对域的泛在语言更具描述性。如果我想要特定用户拥有的所有乙烯基,我会使用getVinylOwnedByUserId。

喜欢的方法getById是相当 YAGNI

这导致我们成为创建存储库的首选方式。

2.按实体/数据库表的存储库

我喜欢能够快速添加对我正在工作的域有意义的方便方法,所以我通常会从一个苗条的基础存储库开始:

<b>interface</b> Repo<T> {
  exists(t: T): Promise<<b>boolean</b>>;
  delete(t: T): Promise<any>;
  save(t: T): Promise<any>;
}

然后使用其他更多关于域的方法扩展它。

export <b>interface</b> IVinylRepo <b>extends</b> Repo<Vinyl> {
  getVinylById(vinylId: string): Promise<Vinyl>;
  findAllVinylByArtistName(artistName: string): Promise<VinylCollection>;
  getVinylOwnedByUserId(userId: string): Promise<VinylCollection>;
}

为什么总是将存储库定义为接口是有益的,因为它遵循 Liskov Subsitution Principle (可以使结构被替换),并且它使结构成为 依赖注入

让我们继续创建我们的IVinylRepo:

<b>import</b> { Op } from 'sequelize'
<b>import</b> { IVinylRepo } from './IVinylRepo';
<b>import</b> { VinylMap } from './VinyMap';

<b>class</b> VinylRepo implements IVinylRepo {
  <b>private</b> models: any;

  constructor (models: any) {
    <b>this</b>.models = models;
  }

  <b>private</b> createQueryObject (): any {
    <b>const</b> { Vinyl, Track, Genre, Label } = <b>this</b>.models;
    <b>return</b> { 
      where: {},
      include: [
        { model: User, as: 'Owner', attributes: ['user_id', 'display_name'], where: {} },
        { model: Label, as: 'Label' },
        { model: Genre, as: 'Genres' },
        { model: Track, as: 'TrackList' },
      ]
    }
  }

  <b>public</b> async exists (vinyl: Vinyl): Promise<<b>boolean</b>> {
    <b>const</b> VinylModel = <b>this</b>.models.Vinyl;
    <b>const</b> result = await VinylModel.findOne({ 
      where: { vinyl_id: vinyl.id.toString() }
    });
    <b>return</b> !!result === <b>true</b>;
  }

  <b>public</b> delete (vinyl: Vinyl): Promise<any> {
    <b>const</b> VinylModel = <b>this</b>.models.Vinyl;
    <b>return</b> VinylModel.destroy({ 
      where: { vinyl_id: vinyl.id.toString() }
    })
  }

  <b>public</b> async save(vinyl: Vinyl): Promise<any> {
    <b>const</b> VinylModel = <b>this</b>.models.Vinyl;
    <b>const</b> exists = await <b>this</b>.exists(vinyl.id.toString());
    <b>const</b> rawVinylData = VinylMap.toPersistence(vinyl);

    <b>if</b> (exists) {
      <b>const</b> sequelizeVinyl = await VinylModel.findOne({ 
        where: { vinyl_id: vinyl.id.toString() }
      });

      <b>try</b> {
        await sequelizeVinyl.update(rawVinylData);
        <font><i>// scaffold all of the other related tables (VinylGenres, Tracks, etc)</i></font><font>
        </font><font><i>// ...</i></font><font>
      } <b>catch</b> (err) {
        </font><font><i>// If it fails, we need to roll everything back this.delete(vinyl);</i></font><font>
      }
    } <b>else</b>  {
      await VinylModel.create(rawVinylData);
    }

    <b>return</b> vinyl;
  }

  <b>public</b> getVinylById(vinylId: string): Promise<Vinyl> {
    <b>const</b> VinylModel = <b>this</b>.models.Vinyl;
    <b>const</b> queryObject = <b>this</b>.createQueryObject();
    queryObject.where = { vinyl_id: vinyl.id.toString() };
    <b>const</b> vinyl = await VinylModel.findOne(queryObject);
    <b>if</b> (!!vinyl === false) <b>return</b> <b>null</b>;
    <b>return</b> VinylMap.toDomain(vinyl);
  }

  <b>public</b> findAllVinylByArtistName (artistName: string): Promise<VinylCollection> {
    <b>const</b> VinylModel = <b>this</b>.models.Vinyl;
    <b>const</b> queryObject = <b>this</b>.createQueryObject();
    queryObjectp.where = { [Op.like]: `%${artistName}%` };
    <b>const</b> vinylCollection = await VinylModel.findAll(queryObject);
    <b>return</b> vinylCollection.map((vinyl) => VinylMap.toDomain(vinyl));
  }

  <b>public</b> getVinylOwnedByUserId(userId: string): Promise<VinylCollection> {
    <b>const</b> VinylModel = <b>this</b>.models.Vinyl;
    <b>const</b> queryObject = <b>this</b>.createQueryObject();
    queryObject.include[0].where = { user_id: userId };
    <b>const</b> vinylCollection = await VinylModel.findAll(queryObject);
    <b>return</b> vinylCollection.map((vinyl) => VinylMap.toDomain(vinyl));
  }
}
</font>

看到我们封装了我们的sequelize数据访问逻辑?我们已经不再需要重复编写includes,因为现在所有必需的 include语句都在这里。

我们也提到过VinylMap。让我们快速看一下数据映射器Mapper的责任。

数据映射器

Mapper的职责是进行所有转换:

  • 从Domain到DTO
  • 从域到持久性
  • 从持久性到域

这是我们的VinylMap样子:

<b>class</b> VinylMap <b>extends</b> Mapper<Vinyl> {
  <b>public</b> toDomain (raw: any): Vinyl {
    <b>const</b> vinylOrError = Vinyl.create({

    }, <b>new</b> UniqueEntityID(raw.vinyl_id));
    <b>return</b> vinylOrError.isSuccess ? vinylOrError.getValue() : <b>null</b>;
  }

  <b>public</b> toPersistence (vinyl: Vinyl): any {
    <b>return</b> {
      album_name: vinyl.albumName.value,
      artist_name: vinyl.artistName.value
    }
  }

  <b>public</b> toDTO (vinyl: Vinyl): VinylDTO {
    <b>return</b> {
      albumName: vinyl.albumName,
      label: vinyl.Label.name.value,
      country: vinyl.Label.country.value
      yearReleased: vinyl.yearReleased.value,
      genres: result.Genres.map((g) => g.name),
      artistName: result.artist_name,
      trackList: vinyl.TrackList.map((t) => TrackMap.toDTO(t))
    }
  }
}

好的,现在让我们回过头来使用我们的VinylRepo和重构我们的控制器VinylMap。

export <b>class</b> GetVinylById <b>extends</b> BaseController {
  <b>private</b> vinylRepo: IVinylRepo;

  <b>public</b> constructor (vinylRepo: IVinylRepo) {
    <b>super</b>();
    <b>this</b>.vinylRepo = vinylRepo;
  }

  <b>public</b> async executeImpl(): Promise<any> {
    <b>try</b> {
      <b>const</b> { VinylRepo } = <b>this</b>;
      <b>const</b> vinylId: string = <b>this</b>.req.params.vinylId;
      <b>const</b> vinyl: Vinyl = await VinylRepo.getVinylById(vinylId);
      <b>const</b> dto: VinylDTO = VinylMap.toDTO(vinyl);
      <b>return</b> <b>this</b>.ok<VinylDTO>(<b>this</b>.res, dto)
    } <b>catch</b> (err) {
      <b>return</b> <b>this</b>.fail(err);
    }
  }
}

源码:


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

互联网+秋叶课堂:我的网课创业

互联网+秋叶课堂:我的网课创业

秋叶 / 北京:机械工业出版社 / 2016-3-1 / 39.0

在线教育被很多人视为 “互联网+”创业热潮中的下一个风口,越来越多的老师和创业者选择在线教育创业。本书作者秋叶老师2013年双11正式带一帮小伙伴开始在线教育创业,在短短两年内“从0到1”,累计做到了超500万元的销售业绩,成为国内Office领域在线教育运营最成功的团队之一。在这本书中秋叶老师结合自己的实战经历,向各位完整剖析了两年创业的真实复盘思考,是一本值得在线教育创业同行入手的必读书。 ......一起来看看 《互联网+秋叶课堂:我的网课创业》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具