0%

[toc]

从类库到服务#

1 服务发现#

1.1 服务发现的意义#

以前是DNS以及DNS之后的负载均衡承担了服务地址翻译的一部分能力
但是随着微服务的流向 服务的非正常掉线、重启、上下线越来越频繁。
zk曾经活跃过,但是过于底层,需要用户自己做很多额外工作。
因此专用于服务发现的 eureka出现并被纳入spring cloud。
后来就是Consul和nacos继承了eureka的衣钵。

如何在基础设置和网络协议层面,对应用尽可能无感知、方便地实现服务发现是目前服务发现的一个发展方向

1.2 服务发现组件中CA和AP的抉择#

服务注册中心非常关键, 一旦崩溃将不可使用,因此必须最大程度保证可用。例如搞多个注册中心节点提供服务,不断复制各自的信息,随时提供服务。

但是复制信息又要即时响应的过程会造成结果的不一致,缺乏一致性。

  • Eureka优先保证可用性,牺牲一致性, 选择异步复制来交换服务注册信息,同时并不会强行等待复制成功,有新服务注册对应节点可立刻宣布该服务。
    适合节点关系相对固定, 服务一般不会频繁上下线的系统。

  • Consul是优先保证一致性,牺牲可用性。使用Raft算法,要求多数节点写入成功后服务的注册才算完成,严格保证了一致性。同时采用gossip协议,支持多个数据中心逐渐的服务同步。

选择哪个方案,一定程度上是基于产品现实而做的决策。当系统C因为网络问题,变成了A\B两个分区后,是否对你的服务有重大影响?是否是有状态的系统? 如果是,那么选择对一致性严格要求的Consul,如果不是,则选择Eureka。

1.3 注册中心的实现#

在分布式KV存储框架上单独做的服务发现#

典型代表: zk、 etcd。
etcd采用Raft算法,
zk采用的zab算法是MultiPaxos的派生算法。
共同特点是在整体较高复杂度的架构和算法的外部,维持着极为简单的应用接口,只有少量CRUD和watch的api, 所以要实现完整的服务发现,要做很大量的工作,只有自研大厂才会这么做,小厂不会首选耗费大量人力去从零实现。

基础设施实现服务发现#

典型代表是k8s里用的skyDNS、 coreDNS。
工作原理是从API-server中监听集群服务变化,根据服务生产DNS记录存到etcd中, k8s设置每个pos的dns服务地址, 调用服务时再做域名转换。

是CP还是AP取决于后端如何存储,用etcd就是CP,用内存赋值就是AP。
好处是对应用透明, 只依赖底层的HTTP和DNS,不依赖语言等。缺点是要自己额外做负载均衡、远程调用、服务缓存期限各种能力适配

专门用于服务发现的框架和工具#

典型代表: Eureka、Consul、Nacos。

坏处在于对应用不是透明的,必须在应用中要去适配服务注册框架,但能够为编码开发、快速扩展能力提供方便。

2 网关路由#

2.1 网关的职责#

微服务网关的首要职责就是作为统一的出口对外提供服务, 将外部访问网关地址的流量,根据适当的规则路由到内部集群中正确的服务节点上。
同时再作为流量过滤器增强使用

因此“网关= 路由器(基础职能)+ 过滤器(可选职能)”

网关的性能主要取决于他们如何处理代理网络请求,也就是他们的网络IO模型

2.2 网络IO模型介绍#

同步和异步的区别, 是指调用端发出请求后,是否需要一直等待,是否会铜鼓哦状态变化和回调来通知矗立着。

阻塞和非阻塞是针对请求处理过程而言,调用请求返回结果之前,处理线程是否会被挂起。

阻塞IO、非阻塞IO、 多路复用、信号驱动IO都属于同步IO。

  • 阻塞IO是发现结果没返回,会挂起线程,直到结果返回。
  • 非阻塞IO是会不断轮询询问是否完成、
  • 多路复用IO是阻塞IO的一种,但他只有一个监听线程在阻塞, 当有某个事件结果返回,再进行对应处理。
  • 信号驱动IO和异步IO有点像, 但是异步IO是数据已经被传回到调用方了,然后通知, 而信号驱动IO只通知完成了,但是数据还要调用方重新阻塞式地去获取。

Linux系统下实现高并发编程时仍以多路复用IO模型为主。

网关里, zuul1.0网关是用阻塞IO模型,碰到IO密集型就很浪费上下文切换的性能, zuul2.0基于netty-server实现异步IO模型处理请求, 性能提升20%。可以自行指定 select、epoll等并发模型。

