用ABP入门DDD

前言

ABP框架一直以来都是用DDD(领域驱动设计)作为宣传点之一。但是用过ABP的人都知道,ABP并不是一个严格遵循DDD的开发框架,又或者说,它并没有完整实现DDD的所有概念。

但是反过来说,认真学过DDD的人会发现,所谓“完整实现了DDD,严格遵循DDD概念”的开发框架其实并不存在。因为DDD本质上是在分析业务,在“落地”的时候与代码有关,但是关系并没有我们所认为的那么大。

所以,个人觉得,从学习如何正确使用ABP框架,去揣摩框架的部分功能的设计意图,也是一种很好的DDD入门方案。

先抛几个常见问题:

  1. 命名空间该如何组织?

  2. AppService应该怎么写?

  3. 实体类应该充血还是贫血?

  4. 什么时候需要写领域服务(DomainService)?

  5. 领域事件(DomainEvents)应该怎么用?

框架并不会严格规定我们该怎么写代码,但是DDD给出了指导性的建议。但如果我们不了解DDD,那么所谓建议就无从说起。

所以,我们还是要从介绍DDD开始。

DDD是一种业务分析方法

DDD领域驱动设计是计算机软件行业为了项目能尽量趋向成功,根据多年经验总结出来的一套业务分析的方法论。其核心是消化特定业务领域的知识并创建忠实反映它的软件模型。

正确的实施并非极其困难,错误的实施却很容易。

DDD并不难,只是中文资料相对缺少,部分词汇初次接触有可能觉得过于抽象(加上某些词的翻译版本不一样),会有点晦涩的感觉。

想找中文资料学习DDD的,可以去博客园搜一下领域驱动设计,这里首推ENode作者汤雪华的博客。

本文重点在于普及,不会讲的特别深入。

要想讲清楚ABP开发框架和DDD的关系,还是要从DDD的作用讲起。

DDD的分析部分——顶层设计

DDD有一些词汇:

  • 统一语言

  • 问题空间,解决方案空间

  • 领域,子领域

  • 上下文,绑定上下文(Bounded Context 有些翻译成边界上下文,简称BC),上下文映射

  • 聚合,实体,值对象

  • 领域服务,领域事件

在分析部分(也有人称之为战略设计,其实就是自上而下的进行分析),我们还不用管聚合、实体、值对象、领域服务、领域事件,只要看前面这些比较抽象的词汇。

统一语言

DDD的第一件事,是定义“统一语言”。

什么是统一语言?

大概解释下,统一语言是为了降低沟通成本(口头、文档、代码等)、减少歧义,通过业务专家(又叫领域专家,就是非常熟悉业务的人)核准和明确语义,项目的官方语言(可以认为是一份术语表,由类似架构师的角色在确认需求的过程中提炼出草案,并后续逐步完善——增加新词汇,明确语义,处理歧义、同义等)。

写代码最头疼的命名问题,统一语言可以帮你解决。不仅是参考,还是标准,原则上不允许随便命名,必须和统一语言保持一致。

问题空间和解决方案空间

问题空间和解决方案空间基本就是字面意思。

形象点说,问题空间是我们在白板上画的一个大圈圈,写上“电子商务”。然后大圈圈里再画上一些线分割开来,一部分是“C端商城”,一部分是“后台管理系统”,一部分是“供应链系统”。(下图只是简化的示意图,不具备参考意义,真实场景需要更细化)

640?wx_fmt=png

而解决方案空间,可以理解为针对“问题”的“答案”,解决方案空间的划分最终对应到我们的代码实现,但这个粒度依然是很大的,比如我们用一个VS2017里解决方案sln(通常是一个单独的代码库)关联的所有项目去实现“C端商城”,另一个sln涉及的项目去实现“供应链系统”。所有sln合起来是这个“问题空间”的“解决方案空间”。当然有时候简单系统只需要一个sln就够了。

除了代码的大粒度组织,这往往也影响团队分工,影响人员组织。

子领域就是对问题空间的继续划分。划分的参考标准是统一语言中的某些词汇是否出现了歧义——部分词汇出现多重含义往往预示着存在子领域。每个子领域中的统一语言是一致的,无歧义的。

绑定上下文就是对解决方案空间(不是VS2017那种解决方案)的继续划分。

