1
Abstract: summarizes the DCI architecture in one sentence: Domain objects (Object) play (Cast) different roles (Role) in different scenarios (Context), and the roles complete specific services through interaction (Interactive) logic.

This article is shared from Huawei Cloud Community " achieve DCI architecture ", author: Yuan Runzi.

Preface

In the concept of object-oriented programming, an application is an abstraction of the real world. We often model things in reality as classes/objects in programming languages ( "what" ), and the behavior of things builds The modulus is the method ( "what to do" ). Object-oriented programming has three basic characteristics (encapsulation, inheritance / combinations, polymorphism) and five basic principles (single responsibility principle, the principle of opening the closure, Richter substitution principle, dependency inversion principle, the principle of separation of the interface), but Knowing this is not enough for us to design a good program, so many methodologies have emerged.

The most popular recently is Domain Driven Design (DDD), where the entity, value object, aggregation and other modeling methods proposed by tactical modeling can guide us to design a domain model that conforms to the real world. But DDD is not a panacea. In some application scenarios, programs designed according to traditional tactical modeling/object-oriented methods may have problems such as poor maintainability and violation of the single responsibility principle.

The DCI modeling method introduced in this article can be regarded as an aid to tactical modeling. In some scenarios, it can make up for some shortcomings of DDD tactical modeling. Next, we will use a case to introduce how DCI solves these shortcomings of DDD tactical modeling.

The code involved in this article is archived in the github project: https://github.com/ruanrunxue/DCI-Architecture-Implementation

Case study

Considering the daily life of an ordinary person, he will attend classes at school, go to work in the company during the summer vacation, go to the park after work, and eat, drink and play at home like ordinary people. Of course, a person's life is much more than that. For the convenience of explanation, this article only provides modeling examples for these typical scenarios.
image.png

Modeling with DDD

According to the idea of DDD tactical modeling, first, we will list the common language of

People, ID card, bank card, home, eating, sleeping, playing games, school, student card, study, exam, company, work card, work, work, park, ticket purchase, play
Next, we use tactical modeling techniques (value objects, entities, aggregations, domain services, resource libraries) to model the domain of the common language.

The code directory structure after DDD modeling is as follows:

- aggregate: 聚合
  - company.go
  - home.go
  - park.go
  - school.go
- entity: 实体
  - people.go
- vo: 值对象
  - account.go
  - identity_card.go
  - student_card.go
  - work_card.go

We model the concepts of ID card, student card, work card, and bank card as Value Objects:

package vo

// 身份证
type IdentityCard struct {
    Id   uint32
    Name string
}

// 学生卡
type StudentCard struct {
    Id     uint32
    Name   string
    School string
}

// 工卡
type WorkCard struct {
    Id      uint32
    Name    string
    Company string
}

// 银行卡
type Account struct {
    Id      uint32
    Balance int
}

...

Then we model the person as an Entity, which contains objects such as ID cards and student cards, and also has behaviors such as eating and sleeping:

package entity

// 人
type People struct {
    vo.IdentityCard
    vo.StudentCard
    vo.WorkCard
    vo.Account
}

// 学习
func (p *People) Study() {
    fmt.Printf("Student %+v studying\n", p.StudentCard)
}
// 考试
func (p *People) Exam() {
    fmt.Printf("Student %+v examing\n", p.StudentCard)
}
// 吃饭
func (p *People) Eat() {
    fmt.Printf("%+v eating\n", p.IdentityCard)
    p.Account.Balance--
}
// 睡觉
func (p *People) Sleep() {
    fmt.Printf("%+v sleeping\n", p.IdentityCard)
}
// 玩游戏
func (p *People) PlayGame() {
    fmt.Printf("%+v playing game\n", p.IdentityCard)
}
// 上班
func (p *People) Work() {
    fmt.Printf("%+v working\n", p.WorkCard)
    p.Account.Balance++
}
// 下班
func (p *People) OffWork() {
    fmt.Printf("%+v getting off work\n", p.WorkCard)
}
// 购票
func (p *People) BuyTicket() {
    fmt.Printf("%+v buying a ticket\n", p.IdentityCard)
    p.Account.Balance--
}
// 游玩
func (p *People) Enjoy() {
    fmt.Printf("%+v enjoying park scenery\n", p.IdentityCard)
}

Finally, we model schools, companies, parks, and homes into Aggregates, which are composed of one or more entities and value objects, and organize them to complete specific business logic:

package aggregate