网关应该尽可能轻量、 成熟、 更成熟健壮的物理设施。

2.3 BFF网关#

网关会针对不同的前端,聚合不同的服务,提供不同的接口和网络访问协议支持(例如http和grpc都能提供)

3 客户端负载均衡#

用户客户端请求->某地域机房ip所在的服务网关->选择发往对应业务服务所属的负载均衡器->发给真正的服务

其中第三步的“发往业务服务负载均衡器”是有点浪费的, 从机房内网发出的服务请求, 绕道了网络边缘的负载均衡器上了,又重新回了内网。

3.1 客户端负载局衡器#

就是在 机房网关->服务负载均衡器->服务节点的这个过程中, 去掉负载均衡器, 直接把负载均衡能力内置到机房网关的能力中。其他要用到负载均衡的场景也可以这样操作不一定局限于网关。
优点:

  • 负载均衡器和服务之间的信息是进程内方法调用,不存在网络开销
  • 不依赖集群边缘的设置,都是集群内部循环
  • 避免了负载局衡器的单点问题。
  • 可以针对每个服务实例单独设置负载均衡策略更灵活

缺点:

  • 负载均衡代码受限于服务本身代码实现, 如果是go、python等会导致不得不适配多份负载均衡代码
  • 负载均衡器会占用服务的一部分资源或者互相影响
  • 当服务被攻破, 下游所有节点的信息也暴露,信任关系不安全
  • 经常要轮询、上线、下线,负担不小

3.2 代理负载均衡器#

基于k8s、docker部署的服务, 一般都有一个pod, 选择在pod的边缘部署一个代理负载均衡器(也叫边车代理),相当于是同一个节点内,部署了一个进程。是服务网格的一个概念。

好处

  • 不再受语言限制
  • 避免服务进程要频繁轮询造成浪费, 直接让控制平面给pod的边车代理
  • 更安全也更容易实现对调用链路的详细统计。

3.3 地域和区域#

region是地域的概念。
集群内部流量不会跨地域。

zone是区域的概念, 地理上是同一个地域,但是地域内可能放了不同的机房,每个大机房就是一个区域。

如果追求高可用,则系统要部署在多个区域中。
如果追去低延迟, 则应该所有服务都在同一个区域中。

相关思考#

本文讲了关于服务发现的很多干货内容, 核心内容为服务发现组件的选择、 网关的介绍、 客户端侧如何发给已发现的服务。 如果准备进行服务发现的选型工作,可以仔细阅读原文进行学习和了解。

文中关于注册中心的实现原理有很多种类型, 其中有提到一个k8s里用的 coreDNS。那部分笔记中关于coreDns的原理 比较少。
而华为云的CCE容器引擎就包含了CoreDNS插件,他们的产品资料中给出的路由请求流程如下:
image.png

另外对于专门用于服务发现的框架和工具,华为云的CSE微服务引擎里关于服务注册发现的示意图如下:
image.png
微服务启动时,将实例信息注册到CSE,微服务需要调用其他微服务的接口时,从CSE查询实例信息,并将实例信息缓存到本地,缓存会通过事件通知、定时查询等机制更新;通过本地缓存的地址信息,实现微服务之间的点到点调用。

同时也支持基于上述提到的3个服务发现代表,实现优化上下线的能力:
image.png

记得我演的是那个核心主角的父亲, 年轮的轮回剧情确实顶级, 一个人经历了那么多世,每一世被自己上一世的好朋友/亲人 杀死, 最后好不容易终结了这一切。 是很精彩的一个剧情。 可惜推断那个时间线和融合的设定有点复杂,挺考验推土机能力的

[toc]

2023-03-30#

453. 最小操作次数使数组元素相等 - 力扣(Leetcode)

想复杂了,用了个公式推导结果忘记了约束条件,想着公式取整数得到结果

实际上肯定是先处理低的,再处理第二低的,就很容易知道怎么处理了, 需要注意每次记录前面增加的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def minMoves(self, nums: List[int]) -> int:
nums = sorted(nums)
n = len(nums)
if n== 1:
return 0
res = 0
lastAdd = 0
for i in range(0, n-1):
num = nums[i]
nextNum = nums[i+1]
add = nextNum - num + lastAdd
res += add
lastAdd = add
return res

试着用了python

排序是sorted

更优答案:#