所以子领域对应绑定上下文。

而上下文映射,就是搞清楚绑定上下文之间的关系(上下游依赖关系,下游依赖上游——下游上下文受上游上下文变更影响,通常说的防腐层就是为了隔离这种影响)。

640?wx_fmt=png

所有这些词汇,其实核心思想非常简单,四个字——“分而治之”。

但是具体怎么“分”,却没有固定的方案,完全依赖个人对业务领域的理解程度。甚至这个划分方案是随着对业务领域理解的加深而持续变化的。体现到“落地”,就是不断的调整架构或者重构代码。

分析部分最擅长处理的两种场景

一个场景是,业务逻辑确实很多,很难消化、提炼和组织。就是非常复杂,也是DDD的主要目的——应对软件核心复杂性。

另一个场景是业务逻辑还没完全清楚,这一般是指初创企业,特别是创新型企业,没有行业参照,自己摸索的情况下。

两个场景都依赖“统一语言”的威力。前者可以通过统一语言促进理解,降低沟通成本。后者可以通过统一语言来表现对业务现状的理解和展望其未来的走向。

分析部分最重要的两个元素

统一语言和绑定上下文是DDD分析部分最重要的两个元素。


640?wx_fmt=png

定上下文继续向下细分,才会涉及每个绑定上下文的架构问题,此时才开始考虑如何“落地”,也就是下面说的策略部分,选择支撑架构。

关于DDD分析部分,还涉及很多具体的指导方法,请自行参阅文末所列相关书籍。分析部分进行顶层设计,最重要的产出就是绑定上下文(BC)的划分及BC之间的关系(上下文映射)。

DDD的策略部分——支撑架构

众所周知,DDD有一定的前期成本,而它的好处是降低了一个系统后续的长期维护代价。

所以,为每个绑定上下文(BC)选择支撑架构(实现方案)的指导原则是看“软件的使用期限”。

上面两句话其实有一点矛盾——看起来好像是用了就丢的一次性软件系统不值得使用DDD,但是这个系统的BC是用DDD划分出来的。

其实这里的DDD,有歧义,指的是DDD的一个推荐支撑架构——领域模型,而我们前面分析得到这个绑定上下文(BC),是DDD分析部分的一个结果。

也只有到了某个BC是核心业务,需要长期维护、迭代演进的时候,我们才会考虑用领域模型(一种特殊的对象模型)来实现这个BC的支撑架构。到这一步,我们才涉及到诸如OOP开发语言,ABP开发框架这些选择具体技术栈的问题。

特殊的对象模型意思是,对象模型关注对象和对象之间的关系,即使贫血模型依然是对象模型,特殊是指领域模型关注对象的行为,即要求充血模型。

我们先看看除了领域模型,对于支撑架构还有哪些可能选择。

CRUD也是一种支撑架构

在看DDD相关的书之前,我们往往认为CRUD相当low,事务脚本相当low,不管什么都该用领域模型(这里不叫DDD了,区分下)来实现。

这就有种,拿着锤子,看什么都像钉子的感觉。

其实所有DDD相关书籍都在劝我们,具体情况具体分析。

如果是短期、一次性项目(这里所有的讨论都是针对某个BC),一般叫“快速应用程序”,工期紧也是一种考虑因素,自然什么熟用什么,CRUD也行,只要行得通。

很多时候优先是解决问题。换句话说:

可以只追求 Make It Work,只要项目是一次性的,无需后续维护的。

再如,一个纯展示的项目,可以直接套用一个现成的CMS系统,而非投入人力去从头开发。

只有当通用软件产品(财务管理,CRM,CMS之类)无法满足需求,而且也无法简单通过一个阶段的定制投入就能解决问题时,我们才需要采用领域模型去分析业务,进行软件建模。

这通常也是老板为什么需要组建一个自己的技术团队的原因。

ABP中的DDD构件

所以,任何开发语言,任何一个能实现CRUD的框架,都可能作为DDD指导下划分出来的某个BC的支撑架构的实现选择。DDD并没有贬低非领域模型式的支撑架构,而是平等的对待它们,因为总有合适的场景,只是依赖个人的经验。

直到这里,我们才开始涉及ABP框架。

分而治之,从大到小

