首页 > 其他分享 >DDD(一)

DDD(一)

时间:2024-07-23 17:22:53浏览次数:10  
标签:set 聚合 get void 实体 public DDD

DDD领域驱动模型

领域划分

核心域:解决项目的核心问题,和组织业务紧密相关。

支撑域:解决项目的非核心问题,具有组织特性,但不具有通用性。

通用域:解决通用问题,没有组织特性。

领域模型

事务脚本

界限上下文

实 体

  1. ”标识符“用来唯一定位一个对象,在数据库中我们一般用表的主键来实现“标识符”。主键和标识符的思考
    角度不同。
  2. 实体:拥有唯一的标识符,标识符的值不会改变,而对象的其他状态则会经历各种变化。标识符用来跟踪对象状态变化,一个实体的对象无论怎样变化,我们都能通过标识符定位这个对象。
  3. 实体一般的表现形式就是EF Core中的实体类。

值对象(Value Object)

  1. 值对象:没有标识符的对象,也有多个属性,依附于某个实体对象而存在。比如“商家”的地理位置、衣服的RGB颜色。
  2. 定义为值对象和普通属性的区别:体现整体关系。

聚合(Aggregate)

  1. 目的:高内聚,低耦合。有关系的实体紧密协作,而关系很弱的实体被隔离。
  2. 把关系紧密的实体放到一个聚合中,每个聚合中有一个实体作为聚合根(Aggregate Root),所有对于聚合内对象的访问都通过聚合根来进行,外部对象只能持有对聚合根的引用。
  3. 聚合根不仅仅是实体,还是所在聚合的管理者。
  4. 聚合的判断标准:实体是否是整体和部分的关系,是否存在相同的生命周期。

聚合延伸到服务

  1. 聚合中的实体中没有业务逻辑代码,只有对象的创建、对象的初始化、状态管理等个体相关的代码。
  2. 对于聚合内的业务逻辑,我们编写领域服务(Domain Service),而对于跨聚合协作以及聚合与外部系统协作的逻辑,我们编写应用服(ApplicationService) .
  3. 应用服务协调多个领域服务、外部系统来完成一个用例。

实体的逻辑代码

管理实体的创建,状态管理等非业务逻辑。

领域服务

聚合内的业务逻辑。

应用服务

聚合间的业务逻辑,和外部系统的业务逻辑。

仓储

按照要求从数据库中读取数据以及把领域服务修改的数据保存回数据库。

工作单元

工作单元内的代码要么全部成功执行,要么全部执行失败。

领域事件

继承事件

开闭原则

对扩展开放,对修改关闭。

领域事件

在同一个微服务内的聚合之间的事件传递。使进程内的通讯机制完成。

集成事件

跨微服务的事件传递。使用事件总线(EventBus)实现。

充血模型与贫血模型

贫血模型

一个类中只有属性或者成员变量,没有方法

充血模型

一个类中既有属性、呈椭圆变量、也有方法。

EF Core对实体属性的操作

EF Core在读写实体对象的属性时,会查找属性对应的成员变量,如果能找到,EF Core会直接读写这个成员变量的值,而不是通过set和get代码块来读写。

充血模型实现的要求

  1. 属性是只读的或者是只能被类内部的代码修改。
  2. 定义有参数的构造方法。
  3. 有的成员变量没有对应属性,但是这些成员变量需要映射为数据表中的列,也就是我们需要把私有成员变量映射到数据表中的列。
  4. 有的属性是只读的,也就是它的值是从数据库中读取出来的,但是我们不能修改属性值。
  5. 有的属性不需要映射到数据列,仅在运行时被使用。

实体在EFCore中的实现

internal class User
{
    public int id {get;init}
    public DateTime CreateDateTime{get;init }//初始化属性
    public string UserName {get;private set;}
    public int Credits{get;set;}
    public string? passwordHash;//只在类中使用
    public string? remark {get;}
    public string? Remark{
        get
        {
            return this.remark;
        }
    }
    public string Tag {get;set;}
    
    //无参的构造方法
    //给EFCOre从数据库中加载数据然后生成User对象返回用的
    private User()
    {
        
    }
    