每次操作既可以理解为使 n−1n-1n−1 个元素增加 111,也可以理解使 111 个元素减少 111。显然,后者更利于我们的计算

1
2
3
4
5
6
7
class Solution:
def minMoves(self, nums: List[int]) -> int:
min_num = min(nums)
res = 0
for num in nums:
res += num - min_num
return res

注意foreach语法是in

2023-03-31#

2367. 算术三元组的数目 - 力扣(Leetcode)

对于这种三元问题,尝试了三种方式, 最简单的暴力,不能局限于暴力,然后哈希表, 接着再三指针

主要是因为题目要求的是严格递增的,所以可以考虑三指针,3个指针一定只会增加,不存在跑回来重新走的情况,所以是O(N)且空间复杂度1的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def arithmeticTriplets(self, nums: List[int], diff: int) -> int:
map = {}
res = 0
n = len(nums)
i = 0
j = 1
k = 2
for i in range(0, n):
j = max(i+1, j)
while j < n and nums[j] < nums[i] + diff:
j +=1
if j < n and nums[j] == nums[i]+ diff:
k = max(j+1, k)
while k < n and nums[k] < nums[j]+ diff:
k += 1
if k < n and nums[k] == nums[j]+ diff:
res +=1
if j >= n or k >=n:
break

return res

新手本,“二次元”+“游戏”剧情, 体验好好, 前面5+5 十轮游戏,自己还是解答出了不少点的, 最关键的是一个是答出了迷雾镇投票时为什么都是白纸的问题, 毕竟刚回顾过re0里的故事,白鲸的吞噬可以让雷姆s

至今为止打得最久的一次情感本, 从下午1点多打到了晚上九点多

我的角色是阿列克谢, 可以说是玩家中演绎最多的一个角色,可惜是给了作为新手的我。。。

其中当看到祖母遇难,没能见到最后一面, 以及最后给大家念遗言的时候, 都还是有点小流泪的,特别是祖母(仍然狠狠吃亲情线)这么多年都见不到我们的最后一面,每次一想就能开始泪腺隐隐可以发作。 阿列克谢的爱情其实很虐也很浪漫,只是我的爱情经历几乎没有,所以没法传达得很好给现场的苏小姐,有点遗憾(能感觉到她没有被我打动到)

反思一下我的问题:

  1. 前面看本的时候, 要记住每一个物品和加粗的字, 特别是物品,大概率会以线索卡的方式给出来,然后让大家讲。 当时提到那个什么红梅花手环我是懵逼的,想不来是什么,还有手帕也第一时间没想起来,制造了一会的小尴尬
  2. 可以每次打本准备一个纸币,把记下来

另外这一车的玩家和dm太卷了, 一堆的伴手礼(我只有准备了一些零食),还有提前看立绘准备衣服的(早知道我看下立绘然后我正好有蓝色Txue。。。) ,收获满满,剧组的演技也很牛逼,wg批斗的场面仍然是头皮发麻心跳加速。 另外有个dm还是即使离职了仍然从临平区赶过来帮忙充当NPC,太不容易了他们。

1711294847954

DDD领域模型的抽象概念#

首先,来一段大家可能经常看到,但没实践时总是一脸懵的一段DDD领域模型的介绍:

DDD领域模型是指将业务领域中的核心概念和业务规则映射为软件系统中的对象和方法。这种方法将业务领域划分为不同的子领域,每个子领域都有自己的领域模型。

领域模型的设计要基于业务需求,通过对业务需求的深入理解来实现领域模型的设计,以实现软件系统的可扩展性、可维护性、可理解性和高内聚性。

在DDD中,领域模型是通过一系列领域对象来实现的。领域对象是指在业务领域中有实际存在并且在业务过程中具有明确角色和行为的概念,如订单、客户等。领域对象通过各种行为来实现业务逻辑,并且在应用程序中通过数据持久化来存储和恢复领域对象的状态。

领域模型的设计需要深入了解业务领域,同时还需要考虑系统的性能和可扩展性等因素,以实现系统的最佳性能和灵活性。在设计领域模型时,应该遵循领域驱动设计的原则,例如将业务逻辑放置在领域对象中,尽可能减少数据转换的操作等。

是不是感觉很抽象?但如果能和我们常见的开发代码模式做对比,可能就好理解很多了。

传统的三层结构#