前面我们讲到在统一语言中根据同个词汇的多重含义的线索我们可能将一个问题空间划分成多个子域,为每个子域确定绑定上下文(BC)。这可能涉及到多个VS解决方案(sln文件),我们先假设只有一个VS解决方案。

我们通常通过ABP官网的项目模板来初始化我们自己项目的VS解决方案。

在下载完成,解压后,我们可以观察下程序集名称和默认命名空间,这里可以参考ABP系列——QuickStartB:正确理解Abp解决方案的代码组织方式、分层和命名空间。

接下来以Personball.Demo.sln为例

对于解决方案Personball.Demo.sln,我们发现多数类库程序集的默认命名空间是Personball.Demo。再下一层,一般就是实体名称的复数形式命名的文件夹(跨程序集保持一致)。

注意,命名空间的层次是没有限制的,而且默认对应了文件夹层次结构。
所以

对于一个解决方案中容纳多个BC,我们可以通过命名空间来体现BC的隔离。

在BC之上,我们描述架构,可能是一系列草图,主要用于分析边界、BC之间的关系,做一些顶层设计。当各个BC的边界划分明确后,开始分析一个BC内的业务,我们就用到了聚合和实体的概念。

实体的定义很简单,ABP有实体的泛型基类Entity<T>,其中主要就是一个属性:Id。其他的FullAuditedEntity或者CreationAuditedEntity都是框架提供的方便审计的基类扩展。

所以,实体就是

领域中具有唯一标识的对象。

从命名空间上看,我们可以给BC一个名字,让它逻辑上“统领”一部分代码,这些代码主要就是一些实体类。但是实体类也是有主次之分的。典型的例子就是Order实体和OrderItem实体。虽然OrderItem有自己的id,但我们几乎不会单独引用OrderItem,因为单独一条OrderItem几乎不会有业务意义(不能说死,不排除个别我没见识过的业务场景)。一个Order有多个OrderItem,对OrderItem的操作通过Order进行代理,这里,Order就是聚合根。

把一组实体放一起,就是聚合,其中作为主要代表的实体即是聚合根。聚合之间只能通过聚合根进行引用,不能直接引用聚合中的非聚合根实体。

按Order来说,其他聚合要引用Order的时候,记录的是OrderId(或者订单号),假设其他聚合要处理某个Order的OrderItem,它也只能引用Order,让Order去处理它自己的OrderItem。这其实是一种内聚的思想,或者叫封装,或者叫关注点分离,总之是一种复杂性的隔离(划分BC也是一种复杂性的隔离)。

我们一开始看到ABP的AggregateRoot<T>和IAggregateRoot<T>,几乎是懵的,项目模板中也没有这个基类的范例。再看看这个基类提供的属性DomainEvents,以及ABP框架中涉及该属性机制的源码(看AbpDbContext的SaveChange方法实现)。这时候,我们看到了事件怎么用,开始思考领域事件这个词,开始去学习DDD。

当我们开始思考事件的时候,我们很自然的就会去思考实体的行为(方法)。

我们通过实体方法实现实体自己能够处理的业务逻辑。以“Tell,Not Ask”的原则实现实体的行为。在行为成功完成后,抛出事件,以便外部协同。而聚合根(继承AggregateRoot<T>基类或者实现IAggregateRoot<T>接口)作为其他实体的代理,实现本聚合内的逻辑,通过DomainEvents收集各类事件,交由ABP框架底层来触发事件,实现跨聚合甚至跨BC的协同(同时事件的发布订阅模式也是一种逻辑代码的解耦,顺序无关,EventHandler也可以回滚工作单元)。

另外,DDD中的仓储模式是基于聚合根实体的(聚合根同时代理了非聚合根实体的仓储职责,就是说OrderItem不应该有自己的仓储接口和实现),这一点在ABP中并没有严格限制,或许是ABP作者不希望把框架的使用门槛定的太高。

实体(聚合根也是实体),只能实现自己控制范围内的业务逻辑,控制范围外的呢?

所有无法放到单个实体内实现的业务逻辑,都可以放到领域服务中实现。

这包含,需要同一个实体类的多个实例配合的,需要不同实体类的多个实例配合的,还有其他。只要一个实体的实例无法自己完成这部分逻辑,就需要构建领域服务。