// 家
type Home struct {
    me *entity.People
}
func (h *Home) ComeBack(p *entity.People) {
    fmt.Printf("%+v come back home\n", p.IdentityCard)
    h.me = p
}
// 执行Home的业务逻辑
func (h *Home) Run() {
    h.me.Eat()
    h.me.PlayGame()
    h.me.Sleep()
}

// 学校
type School struct {
    Name     string
    students []*entity.People
}
func (s *School) Receive(student *entity.People) {
    student.StudentCard = vo.StudentCard{
        Id:     rand.Uint32(),
        Name:   student.IdentityCard.Name,
        School: s.Name,
    }
    s.students = append(s.students, student)
    fmt.Printf("%s Receive stduent %+v\n", s.Name, student.StudentCard)
}
// 执行School的业务逻辑
func (s *School) Run() {
    fmt.Printf("%s start class\n", s.Name)
    for _, student := range s.students {
        student.Study()
    }
    fmt.Println("students start to eating")
    for _, student := range s.students {
        student.Eat()
    }
    fmt.Println("students start to exam")
    for _, student := range s.students {
        student.Exam()
    }
    fmt.Printf("%s finish class\n", s.Name)
}

// 公司
type Company struct {
    Name    string
    workers []*entity.People
}
func (c *Company) Employ(worker *entity.People) {
    worker.WorkCard = vo.WorkCard{
        Id:      rand.Uint32(),
        Name:    worker.IdentityCard.Name,
        Company: c.Name,
    }
    c.workers = append(c.workers, worker)
    fmt.Printf("%s Employ worker %s\n", c.Name, worker.WorkCard.Name)
}
// 执行Company的业务逻辑
func (c *Company) Run() {
    fmt.Printf("%s start work\n", c.Name)
    for _, worker := range c.workers {
        worker.Work()
    }
    fmt.Println("worker start to eating")
    for _, worker := range c.workers {
        worker.Eat()
    }
    fmt.Println("worker get off work")
    for _, worker := range c.workers {
        worker.OffWork()
    }
    fmt.Printf("%s finish work\n", c.Name)
}

// 公园
type Park struct {
    Name     string
    enjoyers []*entity.People
}
func (p *Park) Welcome(enjoyer *entity.People) {
    fmt.Printf("%+v come to park %s\n", enjoyer.IdentityCard, p.Name)
    p.enjoyers = append(p.enjoyers, enjoyer)
}
// 执行Park的业务逻辑
func (p *Park) Run() {
    fmt.Printf("%s start to sell tickets\n", p.Name)
    for _, enjoyer := range p.enjoyers {
        enjoyer.BuyTicket()
    }
    fmt.Printf("%s start a show\n", p.Name)
    for _, enjoyer := range p.enjoyers {
        enjoyer.Enjoy()
    }
    fmt.Printf("show finish\n")
}

Then, the model modeled according to the above method is like this:
image.png

The operating method of the model is as follows:

paul := entity.NewPeople("Paul")
mit := aggregate.NewSchool("MIT")
google := aggregate.NewCompany("Google")
home := aggregate.NewHome()
summerPalace := aggregate.NewPark("Summer Palace")
// 上学
mit.Receive(paul)
mit.Run()
// 回家
home.ComeBack(paul)
home.Run()
// 工作
google.Employ(paul)
google.Run()
// 公园游玩
summerPalace.Welcome(paul)
summerPalace.Run()

Anemia Model VS Congestion Model (Engineering School VS Academic School)

In the previous section, we used DDD's tactical modeling to complete the case domain model. The core of the model is the People entity, which has data attributes such as IdentityCard and StudentCard, as well as business behaviors such as Eat(), Study(), and Work(), which are very in line with the definition in the real world. hyperemia model advocated by the academic school, which has both data attributes and business behaviors.

However, the congestion model is not perfect, it also has many problems, the more typical are these two:

Question 1: God class

People as an entity contains too many responsibilities, causing it to become a veritable God class. Imagine that there are still many attributes and behaviors contained in "people" are cut out. If you want to model a complete model, there are so many attributes and methods that you can't imagine. God class violates the single responsibility principle, which will cause the maintainability of the code to become extremely poor.

Problem 2: Coupling between modules

School and Company should be independent of each other. School does not have to pay attention to whether they are working or not, and Company does not have to pay attention to whether they are taking exams. But now because they all rely on the entity People, School can call the Work() and OffWork() methods related to Company, and vice versa. This leads to unnecessary coupling between modules and violates the principle of interface isolation.