    //构造方法
    public User(string UName)
    {
        this.UserName=UName;
        this.CreatDateTime=DateTime.Now;
        this.Credits=10;
    }
    public void ChangeUserName(string un)
    {
        //起到数据校验的作用
        if(un.Length>5)
        {
            Console.WriteLine(".......");
            return;
		}
        this.UserName=UName;
    }
    public void ChangePassword(string pwd)
    {
        this.passwordHash=pwd;
    }
}
internal class UserConfig:IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.Property("passwordHash");
        builder.Property(e=>e.Remark).HasField("remark");//让一个属性只从数据库中读出来 
        builder.Ignore(e=>e.Tag);//忽略属性
        
    }
}
internal class MyDbContext:DbContext
{
	public DbSet<User> Users{get;set;}
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		base.OnConfiguring(optionsBuilder);
		optionsBuilder.UseSqlServer
			("Server=.;Database=dddl;Trusted_Connection=True;");
	}
	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		base.OnModelCreating(modelBuilder);
		modeBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
	}
}

EFCore中实现值对象

实体和值对象的区别是是否包含标识符

值对象是属于实体的一部分,并且不拥有标识符

internal class Entity
{
    public int Id {get;set;}
    public string Name {get;set;}
    public CurrencyName Currency{get;set;}
    
}
enum CurrencyName
{
    CNY,USD,NZD
}
internal class EntityConfig:IEntityTypeConfiguration<Entity>
{
    public void Configurs(EntityTypeBuilder<Entity> builder)
    {
        //将写入数据库的枚举值从int转换为string
        builder.Property(e=>e.Currency).HasConversion<string>();
    }
}

从属实体类型的值对象的配置方法

(这里有个大坑,EF 7 下,值对象不能在DbContext里有DbSet以及对应实现了的IEntityTypeConfiguration配置类)

(EF7.0 location类需要加上Owned特性,不然会报没主键的错)

 internal class Geo
 {
     public double Latitude { get; set; }
     public double Longitude { get; set; }

     public Geo(double latitude, double longitude)
     {
         Latitude = latitude;
         Longitude = longitude;
     }
 }
internal class Shop
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Geo Location { get; set; }
}
 internal class ShopConfig : IEntityTypeConfiguration<Shop>
 {
     public void Configure(EntityTypeBuilder<Shop> builder)
     {
         //将Location属性Geo设置为从属于shop
         builder.OwnsOne(c => c.Location);
     }
 }

聚合在.net中的实现

把关系强的实体,放到同一个聚合中,把其中一个实体作为“聚合根”,对于同一个聚合内的其他实体,都通过聚合根来调用。

工作单元(UnitOfWork)

我们在上下文中只为聚合根实体生命DbSet类型的属性。对非聚合根实体,值对象的操作都通过根实体进行。

跨表查询

所有跨聚合的数据查询都应该是通过领域服务的协作来完成的,而不应该是在数据库表之间进行join查询。会有性能损失,需要做权衡。

意思是不是各自聚合自己取各自内部的数据,不要跨表查询

对于统计。汇总等报表类的应用,则不需要遵循聚合的约束,可以通过执行原生SQL等方式进行跨表的查询。

领域事件的实现方式

中介者模式

使用进程内消息传递的开源库MediatR

mediatR支持 一个发布者对应一个处理者 和 一个发布者对应多个处理者 两种模式