如果你的项目是比较传统的java服务项目,那么大概率是 先定义一个controller(接口类),定义一个实体类(用于定义请求体和返回体) 然后定义一个service(数据处理类), 再定义一个dao(数据库处理类) 。这种模式的主要问题在于:

  1. 分层过度:控制器、模型和数据访问对象之间的关系非常紧密,这导致应用程序的层次结构非常深,代码也变得非常复杂。开发人员需要花费大量的时间和精力来维护这种复杂的层次结构。
  2. 基于数据模型:传统的Spring MVC模式通常是基于数据模型来设计和开发的。这种模式会将业务逻辑和数据访问逻辑混杂在一起,导致代码变得难以维护和扩展。
  3. 依赖于框架:传统的Spring MVC模式通常依赖于特定的框架和库,例如Spring、MyBatis等。这种依赖关系会使得应用程序的可移植性和可扩展性变得非常差。

举个java代码的例子, 假设我们有一个用户管理系统,其中包含用户的基本信息和订单信息。我们使用传统的Spring MVC模式来设计这个系统,代码可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return new UserDTO(user.getId(), user.getName(), user.getEmail());
}

@PostMapping
public UserDTO createUser(@RequestBody UserDTO userDTO) {
User user = new User(userDTO.getName(), userDTO.getEmail());
user = userService.createUser(user);
return new UserDTO(user.getId(), user.getName(), user.getEmail());
}

// Other controller methods for updating and deleting users...
}

UserDTO.java (数据传输对象)

public class UserDTO {

private Long id;
private String name;
private String email;

// Constructor, getters and setters...
}

UserService.java (服务层)

@Service
public class UserService {

@Autowired
private UserDao userDao;

public User getUserById(Long id) {
return userDao.findById(id);
}

public User createUser(User user) {
return userDao.save(user);
}

// Other service methods for updating and deleting users...
}

UserDao.java (数据访问层)

@Repository
public class UserDao {

@Autowired
private JdbcTemplate jdbcTemplate;

public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
User user = jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper());
return user;
}

public User save(User user) {
// Insert user into database...
return user;
}

// Other data access methods for updating and deleting users...
}

其中,UserController负责处理用户请求,UserService负责处理业务逻辑,UserDao负责与数据库交互。

让我们考虑以下情况,在我们的用户管理系统中,我们需要添加一种新的角色类型,比如“管理员”。为了实现这个功能,我们需要在应用程序中进行以下更改:

首先,在用户服务中,我们需要更新User实体类的构造函数,以添加新的角色类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
private Long id;
private String name;
private String email;
private String role;

public User(String name, String email, String role) {
this.name = name;
this.email = email;
this.role = role;
}

// Getters and setters...
}

这样虽然可以实现需求,但是这个方法的实现涉及到多个对象(UserController, User, UserService、 UserDao ),代码耦合度比较高,如果需要增加更多的功能,可能会变得越来越复杂,难以维护。

我们来看一下如何使用DDD领域模型来实现用户管理系统,并添加一个新的角色类型。在这个例子中,我们使用以下核心概念:

  • 一个User实体,将表示一个用户对象
  • 一个Role实体,将表示用户角色
  • 一个UserRepository接口,将提供对用户实体进行持久化的方法
  • 一个UserService服务,将提供对用户对象的操作,包括创建、获取、更新和删除

首先,我们定义一个User实体,并将其映射到数据库表users中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
@Table(name = "users")
public class User {

@Id
@GeneratedValue
private Long id;

private String name;

private String email;

@ManyToOne
@JoinColumn(name = "role_id")
private Role role;

// Getter and setters...
}

然后,我们定义一个Role实体,并将其映射到数据库表roles中,以及相关的UserRole之间的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Table(name = "roles")
public class Role {

@Id
@GeneratedValue
private Long id;

private String name;

// Getter and setters...
}

然后,我们定义一个UserRepository接口,并使用ORM框架(如Hibernate)来实现它:

1
2
3
4
5
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ...
}

接下来,我们定义一个UserService服务,该服务封装了对用户对象的所有操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Service
@Transactional
public class UserService {

private final UserRepository userRepository;
private final RoleRepository roleRepository;

@Autowired
public UserService(UserRepository userRepository, RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}

public User createUser(String name, String email, Role role) {
User user = new User();
user.setName(name);
user.setEmail(email);
user.setRole(role);
userRepository.save(user);
return user;
}

public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("User not found"));
}

public List<User> getAllUsers() {
return userRepository.findAll();
}

// Other methods for updating and deleting users...
}