These problems are unacceptable to the engineering school. From the perspective of software engineering, they make the code difficult to maintain. The more common way to solve this kind of problem is to split the entity, such as modeling the entity's behavior into a domain service, like this:

type People struct {
    vo.IdentityCard
    vo.StudentCard
    vo.WorkCard
    vo.Account
}

type StudentService struct{}
func (s *StudentService) Study(p *entity.People) {
    fmt.Printf("Student %+v studying\n", p.StudentCard)
}
func (s *StudentService) Exam(p *entity.People) {
    fmt.Printf("Student %+v examing\n", p.StudentCard)
}

type WorkerService struct{}
func (w *WorkerService) Work(p *entity.People) {
    fmt.Printf("%+v working\n", p.WorkCard)
    p.Account.Balance++
}
func (w *WorkerService) OffWOrk(p *entity.People) {
    fmt.Printf("%+v getting off work\n", p.WorkCard)
}

// ...

image.png

This modeling method solves the above two problems, but it also becomes the so-called anemia model : People becomes a pure data type without any business behavior. In human psychology, such a model cannot establish a corresponding relationship with the real world, and it is not easy for people to understand, so it is resisted by the academics.

So far, both the anemia model and the congestion model have their own advantages and disadvantages, and neither the engineering school nor the academic school can convince each other. Next, it's the turn of the protagonist of this article.

DCI architecture

DCI (Data, Context, Interactive) architecture is an object-oriented software architecture model, which was first proposed in the article "The DCI Architecture: A New Vision of Object-Oriented Programming". Compared with traditional object-oriented, DCI can better model the relationship between data and behavior, so that it is easier to understand.

  • Data , also known as data/domain objects, is used to describe the "what" of the system. Tactical modeling in DDD is usually used to identify the domain objects of the current model, which is equivalent to the domain layer in the DDD layered architecture.
  • Context , also known as the scene, can be understood as the Use Case of the system, representing the business processing flow of the system, and equivalent to the application layer in the DDD layered architecture.
  • Interactive , also known as interaction, is the biggest development of DCI relative to traditional object-oriented. It believes that we should explicitly play the role of the domain object (Object) in each business scenario (Context) (Cast) (Role) Perform modeling. Role represents the business behavior ("what to do") of the domain object in the business scenario, and the complete obligation process completed through interaction between Roles.

This kind of role-playing model is not unfamiliar to us, and it can be seen everywhere in the real world. For example, an actor can play the role of a hero in this movie or the role of a villain in another movie.

DCI believes that role modeling should be context-oriented, because specific business behaviors are meaningful only in specific business scenarios. By modeling the Role, we can split the methods of the domain object, thus avoiding the emergence of the god class. Finally, domain objects integrate Role through composition or inheritance, so that they have the ability to play roles.
image.png

On the one hand, the DCI architecture makes the domain model easy to understand through the role-playing model, and on the other hand, the " small class and large object " approach avoids the problem of gods, thereby better solving the dispute between anemia model and congestion model. In addition, after splitting the behavior of domain objects according to Role, the modules are more highly cohesive and less coupled.

Modeling with DCI

Going back to the previous case, using the DCI modeling idea, we can divide the behaviors of "people" according to different roles. Eating, sleeping, and playing games are behaviors as human roles; studying and taking exams are behaviors as student roles; going to work and leaving work are behaviors as employees; buying tickets and playing games are behaviors as players . In the scene of "people" at home, they play the role of human beings; in the school scene, they play the role of students; in the company scene, they play the role of employees; in the park scene, they play the role of The role of the player.
image.png

It should be noted that students, employees, and players should all have the behavior of human roles. For example, in school, students also need to eat.

Finally, the model modeled according to DCI should look like this:
image.png

In the DCI model, People is no longer a "god class" containing many attributes and methods. These attributes and methods are split into multiple Roles for implementation, and People is composed of these Roles.

In addition, School and Company are no longer coupled. School only refers to Student and cannot call the Work() and OffWorker() methods of Workers related to Company.
image.png

Code to implement DCI model

The code directory structure after DCI modeling is as follows;

- context: 场景
  - company.go
  - home.go
  - park.go
  - school.go
- object: 对象
  - people.go
- data: 数据
  - account.go
  - identity_card.go
  - student_card.go
  - work_card.go
- role: 角色
  - enjoyer.go
  - human.go
  - student.go
  - worker.go