最后,最小的DDD构件,值对象。ABP框架中有一个基类ValueObject<T>,即用来表示值对象。

其实DDD中的值对象对应到代码,有一个很宽泛的范围,可以认为

所有没有唯一标识的数据对象,都是值对象。  

最基本的,比如C#语言的值类型,像string,int,decimal,都是值对象。那么我们为什么还需要一个基类来辅助构造值对象?

第一个原因是,值类型,业务表达能力弱。  

通过float,我们可以知道数量,但是不知道是重量还是体积;
通过decimal我们能表示金额,但是不知道是人民币还是美元。

所以,我们需要自己构建值对象,来更准确的表达业务概念。

第二个原因是,方便。

值对象只能通过各个属性的具体值比较来唯一确定,这个基类帮我们重写了Equals()和GetHashCode(),并重载了相等和不等操作符。

但,这里有个坑

值对象必须保证其不变性

具体看Abp系列——为什么值对象必须设计成不可变的,而ABP框架是无法控制你如何使用ValueObject<T>的子类的。具体地说,

你的值对象必须关闭所有属性的setter,必须通过构造函数来初始化,且不允许通过方法改变属性值。
忘了分层,应用服务层和基础设施层

上面讲的(聚合、聚合根、实体、值对象、领域服务、领域事件)基本都是领域层。
DDD讲领域模型支撑架构的时候,特别提到分层,也是我们从ABP中学到的分层方式:表现层、应用服务层、领域层、基础设施层。

  • 表现层并不特指前端界面,MVC框架也只是一种表现层框架,它只是特别擅长处理Http协议。

  • 应用服务层就是Application程序集,是DDD建议的体现用例的一层,直接对接表现层(类似MVC控制器的协调作用,接受请求,返回DTO/ViewModel),用来编排任务,将工作指派给下层。所以应用服务(AppService)的代码,根据用例进行组织即可。

  • 领域层即是业务模型的完整实现。

  • 基础设施层侧重于持久化技术,比如EF,但是不限于持久化技术(通用功能接口的具体技术实现,类似仓储,接口定义在领域层,实现放在基础设施层)。ABP按照ORM框架名称作为基础设施层的程序集命名可以理解,但不能被其限制。个人建议另开一个程序集如Personball.Demo.Infrastructure,依赖于Personball.Demo.EntityFramework,再让启动模块依赖Infrastructure模块。

扩展:CQRS和事件溯源

当我们说经典领域模型的时候,指的就是基于对象模型来实现业务,数据存储走关系型数据库,一切看起来都很完美。

但是DDD研究的是复杂性。

软件开发行业几十年的经验累积下,前辈们发现如果把软件功能分成两方面,假设系统中查询部分的复杂度是N,命令(创建或变更数据)部分的复杂度也是N。

那么经典领域模型的情况下,系统的命令和查询混在一起,这个总体复杂度就是N乘以N,如果分开,那么系统总体复杂度就会降低到N加N。

另一种说法是,对象模型的局限性日益显现,现在发现关注事件比关注对象更方便业务建模,因为现实世界是基于事件的。这引导我们可以使用函数式编程来实现支撑架构,同时也引出了事件溯源架构。

CQRS,命令与查询职责分离,正如其字面上的意思,一个相当简单的原则,却非常有效的降低了系统的复杂性。

这里并不是要推荐一个CQRS开发框架,只是提一下,大家可以在任何开发框架,任何场景下,按CQRS的方式去思考,都可以获得实际的好处。

再理一遍

  • 统一语言

  • 问题空间、子领域

  • 解决方案空间、绑定上下文/上下文映射、聚合/聚合根、实体、值对象

如果还有不明白的,可以参考下列书籍;如果还想深入学习的,可以参考下列书籍。

希望本文能对你有所启示,由于本人水平有限,若有表达错误的地方,欢迎斧正。

相关书籍

《Microsoft.Net企业级应用架构设计》
架构师参考书,后半本基本都是讲DDD的,也是本文的主要参考(这本最近刚重新看完,也在整理思维导图,下面几本专讲DDD的还没复习,忘得差不多了)

《领域驱动设计》
又称DDD

《实现领域驱动设计》
又称IDDD

《领域驱动设计模式、原理与实践》
又称PPPDDD(英文版书名三个P开头的词在前面)