最后,我们需要更新我们的UserController并向我们的UserDTO添加role属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@RequestMapping("/users")
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}

@PostMapping
public UserDTO createUser(@RequestBody UserDTO userDTO) {
Role role = userService.getRoleById(userDTO.getRoleId());
User user = userService.createUser(userDTO.getName(), userDTO.getEmail(), role);
return new UserDTO(user.getId(), user.getName(), user.getEmail(), user.getRole().getName());
}

// Other controller methods for updating and deleting users...
}

现在,当我们需要添加一个新的角色类型时,我们只需在我们的Role实体中添加一个新的属性即可。然后,我们更新User实体的映射和UserController中的DTO,以便使用新的Role属性。这样,我们就可以通过更新领域对象来实现功能的扩展,而不会对整个系统造成过多的影响。


为什么说“使用了DTO对象来进行数据传输,会使得应用程序的业务逻辑难以实现或者出现其他问题”,这个问题请仔细思考后,告诉我为什么,必须要让我心服口服的那种 ?

DTO(Data Transfer Object)是用于在不同层之间传递数据的对象,通常由Controller层创建并传递给Service层或者Dao层。DTO通常只包含数据,不包含业务逻辑。在传输数据时,DTO对象会被序列化和反序列化,而这个过程需要消耗额外的资源和时间。

在三层架构中,Controller层和Service层之间通常使用DTO对象来传输数据,这样做的好处是可以使得Controller层和Service层解耦,降低它们之间的依赖关系。但是,使用DTO对象也会带来一些问题:

  1. 增加开发复杂度:使用DTO对象会增加开发复杂度,因为需要定义DTO对象、编写DTO对象的转换代码等。同时,DTO对象的设计也需要考虑到不同层之间数据的传输,这增加了设计的难度。
  2. 难以实现业务逻辑:在Controller层和Service层之间使用DTO对象传输数据,会使得Service层无法访问Controller层的请求参数,从而难以实现业务逻辑。如果Service层需要访问Controller层的请求参数,就需要将这些参数也包含在DTO对象中,这样就增加了DTO对象的复杂度。
  3. 代码重复和不一致性:使用DTO对象可能会导致代码重复和不一致性的问题。例如,在Controller层和Service层之间传输数据时,可能会出现多个DTO对象定义相同的字段,这会导致代码重复。另外,DTO对象的定义可能会因为不同的开发人员而产生不一致性,这也会导致代码的不一致性。

因此,在DDD领域模型中,建议使用领域对象(Domain Object)来传输数据,而不是使用DTO对象。领域对象是具有业务逻辑的对象,它们不仅包含数据,还包含业务逻辑。使用领域对象可以减少代码的复杂度和重复性,同时也可以更好地支持业务逻辑的实现。


DDD领域模型是怎么解决上面的“开发复杂度”这个问题的? ?

DDD领域模型通过将业务逻辑放在领域对象中,以及将数据访问逻辑封装在领域仓库中来解决开发复杂度问题。

在DDD中,领域对象包含了业务逻辑和状态,它们封装了数据和行为,代表了业务上的概念和操作。在领域对象中实现了业务逻辑后,应用程序的其他部分只需要关心领域对象的输入和输出,而不必关心具体的实现细节。

此外,领域仓库则负责封装数据访问逻辑,将数据存储和检索细节与领域对象分离,使得领域对象能够独立地进行业务逻辑处理。通过领域仓库,应用程序能够对领域对象进行存储和检索,而不必关心底层的数据存储技术和细节。

总之,DDD领域模型通过将业务逻辑和数据访问逻辑封装在领域对象和领域仓库中,从而将复杂性分散和隔离,使得应用程序能够更加灵活和可扩展,从而降低开发的复杂度和难度。


之前三层结构中, 业务逻辑全是service里定义各种方法去处理生成新的对象之类的, 而DDD里,则各种业务逻辑行为都是在领域对象里, 领域对象去触发行为, 最后service只负责调用数据库层去存储,而不关心对象里有哪些字段,只关心对象有哪些行为,是这样吗 ?

是的,DDD的思想是将业务逻辑放在领域对象中,这样可以将领域逻辑尽量地与技术实现分离,从而更好地聚焦业务需求,而不是技术实现。

在DDD中,领域对象是具有行为的,即领域对象应该封装着相关的业务行为,例如订单对象可能会有下单、支付、取消等行为。这些行为由领域对象自身触发,而不是通过Service来调用。