From the code directory structure, the DDD and DCI architectures are not much different. The aggregate directory has evolved into a context directory; the vo directory has evolved into a data directory; and the entity directory has evolved into an object and role directory.

First of all, we realize the basic role Human, Student, Worker, Enjoyer all need to combine it:

package role

// 人类角色
type Human struct {
    data.IdentityCard
    data.Account
}
func (h *Human) Eat() {
    fmt.Printf("%+v eating\n", h.IdentityCard)
    h.Account.Balance--
}
func (h *Human) Sleep() {
    fmt.Printf("%+v sleeping\n", h.IdentityCard)
}
func (h *Human) PlayGame() {
    fmt.Printf("%+v playing game\n", h.IdentityCard)
}

Next, we will implement other roles. It should be noted that Student, Worker, and Enjoyer cannot be directly combined with Human , otherwise the People object will have 4 Human sub-objects, which does not match the model:

// 错误的实现
type Worker struct {
    Human
}
func (w *Worker) Work() {
    fmt.Printf("%+v working\n", w.WorkCard)
    w.Balance++
}
...
type People struct {
    Human
    Student
    Worker
    Enjoyer
}
func main() {
    people := People{}
  fmt.Printf("People: %+v", people)
}
// 结果输出, People中有4个Human:
// People: {Human:{} Student:{Human:{}} Worker:{Human:{}} Enjoyer:{Human:{}}}

To solve this problem, we introduced the xxxTrait interface:

// 人类角色特征
type HumanTrait interface {
    CastHuman() *Human
}
// 学生角色特征
type StudentTrait interface {
    CastStudent() *Student
}
// 员工角色特征
type WorkerTrait interface {
    CastWorker() *Worker
}
// 游玩者角色特征
type EnjoyerTrait interface {
    CastEnjoyer() *Enjoyer
}
Student、Worker、Enjoyer组合HumanTrait,并通过Compose(HumanTrait)方法进行特征注入,只要在注入的时候保证Human是同一个,就可以解决该问题了。

// 学生角色
type Student struct {
    // Student同时也是个普通人,因此组合了Human角色
    HumanTrait
    data.StudentCard
}
// 注入人类角色特征
func (s *Student) Compose(trait HumanTrait) {
    s.HumanTrait = trait
}
func (s *Student) Study() {
    fmt.Printf("Student %+v studying\n", s.StudentCard)
}
func (s *Student) Exam() {
    fmt.Printf("Student %+v examing\n", s.StudentCard)
}

// 员工角色
type Worker struct {
    // Worker同时也是个普通人,因此组合了Human角色
    HumanTrait
    data.WorkCard
}
// 注入人类角色特征
func (w *Worker) Compose(trait HumanTrait) {
    w.HumanTrait = trait
}
func (w *Worker) Work() {
    fmt.Printf("%+v working\n", w.WorkCard)
    w.CastHuman().Balance++
}
func (w *Worker) OffWork() {
    fmt.Printf("%+v getting off work\n", w.WorkCard)
}

// 游玩者角色
type Enjoyer struct {
    // Enjoyer同时也是个普通人,因此组合了Human角色
    HumanTrait
}
// 注入人类角色特征
func (e *Enjoyer) Compose(trait HumanTrait) {
    e.HumanTrait = trait
}
func (e *Enjoyer) BuyTicket() {
    fmt.Printf("%+v buying a ticket\n", e.CastHuman().IdentityCard)
    e.CastHuman().Balance--
}
func (e *Enjoyer) Enjoy() {
    fmt.Printf("%+v enjoying scenery\n", e.CastHuman().IdentityCard)
}

Finally, realize the object of People in this domain:

package object

type People struct {
    // People对象扮演的角色
    role.Human
    role.Student
    role.Worker
    role.Enjoyer
}
// People实现了HumanTrait、StudentTrait、WorkerTrait、EnjoyerTrait等特征接口
func (p *People) CastHuman() *role.Human {
    return &p.Human
}
func (p *People) CastStudent() *role.Student {
    return &p.Student
}
func (p *People) CastWorker() *role.Worker {
    return &p.Worker
}
func (p *People) CastEnjoyer() *role.Enjoyer {
    return &p.Enjoyer
}
// People在初始化时,完成对角色特征的注入
func NewPeople(name string) *People {
  // 一些初始化的逻辑...
    people.Student.Compose(people)
    people.Worker.Compose(people)
    people.Enjoyer.Compose(people)
    return people
}

After the role is split, when implementing scenarios such as Home, School, Company, Park, etc., you only need to rely on the corresponding roles, and you no longer need to rely on the People in this domain:

// 家
type Home struct {
    me *role.Human
}
func (h *Home) ComeBack(human *role.Human) {
    fmt.Printf("%+v come back home\n", human.IdentityCard)
    h.me = human
}
// 执行Home的业务逻辑
func (h *Home) Run() {
    h.me.Eat()
    h.me.PlayGame()
    h.me.Sleep()
}

// 学校
type School struct {
    Name     string
    students []*role.Student
}
func (s *School) Receive(student *role.Student) {
  // 初始化StduentCard逻辑 ...
    s.students = append(s.students, student)
    fmt.Printf("%s Receive stduent %+v\n", s.Name, student.StudentCard)
}
// 执行School的业务逻辑
func (s *School) Run() {
    fmt.Printf("%s start class\n", s.Name)
    for _, student := range s.students {
        student.Study()
    }
    fmt.Println("students start to eating")
    for _, student := range s.students {
        student.CastHuman().Eat()
    }
    fmt.Println("students start to exam")
    for _, student := range s.students {
        student.Exam()
    }
    fmt.Printf("%s finish class\n", s.Name)
}

// 公司
type Company struct {
    Name    string
    workers []*role.Worker
}
func (c *Company) Employ(worker *role.Worker) {
  // 初始化WorkCard逻辑 ...
  c.workers = append(c.workers, worker)
    fmt.Printf("%s Employ worker %s\n", c.Name, worker.WorkCard.Name)
}
// 执行Company的业务逻辑
func (c *Company) Run() {
    fmt.Printf("%s start work\n", c.Name)
    for _, worker := range c.workers {
        worker.Work()
    }
    fmt.Println("worker start to eating")
    for _, worker := range c.workers {
        worker.CastHuman().Eat()
    }
    fmt.Println("worker get off work")
    for _, worker := range c.workers {
        worker.OffWork()
    }
    fmt.Printf("%s finish work\n", c.Name)
}

// 公园
type Park struct {
    Name     string
    enjoyers []*role.Enjoyer
}
func (p *Park) Welcome(enjoyer *role.Enjoyer) {
    fmt.Printf("%+v come park %s\n", enjoyer.CastHuman().IdentityCard, p.Name)
    p.enjoyers = append(p.enjoyers, enjoyer)
}
// 执行Park的业务逻辑
func (p *Park) Run() {
    fmt.Printf("%s start to sell tickets\n", p.Name)
    for _, enjoyer := range p.enjoyers {
        enjoyer.BuyTicket()
    }
    fmt.Printf("%s start a show\n", p.Name)
    for _, enjoyer := range p.enjoyers {
        enjoyer.Enjoy()
    }
    fmt.Printf("show finish\n")
}

The operating method of the model is as follows:

paul := object.NewPeople("Paul")
mit := context.NewSchool("MIT")
google := context.NewCompany("Google")
home := context.NewHome()
summerPalace := context.NewPark("Summer Palace")

// 上学
mit.Receive(paul.CastStudent())
mit.Run()
// 回家
home.ComeBack(paul.CastHuman())
home.Run()
// 工作
google.Employ(paul.CastWorker())
google.Run()
// 公园游玩
summerPalace.Welcome(paul.CastEnjoyer())
summerPalace.Run()

Write at the end

From the scene described above, we can find that the traditional DDD/object-oriented design method has shortcomings in modeling behavior, which leads to the so-called anemia model and congestion model dispute.

The emergence of the DCI architecture makes up for this point. It cleverly solves the coupling problem between the god class and the module in the congestion model by introducing the idea of role-playing, and does not affect the correctness of the model. Of course, the DCI architecture is not a panacea. In a business model with less behavior, it is not appropriate to use DCI to model.

Finally, the DCI architecture is summarized in one sentence: Domain objects (Object) play (Cast) different roles (Role) in different scenarios (Context), and the roles complete specific business logic through interaction (Interactive) .

refer to

1、The DCI Architecture: A New Vision of Object-Oriented Programming, Trygve Reenskaug & James O. Coplien

2. The evolution of software design, Zhang Xiaolong

3. Implement Domain Object in Golang, Zhang Xiaolong

4. DCI: Code comprehensibility, chelsea

5、DCI in C++, MagicBowen

Click to follow, and learn about Huawei Cloud's fresh technology for the first time~


华为云开发者联盟
1.4k 声望1.8k 粉丝

生于云,长于云,让开发者成为决定性力量