原文地址:https://personball.com/ddd/2018/12/07/from-abp-to-ddd-i

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/318593.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多重背包的二进制优化(ybtoj-宝物筛选)

文章目录题目描述解析朴素算法代码二进制优化代码thanks for reading!题目描述 解析 朴素算法 首先考虑朴素算法 把数量为num的物体拆成num个子物体 其价值与重量是原物体的1&#xff0c;2&#xff0c;3…num倍 然后当成独立的物体求就行了 注意应该先枚举重量&#xff0c;再…

基于.NET Standard的分布式自增ID算法--Snowflake

概述本篇文章主要讲述分布式ID生成算法中最出名的Snowflake算法。搞.NET开发的&#xff0c;数据库主键最常见的就是int类型的自增主键和GUID类型的uniqueidentifier。那么为何还要引入snowflake呢&#xff1f;INT自增主键自增主键是解决主键生成的最简单方案&#xff0c;它有如…

2021“MINIEYE杯”中国大学生算法设计超级联赛(4)Display Substring(后缀数组+二分)

Display Substring #include<bits/stdc.h> using namespace std; typedef long long ll; // sa[i]: 排名是i位的是第几个后缀 // rk[i]: 第i个后缀的排名是多少 // height[i]: sa[i]与sa[i-1] const int N100010; char s[N]; int rk[N],sa[N],cnt[N],height[N]; int x[N]…

领域驱动设计,让程序员心中有码(二)

引子&#xff0c;软件工程没有银弹上一篇博文领域驱动设计&#xff0c;让程序员心中有码&#xff0c;抛出了一个问题&#xff0c;领域驱动设计真的是万能的良方吗&#xff1f;对于这个问题&#xff0c;大家的答案无疑是一致的&#xff0c;作为一种非常受软件行业欢迎的软件思想…

邮局-[IOI2000](四边形不等式)

概要 四边形不等式的核心在于缩小最优转移的范围 题目描述 传送门 解析 这道题说是不等式&#xff0c;但其实也可以感性理解 &#xff08;其实就是不想证明&#xff09; 定义pl[i][k]: i到n的村庄建造k座邮局时&#xff0c;第一座管辖的范围是i-pl[i][k] (也就是最优决策…

.NET Core实战项目之CMS 第九章 设计篇-白话架构设计

前面两篇文章给大家介绍了我们实战的CMS系统的数据库设计&#xff0c;源码也已经上传到服务器上了。今天我们就好聊聊架构设计&#xff0c;在开始之前先给大家分享一下这几天我一直在听的《从零开始学架构》里面关于架构设计的定义以及架构设计的三大原则&#xff0c;希望能对大…

今日头条Marketing API小工具(.Net Core版本)

前言由于工作原因&#xff0c;需要用到今日头条的Marketing API做一些广告投放的定制化开发。然后看现在网上也没多少关于头条Marketing API的文章&#xff0c;于是便就有了该篇文章。头条Marketing API主页地址&#xff1a;https://ad.toutiao.com/openapi/index.html。头条Ma…

.NET Core实战项目之CMS 第十章 设计篇-系统开发框架设计

这两天比较忙&#xff0c;周末也在加班&#xff0c;所以更新的就慢了一点&#xff0c;不过没关系&#xff0c;今天我们就进行千呼万唤的系统开发框架的设计。不知道上篇关于架构设计的文章大家有没有阅读&#xff0c;如果阅读后相信一定对架构设计有了更近一部的理解&#xff0…

分析现有 WPF / Windows Forms 程序能否顺利迁移到 .NET Core 3.0

今年五月的 Build 大会上&#xff0c;微软说 .NET Core 3.0 将带来 WPF / Windows Forms 这些桌面应用的支持。当然&#xff0c;是通过 Windows 兼容包&#xff08;Windows Compatibility Pack&#xff09;实现的。为了提前检查你的程序是否能在未来跑在 .NET Core 3.0 上&…

ML.NET 0.8特性简介

本周.NET生态圈内的更新源源不断&#xff0c;除了.NET Core 2.2&#xff0c;ASP.NET Core 2.2和Entity Framework Core 2.2之外&#xff0c;ML.NET 0.8也一并登上舞台。新的推荐场景ML.NET使用基于矩阵分解(Matrix Factorization)和场感知分解机(Field-aware Factorization Mac…