MediatR用法

  • 创建一个ASP.NET Core项目,NuGet安装MediatR.Extensions.Microsoft.DependencyInjection(已弃用)
  • 我调用了MediatR
  • Program.cs中调用AddMediatR()
  • 定义一个在消息的发布者和处理者之间进行数据传递的类,这个类需要
    实现lNotification接口。一般用record类型。
  • 消息的处理者要继承NotificationHandler接口,其中的泛型
    参数TNotification代表此消息处理者要处理的消息类型。
  • 在需要发布消息的的类中注入IMediator类型的服务,然后我们调用
    Publish方法来发布消息。Send()方法是用来发布一对一消息的,而Publish()方
    法是用来发布一对多消息的。
  builder.Services.AddMediatR(cfg=>cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
  //接收消息放,等待信号
  public class PostNotifHandler1 : NotificationHandler<PostNotification>
  {
      protected override void Handle(PostNotification notification)
      {
          Console.WriteLine("11111"+notification.Body);
      }
  }
//接收消息放,等待信号
public class PostNotifHandler1 : NotificationHandler<PostNotification>
{
    protected override void Handle(PostNotification notification)
    {
        Console.WriteLine("11111"+notification.Body);
    }
}
  [HttpGet(Name = "GetWeatherForecast")]
  public IEnumerable<WeatherForecast> Get()
  {
      //在此处,等待触发,相当于消息的发送者
      Mediator.Publish(new PostNotification("Hello" + DateTime.Now));

      return Enumerable.Range(1, 5).Select(index => new WeatherForecast
      {
          Date = DateTime.Now.AddDays(index),
          TemperatureC = Random.Shared.Next(-20, 55),
          Summary = Summaries[Random.Shared.Next(Summaries.Length)]
      })
      .ToArray();
  }
知识补充

Singler

SignalR:当所连接的客户端变得可用时服务器代码可以立即向其推送内容,而不是让服务器等待客户端请求新的数据。实现实时服务器与客户端通信。是一个开源.NET 库生成需要实时用户交互或实时数据更新的 web 应用程序。

SignalR的出现,让页面通过javascript可以很简单的调用后端服务的方法,而在后端也可以很简单的直接调用javascript所实现的方法,前后端可以进行实时通信。实现了服务器主动推送(Push)消息到客户端页面,这样客户端就不必重新发送请求或使用轮询技术来获取消息。

注意:SignalR 会自动管理连接。客户端和服务器之间的连接是持久性的,不像传统的 HTTP 连接。

EFCore中发布领域事件的时机

 public abstract class BaseEntity : IDomainEvent
 {
     [NotMapped]//使EF Core忽略此属性
     private IList<INotification> events = new List<INotification>();
     public void AdddomainEvent(INotification notif)
     {
         events.Add(notif);
     }

     public void ClearDomainEvents()
     {
         events.Clear();
     }

     public IEnumerable<INotification> GetDomainEvents()
     {
         return events;
     }
 }
 public interface IDomainEvent
 {
     IEnumerable<INotification> GetDomainEvents();
     void AdddomainEvent(INotification notif);
     void ClearDomainEvents();

 }
 public class NewUserHandlercs : NotificationHandler<NewUserNotification>
 {
     protected override void Handle(NewUserNotification notification)
     {
         
     }
 }
 public class User : BaseEntity
 {
     public int Id { get; init; }
     public DateTime CreatDateTime { get; init; }
     public string UserName { get; private set; }
     public int Credits { get; set; }

     public User()
     {
     }
     public User(string UN)
     {
         UserName = UN;
         AdddomainEvent(new NewUserNotification(UN, this.CreatDateTime));//事件
     }
     public void ChangeUserName()
     {

     }
 }
 internal class MyDbContext : DbContext
 {
     private readonly IMediator? mediator;

     public MyDbContext(IMediator? mediator)
     {
         this.mediator = mediator;
     }

     public DbSet<User> Users { get; set; }
     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {
         base.OnConfiguring(optionsBuilder);
         optionsBuilder.UseSqlServer
         ("Server=.;Database=EFCoreDemo;Trusted_Connection=True;");
     }
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         base.OnModelCreating(modelBuilder);
         modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
     }
     // 知道可以这样用就行了, 具体到自己的项目中,根据实际情况处理
     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
     {
         //获取所有含有未发布事件的实体对象
         var domainentities = this.ChangeTracker.Entries<IDomainEvent>().Where(e => e.Entity.GetDomainEvents().Any());
         //获取所有待发布消息
         var domainEvents=domainentities.SelectMany(e => e.Entity.GetDomainEvents()).ToList();
         domainentities.ToList().ForEach(e => e.Entity.ClearDomainEvents());
         foreach(var e in domainentities)
         {
             await mediator.Publish(e);
         }
         
         return await base.SaveChangesAsync(cancellationToken);
         //可以加一个TransactionScope,在savechanges()之后进行发送事件,这样发送事件失败savaechanges也会回滚
     }
 }

集成事件的发布

微服务之间的通信

RabbitMQ的基本概念

1、集成事件是服务器间的通信,所以必须借助于第三方服务器作为事件总
线。常用的消息中间件有Redis、RabbitMQ、Kafka、ActiveMQ等。

2、RabbitMQ的基本概念:

1)信道(Channel):信道是消息的生产者、消费者和服务器进行通信的虚
拟连接。TCP连接的建立是非常消耗资源的,所以RabbitMQ在TCP连接的基础
上构建了虚拟的信道。我们尽量重复使用TCP连接,而信道则是可以用完了
就关闭。

2)队列(Queue):用来进行消息收发的地方,生产者把消息放到队列中,
消费者从队列中获取数据。