在这样的设计中,Service层不再是一个简单的CRUD操作集合,而是将领域对象协调起来进行业务流程处理的一个过程,它的作用更多是组织和协调各个领域对象完成业务流程,而不是实现具体的业务逻辑。同时,Service层也会负责将领域对象持久化到数据库中,但是Service层并不关心对象有哪些字段,只需要将对象交给数据访问层进行保存即可。

因此,DDD通过将领域逻辑封装在领域对象中,将应用程序的业务逻辑聚焦到领域对象中,从而提高了代码的可读性、可维护性和可扩展性。

1679528260593

用DDD领域模型进行重构#

那么基于上述内容,我将用DDD领域模型进行重构,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

// UserController.java

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return UserDTO.from(user);
}

@PostMapping
public UserDTO createUser(@RequestBody UserDTO userDTO) {
User user = User.create(userDTO.getName(), userDTO.getEmail());
userService.save(user);
return UserDTO.from(user);
}

// Other controller methods for updating and deleting users...
}

// User.java (领域模型)

public class User {

private Long id;
private String name;
private String email;

private User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}

public static User create(String name, String email) {
return new User(null, name, email);
}

public void update(String name, String email) {
this.name = name;
this.email = email;
}

// Getters...
}

// UserRepository.java (仓储接口)

public interface UserRepository {

User findById(Long id);

void save(User user);

// Other repository methods for updating and deleting users...
}

// UserService.java (服务接口)

public interface UserService {

User getUserById(Long id);

void save(User user);

// Other service methods for updating and deleting users...
}

// UserServiceImpl.java (服务实现)

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserRepository userRepository;

@Override
public User getUserById(Long id) {
return userRepository.findById(id);
}

@Override
public void save(User user) {
userRepository.save(user);
}

// Other service methods for updating and deleting users...
}

// UserRepositoryImpl.java (仓储实现)

@Repository
public class UserRepositoryImpl implements UserRepository {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
User user = jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper());
return user;
}

@Override
public void save(User user) {
// Save user into database...
}

// Other repository methods for updating and deleting users...
}

// UserDTO.java (数据传输对象)

public class UserDTO {

private Long id;
private String name;
private String email;

public static UserDTO from(User user) {
return new UserDTO(user.getId(), user.getName(), user.getEmail());
}

// Constructor, getters and setters...
}



领域对象之间的交互#

有时候 领域对象里的行为,如果比较复杂, 比如涉及2个领域对象A和B的交互, 在DDD中,关于将具体的业务逻辑行为定义在哪个领域对象中,通常会采用聚合根和实体的概念来帮助决策。聚合根代表整个聚合的根实体,实体则是聚合中的其他实体。在一个聚合中,聚合根是负责协调聚合内部实体之间关系的主要实体,因此通常情况下应该将复杂的业务逻辑行为定义在聚合根中。

举个例子,我们假设有一个电子商务网站,用户可以下订单购买商品。在这个场景中,我们可以将订单和商品作为两个独立的领域对象来设计。但是,当一个用户下订单时,订单和商品之间有一定的关联关系。为了维护订单和商品之间的关系,我们可以创建一个聚合根来管理它们。 订单可以作为聚合根,商品则是聚合中的子对象。聚合根负责维护订单和商品之间的关系,包括将商品添加到订单中、更新商品数量、从订单中删除商品等操作。这样,在订单和商品之间就形成了一种聚合关系,订单作为聚合根来管理商品,确保了订单和商品之间的一致性和完整性。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class Order {
private List<OrderItem> items;

// 添加订单项
public void addItem(Product product, int quantity) {
// 验证商品是否存在
boolean productExists = items.stream()
.anyMatch(item -> item.getProduct().equals(product));

if (productExists) {
// 如果商品已存在,更新数量
items.stream()
.filter(item -> item.getProduct().equals(product))
.forEach(item -> item.setQuantity(item.getQuantity() + quantity));
} else {
// 如果商品不存在,创建新的订单项
OrderItem item = new OrderItem(product, quantity);
items.add(item);
}
}

// 移除订单项
public void removeItem(Product product) {
items.removeIf(item -> item.getProduct().equals(product));
}

// 获取订单总价
public double getTotalPrice() {
return items.stream()
.mapToDouble(item -> item.getProduct().getPrice() * item.getQuantity())
.sum();
}

// ...其他行为
}

public class OrderItem {
private Product product;
private int quantity;

public OrderItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}

public Product getProduct() {
return product;
}