F-Lucky Pascal Triangle(Lucas+数位dp)

F-Lucky Pascal Triangle issue是fw题解 下面代码TLE了&#xff0c;但是此题数位dp的思想非常值得学习 Lucas的过程相当于把n,mn,mn,m在p进制下的每一位拿出来做组合数 Lucas(n,m,p)∏(nkmk)modp\text{Lucas}(n,m,p)\prod \dbinom {n_k}{m_k} \bmod pLucas(n,m,p)∏(mk​nk​…

树的合并(ybtoj-树上dp)

文章目录题目描述前言解析代码thanks for reading&#xff01;题目描述 前言 全网唯一AC&#xff01;&#xff01;&#xff01; 妙啊 而且还是完全自己想出来的做法 开心 &#xff08;APIO还是没白听&#xff09; 但是思路出来后代码实现十分坎坷 建两个图分别dfs3次那个地方…

.net core中的高效动态内存管理方案

.net core在新增的System.Buffers中引入了一大堆高效内存管理的类&#xff0c;如span和memory、内存池。本文今天这里介绍一个高效动态内存访问方案。ReadOnlySequenceSegment<T>在我们读取数据的过程&#xff0c;很多时候会出现如下场景&#xff1a;不知道数据实际大小一…

.net core 上 K8S(三)Yaml文件运行.netcore程序

正文上一章我们通过kubectl run简单运行了一个.netcore网站&#xff0c;但实际的开发中&#xff0c;我们都是通过yaml来实现的。1.编写yaml文件关于yaml文件的格式在此就不多描述了&#xff0c;不熟悉的可以去网上搜一下示例。2.运行yamlkubectl create -f netcore.yaml 我们可…

Jozky模板

文章目录字符串处理后缀数组manacherhashKMP最大最小表达法数论约瑟夫环欧拉函数莫比乌斯反演逆序对归并排序求逆序对素数线性筛欧几里得与扩展欧几里得欧几里得算法&#xff1a;扩展欧几里得算法&#xff1a;逆元扩展欧几里得费马小定理欧拉定理递推求逆元__int128高精度运算唯…

Visual Studio 2017 15.9 版本发布:推出全新的导入 / 导出配置功能

Microsoft 在开发 Visual Studio 2019 的同时&#xff0c;还在继续支持 VS2017 的用户。公司已经发布了 9 次更新&#xff0c;这展示了 Microsoft 在常规更新发布之后仍然会坚守继续支持 Visual Studio 的承诺。我们已经介绍过 15.9 版本中的一些新增内容&#xff0c;但是在最终…

染色(树链剖分 洛谷-P2486)

文章目录题目描述解析代码thanks for reading&#xff01;传送门首先&#xff0c;对hash学姐对本题拔刀相助的debug行为表示衷心的感谢 题目描述 解析 用线段树维护颜色序列个数、最左颜色与最右颜色 合并时如果左儿子的最右颜色等于右儿子的最左颜色&#xff0c;就把加和-1 在…

C# 8中的范围类型(Range Type)

C# 8.0中加入了一个新的范围类型(Range Type)。这里我们首先展示一些代码&#xff0c;并一步一步为代码添加一些不同的东西, 为大家展示一下范围类型的功能和用法。我们最原始的代码如下&#xff1a;这里我们显示的定义了我们查询数组的索引1-3, 并输出他们的值。毫无疑问&…

选数游戏(ybtoj-二叉堆)

文章目录题目描述解析代码thanks for reading&#xff01;题目描述 解析 一道很考验代码能力与思维的题 &#xff08;我不是在为自己的菜找理由&#xff09; 首先由于可以每一列都有类似于环的性质 所以我们可以忽略点的出入的纵坐标的位置&#xff0c;只考虑每列选几个 首先&…

CF741C Arpa’s overnight party and Mehrdad’s si

题目描述&#xff1a; 有n对情侣&#xff08;2n个人&#xff09;围成一圈坐在桌子边上&#xff0c;每个人占据一个位子&#xff0c;要求情侣不能吃同一 种食物&#xff0c;并且桌子上相邻的三个人的食物必须有两个人是不同的&#xff0c;只有两种食物&#xff08;1或者是2&…