3)交换机(exchange):把消息路由到一个或者多个队列中。

RabbitMQ的routing模式

生产者把消息发布到交换机中,消息携带一个routingKey属性,交换机会根据routingKey的值
把消息发送到一个或者多个队列;消费者会从队列中获取消息;交换机和队列都位于RabbitMQ服务器内部。优点:即使消费者不在线,消费者相关的消息也会被保存到队列中,当消费者上线之后,消费者就可以获取到离线期间错过的消息。

标签:set,聚合,get,void,实体,public,DDD
From: https://www.cnblogs.com/guan-tou6/p/18319019

相关文章

  • 领域驱动设计(DDD)的概述与应用
    个人名片......
  • DDD | 05-什么是仓储层
    四、什么是仓储层?在DDD中,仓储(Repository)是一种设计模式,它充当了领域层与数据存储层之间的桥梁。仓储的主要职责是提供一种抽象机制,使得领域对象(尤其是聚合根)可以被透明地持久化和检索,而无需暴露底层的数据访问技术细节给领域层。这样设计的目的是为了保持领域模型的纯净性,让业务......
  • DDD | 03-什么是实体对象
    二、什么是实体?实体(Entity)是一种核心的领域模型组件,用于表示具有唯一标识符、生命周期和行为的对象。实体是领域中关键概念的具体实例,它们通常对应于现实世界中的事物,比如用户、订单、账户等。主要特点唯一标识符(Identity):每个实体都有一个唯一的标识符,这个标识符是用来区......
  • DDD | 04-什么是聚合根
    三、什么是聚合根?聚合根(AggregateRoot)是DDD中的一个核心概念,用于组织和管理一组相关的领域对象,确保它们的整体一致性和完整性。聚合根是领域模型中的关键组件,它不仅封装了领域内的复杂业务逻辑,还提供了控制访问和维护数据一致性的机制,是构建可维护、可扩展的软件系统的重要基石......
  • DDD | 02-值对象拓展示例
    示例拓展金额和货币创建一个表示金额和货币的值对象(AmountVO),在系统中统一处理货币相关的数据,确保精度和一致性。importjava.math.BigDecimal;importjava.util.Currency;/***这个AmountVO类使用BigDecimal来精确存储金额值,避免了浮点运算可能带来的精度问题。同时,利用C......
  • 【DDD实战】ABP vNext框架
    ABPvNext配置1.模块化配置(AbpModule)可支持API、UI扩展可随意整合和拆分定制化需求——options模块整合下面是三种配置依赖模块的方式,分别是引用式、插件式、nuget1.引用式模块需引用dll配置步骤:模块依赖——DependsOn(typeof(___Module))需避免循环依赖配......
  • 拯救中国足球,要不尝试一下DDD事件风暴?
    DDD领域驱动设计批评文集做强化自测题获得“软件方法建模师”称号《软件方法》各章合集张逸老师写了新文章《领域建模的常见问题及解决方案》,我来谈一谈对这篇文章的感想。(1)文章一开始,张逸老师大大地赞扬了事件风暴:图1摘自《领域建模的常见问题及解决方案》张逸老师......
  • python 识别图片验证码/滑块验证码准确率极高的 ddddocr 库
    前言验证码的种类有很多,它是常用的一种反爬手段,包括:图片验证码,滑块验证码,等一些常见的验证码场景。识别验证码的python库有很多,用起来也并不简单,这里推荐一个简单实用的识别验证码的库ddddocr(带带弟弟ocr)库.环境准备python版本要求小于等于python3.9版本pip安装pipin......
  • BP插件暴破验证码实战流程(BP+captcha-killer-modified+ddddocr)
    含有速成版本+工具介绍及问题=保姆级版一、验证码破解流程:BP插件暴破实战流程如下:1、下载安装插件captcha-killer2、启动本地验证码识别服务ddddocr --codereg.py3、抓验证码的包,发送到插件4、配置识别服务模板5、抓登录的包,payload选插件,单线程本次使用到工具如下......
  • 如何使用 Services.AddDistributedMemoryCache
    参考资料:https://www.cnblogs.com/RainFate/p/16920591.html AI生成:在.NETCore中,Services.AddDistributedMemoryCache()方法用于注册分布式内存缓存。这是一个内存中的缓存解决方案,适用于需要在多个服务器或服务之间共享缓存数据的分布式系统。如何使用AddDistributedMemory......