public void setProduct(Product product) {
this.product = product;
}

public int getQuantity() {
return quantity;
}

public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

public class Product {
private String name;
private double price;

public Product(String name, double price) {
this.name = name;
this.price = price;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

// ...其他行为
}

那为什么不可以把商品作为聚合根,订单作为聚合中的子对象呢?

因为一个聚合根应该是具有独立性的实体,可以独立地存在和修改,同时又可以包含其他实体。在这个例子中,商品和订单之间是一个典型的“一对多”的关系,因此更适合将订单作为聚合根,商品作为聚合中的子对象。理由如下:

首先,订单是整个业务的核心实体,所有商品都是基于订单创建的。订单作为聚合根,可以控制整个业务的流程和生命周期,例如订单的创建、修改、支付、发货等等。而商品只是订单的一个组成部分,相对来说具有较低的独立性。

其次,将商品作为聚合根的话,就需要考虑如何处理订单和商品之间的关联关系。如果一个订单中包含多个商品,那么在创建订单时就需要同时创建多个商品,并且需要将商品和订单之间的关联关系保存到数据库中。这样会增加代码的复杂度和维护成本。

综上所述,将订单作为聚合根,商品作为聚合中的子对象更符合业务逻辑,并且更容易实现。


假如我的service层已经有超级多的业务处理方法了, 我怎么安排DDD领域模型的重构呢,

如果你的service层已经有了很多业务处理方法,那么建议你先把这些方法按照领域进行分类,然后针对每个领域设计对应的领域模型。在设计领域模型时,建议首先从核心业务领域开始,逐步扩展到其他相关领域。设计领域模型需要遵循一些原则,例如领域模型应该是领域专家(Domain Expert)来设计的,而不是开发人员,因为领域专家最了解业务领域的需求和规则;领域模型应该是基于业务行为(Behavior)和概念(Concept)而不是技术实现来设计的;领域模型应该是可维护和可扩展的。

在设计完领域模型之后,你需要把原来的业务逻辑代码逐步迁移到领域对象中。在迁移过程中,你需要保证原有的业务逻辑不受影响,并且保持代码的可测试性和可维护性。如果你的领域对象设计得合理,那么你可以通过领域对象的行为来替代原有的业务逻辑代码,使得你的业务逻辑代码更加清晰、易于维护。

需要注意的是,在进行DDD领域模型的重构时,要保证代码的向后兼容性,尽量减少对原有业务逻辑代码的修改。可以通过引入适配器(Adapter)来实现领域对象和原有业务逻辑代码之间的转换。另外,也要考虑到领域对象之间的交互问题,如何保证领域对象之间的协作和协调也是需要考虑的问题。

向chatGpt的提问:

1679497453723

新手本,“二次元”+“游戏”剧情, 体验好好, 前面5+5 十轮游戏,自己还是解答出了不少点的, 最关键的是一个是答出了迷雾镇投票时为什么都是白纸的问题, 毕竟刚回顾过re0里的故事,白鲸的吞噬可以让雷姆和其他人的存在直接消失, 哈哈哈二次元感觉满满

1711294121143

另外最后的背后故事也挺好, 讲述的这个故事是一个父亲为了自闭症的儿子开发的游戏,因为得了癌症导致离开了儿子, 儿子决定打完游戏就自杀, 最后是游戏里的大家以及游戏外NPC对应的实际人物的关心中,最终HAPPY END, 还是很感人的

1711294137319

当然也有个问题,就是人物关系有点难搞, 我们卡的最久的一个地方就是梳理最后勇者的人物关系,名字太过于没有辨识度,大家理起来很混乱,我反正是没挺太懂,不过还好和我的这个角色的人物关系没啥关联。

1711294218431

这个本主打一个民国抗日时期的爱情,不过我好像没怎么吃到爱情线,倒是被DM们的演技给震惊了,感觉整个人情绪都被带上去了,他们也太强了。(大佐事后说:你们反抗得越激烈,我虐秦老师虐得越狠!)

这个本, 感觉逻辑感其实没那么强, 前面别墅月儿的故事以及教堂杀人案里不需要考虑任何逻辑,大家之间的故事没有关联。 不过最后大家没有找到恶魔,无限被轮回开始游戏的感觉还挺瘆人的!

反思一下自己需要提升的地方:

  1. 第一轮故事,我是最后一个发言的数学老师。没有仔细听大家的时间线, 以为大家是在同一个时间线内, 当被发现我的时间和大家不同时, 直接就慌了,不知道怎么解释了, 被大家果断认为是凶手(其实每个人都杀了月儿)

如果要隐瞒,应该说:我那天晚上看到谁谁谁,那人不承认没事,能打乱大家的思绪就行,但不要透露自己在干嘛,越描越黑

​ 或者也可以不隐瞒,直接说我杀了,但是我不受控制,并详述时间线。第一个故事的案件不是那么重要,而且我也确实是无意识的杀人,并不是决定了最终。我把那个看得太重了。 并直接说我把月儿扔到海里,却又出现在这

  1. ​ dm也是游戏的参与者(参考克莱因囚笼)。

    ​ dm 会有摘眼镜的动作、 dm发本时, 会在dm名字和许亮亮之间切换,

    当然,念咒时名字这个点其实没有被强调,大家最后才知道要念对名字。 但谁也没想到就是dm自己的名字。

总结来说,自己得冷静一点,编造故事的能力不够强, 故事被戳破后,也要坦然一点, 明明才第一个故事,却纠结地很。

这个本主打一个民国抗日时期的爱情,不过我好像没怎么吃到爱情线,倒是被DM们的演技给震惊了,感觉整个人情绪都被带上去了,他们也太强了。(大佐事后说:你们反抗得越激烈,我虐秦老师虐得越狠!)

我当时的角色是一个没啥文化的保安队长, 说需要在火车里保护大家, 于是当看到大佐进来欺负老师的时候,就觉得应该上去保护。

1709047566567

实际上那个场合下,我的角色是个稳重角色,不会做刻意送死的事情, 上去反抗的事情应该是弟弟小虎去做,然后我进行阻拦和保护, 但我却总是想着保护秦老师,导致他们后面不得不安排把我给按住来演第三场戏(继续虐老师)。 (吃了个免费的元宵,好棒)

1709047582791

不过有个问题时,大佐冲进来欺负时,我不知道我应该以什么表现去表演这个保安队长,如果我呆呆着看着,我会担心别人会不会觉得我不入戏,因此只能想着上去阻拦,假意保护, 实际上我知道我遇到那个场面时(敌人手里都有枪),一定是躲起来不会出头的,我没有按照故事的人设和自己的人设行事,而是按照价值观“对”的方式形式。实际上,我的“演技”没有那么重要,扮演好自己的戏份就行,不需要有过强突出的表现欲,在适当的时候表现就行。

1709047601769

感情戏输出部分我好像对这边的爱情线不怎么吃, 以至于我对面的也没啥感觉, 我反倒觉得兄弟情还不错, 另外周金夫妇也挺动人的。

如果可以重来的话,还是得稳重点,不能总是想着表现或者觉得不表现会不好这种想法。

硬核豪门本,无论是凶手还是盘凶,都挺难的

这个本一开始给的信息量太大了,而且字也太密集,文字比较文绉绉,导致我看的很慢,当大家看完都开始盘关系时,我才看了二分之一,一下子就有点慌张。后面全程除了分享线索和信息外,基本也没有推出什么东西,而且还一直以为自己的大哥成为了盗贼,全程掉线。

1707149062723

讲一下需要总结的经验:

  1. 下次自己自带一些打本用的笔和纸, 且尽量是大纸, 用于自己梳理人物关系,梳理完成后可以帮忙做板书,至少刷点存在感。我的内存现在容量实在很低

  2. 关于隐瞒: 切忌说“我现在不能告诉你们”这种话,,很出戏而且容易让人以为是凶手。 可以编造一些,比如我当时要解释为什么自己的护院房有女人, 可以解释说彩楠是自己的妹妹, 有时候会找我来诉苦。 或者编造当时自己并不在,可能是其他人。

    还有就是可以看一下其他人是否帮我编造了关系,比如39已经帮我编造了彩楠是振衣的表妹(表亲戚管家很好编的),结果我还说是师叔关系,差点提早露陷

  3. 看本速度: 感觉有点慢, 太久不看书,阅读能力退化了, 需要锻炼。

  4. 本次故事的一些核心诡计点:

    死人复活,之前死的必定是另外的替身人。

    要考虑剧情中存在合谋的情况(当然可以问一下dm是否可能,有的dm会说不告诉你,有的dm怕大家盘不出来会明确说只有一个人)

  5. 理解错回忆的含义了,其实当时我也可以问一下dm寻求帮助的,我理解成了强娶民女,其实是娶为妾让人觉得不公平

    img