详细、简单地学习这6种架构设计原则,再学设计模式也是 So Easy的事

大家好,我是皇子。

架构设计原则,在上一篇已经详细列举了。

今天是在此基础上,把我们最常用的、最接地气的6种架构设计原则提取出来,详细的、深入的,通过简单地方式去讲。

通过学习好这6种架构设计原则,可以帮助架构师创建出既稳定又灵活的软件系统,不仅易于理解、开发与维护,还能适应不断变化的需求和技术环境。并且学习完架构设计原则,再学设计模式就是手到擒来的事。

23种设计模式,分为3种分类分别是:创建型模式、结构型模式、行为型模式。一开始就去学习、记忆和理解,还是很容易有些学了就忘的。

因为设计模式本质上是对架构设计原则在具体应用场景中的具体化和实例化,所以我们先学习好架构设计的原则,相当于是掌握了设计模式背后的哲学和通用指导思想,为理解和运用设计模式奠定了坚实的基础。

今天分享的6种架构设计原则,会结合案例和代码进行讲解,最后还有一个关于高内聚低耦合的电商案例。

内容导读:

  • 单一职责原则(SRP)
  • 开闭原则(OCP)
  • 里氏替换原则(LSP)
  • 接口隔离原则(ISP)
  • 依赖倒置原则(DIP)
  • 迪米特法则( LoD)

其中:单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)、依赖倒置原则(DIP),简称SOLID原则

单一职责原则(SRP)

一句话概括

一个模块、类或组件应该只有一个改变它的原因,即每个部分专注于完成一项特定职责。

案例理解

单一职责原则(SRP)就好比在学校里的分工合作一样。想象一下,你在班级里担任班长,如果你既要负责组织运动会,又要管同学们的学习成绩,还要安排每天的值日生,甚至还得负责午餐订餐工作,这样一来,你的任务就非常多且复杂,任何一个方面的变化都可能让你忙不过来,也可能因为过于繁忙而导致某些事情做得不够好。

单一职责原则就是说,每个“角色”——在编程中我们称之为类或者模块,应该只做一件事情,把它做好做精。具体到班长这个角色上,如果遵循单一职责原则,那就可能是分为四个不同的职务:体育委员专门负责运动会,学习委员关注大家的学习情况,生活委员安排日常事务,而后勤委员则负责午餐预订。

换到程序设计领域,这就意味着一个类或模块应该只有一个主要的功能或责任。比如创建一个“运动会组织者”的类,它只负责运动会相关的各种任务;再创建一个“学生成绩管理器”的类,它只关心成绩的统计和分析。这样做的好处在于,当我们需要修改关于运动会的规定时,不会影响到学生成绩管理的部分,反之亦然。每个类的代码更清晰,也更容易理解和测试,不容易因一处改动导致其他功能出错,就像学校的各项活动能各自有序地进行一样。

代码示例

结合上面的案例理解,使用 Go 语言实现单一职责原则(SRP)的代码示例

package main

import "fmt"

// Step 1: 定义遵循单一职责的结构体(类)

// SportsOrganizer 专门负责运动会相关任务,遵循单一职责原则。
type SportsOrganizer struct{}

func (so SportsOrganizer) OrganizeSportsMeet() {
	fmt.Println("体育委员正在组织运动会...")
}

// StudyManager 专注管理学生成绩,符合单一职责原则。
type StudyManager struct{}

func (sm StudyManager) ManageStudentGrades() {
	fmt.Println("学习委员正在管理学生成绩...")
}

// DailyAffairsCoordinator 负责日常事务安排,遵循单一职责原则。
type DailyAffairsCoordinator struct{}

func (dac DailyAffairsCoordinator) ArrangeDailyChores() {
	fmt.Println("生活委员正在安排每日值日生...")
}

// CateringOfficer 专注于午餐预订工作,符合单一职责原则。
type CateringOfficer struct{}

func (co CateringOfficer) HandleLunchOrders() {
	fmt.Println("后勤委员正在处理午餐订餐...")
}

// Step 2: 主程序:使用遵循单一职责的类进行协作

func main() {
	// 创建各角色实例
	sportsOrganizer := SportsOrganizer{}
	studyManager := StudyManager{}
	dailyAffairsCoordinator := DailyAffairsCoordinator{}
	cateringOfficer := CateringOfficer{}

	// 各角色执行各自职责
	sportsOrganizer.OrganizeSportsMeet()
	studyManager.ManageStudentGrades()
	dailyAffairsCoordinator.ArrangeDailyChores()
	cateringOfficer.HandleLunchOrders()
}
代码说明

图片

定义遵循单一职责的结构体(类):按照学校的分工,我们创建了四个结构体类型,分别对应体育委员、学习委员、生活委员和后勤委员的角色。每个结构体仅包含一个方法,该方法对应其唯一的职责:

  • SportsOrganizer 结构体包含 OrganizeSportsMeet() 方法,负责组织运动会。
  • StudyManager 结构体包含 ManageStudentGrades() 方法,负责管理学生成绩。
  • DailyAffairsCoordinator 结构体包含 ArrangeDailyChores() 方法,负责安排每日值日生。
  • CateringOfficer 结构体包含 HandleLunchOrders() 方法,负责处理午餐订餐。

这些结构体都遵循单一职责原则,每个结构体只承担一个特定的任务,没有多余的职责。

主程序:使用遵循单一职责的类进行协作:在 main 函数中,我们创建了四个角色对应的实例,并分别调用它们的方法,让每个角色执行其唯一的职责。这样,尽管每个角色(类)的功能简单,但通过组合这些角色,可以完成整个班级的管理工作,而且各个角色之间的职责边界清晰,互不影响。

通过上述代码示例,我们展示了如何在 Go 语言中实现单一职责原则。每个结构体(类)专注于一项特定职责,只做一件事并把它做好。当需要修改某个功能时,如修改运动会的组织规则,只需要修改 SportsOrganizer 类,不会影响其他类(如 StudyManagerCateringOfficer)。这种设计使得代码更易于理解和维护,降低了因一处改动引发其他功能错误的风险,符合单一职责原则的精神。

开闭原则(OCP)

一句话概括

软件实体应对于扩展开放,但对于修改关闭。意味着增加新功能时无需更改现有代码,而是通过扩展来实现。

案例理解

开闭原则就像是学校图书馆的扩建。原本的图书馆已经建好了,里面有很多书架、阅览桌椅以及借阅系统。现在学校决定要增加一个电子阅读区,让学生们也能在线阅读电子书。

按照开闭原则,我们不能直接去拆掉现有的图书馆,把墙砸了扩大空间,或者改动原有的布局来挤出地方放电子设备,这样做会破坏图书馆正常运行,影响同学们借阅纸质书。这就好比修改现有的代码,可能会引发未知错误,让整个软件系统不稳定。

正确的做法是,我们在图书馆旁边新建一座专门的电子阅读楼,或者在图书馆原有建筑的基础上加建一层,用于放置电子设备和提供舒适的电子阅读环境。这样,原有的图书馆可以继续照常运作,新来的同学可以去电子阅读区享受数字化阅读,两者互不影响,这就是“对修改关闭”。

同时,这个新区域虽然与原来的图书馆分开建造,但它们可以通过连廊或者内部通道相连,使得读者可以方便地在纸质书和电子书之间切换,图书馆的整体服务得到了扩展,没有因为增加了电子阅读功能而打乱原有的秩序。这就是“对扩展开放”。

对应到编程中,开闭原则就是说,当我们想要给软件添加新功能(比如增加电子阅读功能)时,不应该直接去修改已经写好的、正在正常运行的代码(就像不去改动原有的图书馆)。而是应该编写新的代码(相当于新建电子阅读楼或加层),并通过一种巧妙的方式(比如接口、继承或委托)将新旧代码连接起来,让整个软件系统能够无缝地使用新功能,用户感觉不到任何中断或混乱。这样,既保持了软件的稳定性(对修改关闭),又实现了功能的扩展(对扩展开放),就像扩建后的图书馆既保留了原有的纸质书服务,又增添了电子阅读服务,而且两部分和谐共存。

代码示例

结合上面的案例理解,使用 Go 语言实现开闭原则(OCP)的代码示例

package main

import (
	"fmt"
)

// Step 1: 定义图书接口,作为扩展的基础

type Book interface {
	GetTitle() string
	GetType() string // 新增接口方法,用于区分纸质书和电子书
	Borrow()
	Return()
}

// Step 2: 原有纸质图书结构体,实现图书接口

type PaperBook struct {
	title string
}

func (pb PaperBook) GetTitle() string {
	return pb.title
}

func (pb PaperBook) GetType() string {
	return "Paper"
}

func (pb PaperBook) Borrow() {
	fmt.Printf("Borrowed paper book: %s\n", pb.title)
}

func (pb PaperBook) Return() {
	fmt.Printf("Returned paper book: %s\n", pb.title)
}

// Step 3: 扩展电子图书结构体,实现图书接口

type EBook struct {
	title string
}

func (eb EBook) GetTitle() string {
	return eb.title
}

func (eb EBook) GetType() string {
	return "EBook"
}

func (eb EBook) Borrow() {
	fmt.Printf("Borrowed e-book: %s\n", eb.title)
}

func (eb EBook) Return() {
	fmt.Printf("Returned e-book: %s\n", eb.title)
}

// Step 4: 图书管理系统的操作,依赖图书接口而非具体类型

type LibrarySystem struct {
	books []Book
}

func (ls *LibrarySystem) AddBook(book Book) {
	ls.books = append(ls.books, book)
}

func (ls *LibrarySystem) BorrowBook(title string) {
	for _, book := range ls.books {
		if book.GetTitle() == title {
			book.Borrow()
			return
		}
	}
	fmt.Printf("Book '%s' not found.\n", title)
}

func (ls *LibrarySystem) ReturnBook(title string) {
	for _, book := range ls.books {
		if book.GetTitle() == title {
			book.Return()
			return
		}
	}
	fmt.Printf("Book '%s' not found.\n", title)
}

// Step 5: 主程序:使用图书管理系统进行操作

func main() {
	libSys := &LibrarySystem{}

	// 添加纸质图书
	pb := PaperBook{title: "The Old Man and the Sea"}
	libSys.AddBook(pb)

	// 添加电子图书(扩展功能)
	eb := EBook{title: "Digital Fortress"}
	libSys.AddBook(eb)

	// 借阅和归还图书,不区分纸质书还是电子书
	libSys.BorrowBook("The Old Man and the Sea")
	libSys.ReturnBook("The Old Man and the Sea")

	libSys.BorrowBook("Digital Fortress")
	libSys.ReturnBook("Digital Fortress")
}
代码说明

图片

定义图书接口:首先,我们定义了一个 Book 接口,包含了获取书名、书籍类型、借阅和归还等基本操作。这个接口将成为后续扩展的基础,确保所有类型的图书都能以统一的方式进行管理。

原有纸质图书结构体:接着,我们创建了 PaperBook 结构体,实现了 Book 接口。这里代表了原有的图书管理系统功能,仅支持纸质书的借阅和归还。

扩展电子图书结构体:为了增加电子书的支持,我们创建了 EBook 结构体,同样实现 Book 接口。这是对系统功能的扩展,新添加了电子书的管理逻辑,但并未触及原有纸质图书的代码。

图书管理系统操作LibrarySystem 结构体负责图书的管理和操作。它包含一个 Book 接口类型的切片,用于存储各种类型的图书。其方法(如 AddBookBorrowBookReturnBook)仅依赖于 Book 接口,不关心具体的图书类型。这样设计使得系统可以透明地处理纸质书和电子书,实现了对扩展的开放。

主程序:在 main 函数中,我们创建了一个 LibrarySystem 实例,分别添加了纸质书和电子书。然后通过图书管理系统进行借阅和归还操作,无论是对原有的纸质书还是新增的电子书,系统都能正确处理,且无需修改原有代码来适应新功能。

通过以上代码示例,我们展示了如何在 Go 语言中实现开闭原则。在扩展图书管理系统以支持电子书功能时,我们没有修改原有纸质图书相关的任何代码,而是通过创建新的 EBook 结构体来扩展系统功能,并保持了与原有系统逻辑的解耦。图书管理系统通过对接口的依赖实现了对不同图书类型的透明管理,确保了在增加新功能时对原有代码的修改封闭,同时也实现了对扩展的开放。这样的设计确保了软件的稳定性,并使其易于随着需求变化进行扩展。

里氏替换原则(LSP)

一句话概括

子类型必须能够替换掉它们的基类型,确保继承体系的可替代性和正确性。

案例理解

里氏替换原则(LSP)就像是学校里不同年级的学生都可以参加学校的足球比赛。假设学校规定,只要是学生,无论年级高低,都可以报名参加校队选拔。这是因为学校相信,尽管低年级学生年纪小一些,但他们经过训练,同样能遵守比赛规则,执行好前锋、后卫、守门员等角色的任务,与高年级学生一起组成一支有效的球队。

在这个比喻中,“学生”就像是一个基类型,而“低年级学生”和“高年级学生”则是它的子类型。学校举办足球比赛的要求是“只要是学生就可以”,这就是“基类型可以出现的地方”。当低年级学生报名并成功入选校队时,他们实际上就是在“替换”基类型(学生)的位置,参与到了足球比赛中。如果他们能够胜任比赛任务,不违反比赛规则,那么我们就说低年级学生(子类型)成功地“替换”了学生(基类型)。

回到编程世界,里氏替换原则就是说,如果你有一个父类(基类型),比如一个叫做“图形”的类,它有一些基本的方法,比如“绘制”和“移动”。然后你创建了一些子类(派生类),比如“圆形”和“矩形”,它们都是图形的一种,继承了“图形”的属性和方法。根据里氏替换原则,你应该能够用任何一个子类对象去替换掉父类对象,而不影响程序的正常运行。也就是说,不论程序原本期待的是一个“图形”,还是具体某个子类(圆形或矩形),只要调用“绘制”和“移动”这些方法,都应该能得到正确的结果。

总结来说,里氏替换原则就像是学校足球队选拔的标准:只要是学生,无论年级高低,都能参加比赛。在编程中,就是只要是某个基类的子类,不管具体类型是什么,都能在任何需要基类的地方正常使用,就如同它们是基类本身一样。这样设计的好处是,代码更加灵活,容易扩展,当你添加新的子类时,不需要修改大量已有的代码,只需要确保新子类正确地实现了基类定义的行为即可。

代码示例

结合上面的案例理解,使用 Go 语言实现里氏替换原则(LSP)的代码示例

package main

import (
	"fmt"
)

// Student 基类型(接口),代表所有学生
type Student interface {
	Train()   // 训练方法,模拟学生进行足球训练
	PlayRole(role string) // 扮演角色方法,模拟学生在比赛中担任指定角色(前锋、后卫、守门员等)
}

// JuniorStudent 子类型,代表低年级学生
type JuniorStudent struct{}

// Train 实现 Student 接口的 Train 方法
func (js JuniorStudent) Train() {
	fmt.Println("低年级学生正在进行足球训练...")
}

// PlayRole 实现 Student 接口的 PlayRole 方法
func (js JuniorStudent) PlayRole(role string) {
	fmt.Printf("低年级学生正在担任 %s 角色...\n", role)
}

// SeniorStudent 子类型,代表高年级学生
type SeniorStudent struct{}

// Train 实现 Student 接口的 Train 方法
func (ss SeniorStudent) Train() {
	fmt.Println("高年级学生正在进行足球训练...")
}

// PlayRole 实现 Student 接口的 PlayRole 方法
func (ss SeniorStudent) PlayRole(role string) {
	fmt.Printf("高年级学生正在担任 %s 角色...\n", role)
}

// SchoolFootballTeam 校足球队结构体,包含一个 Student 类型的成员,用于模拟球队选拔过程
type SchoolFootballTeam struct {
	Captain Student
}

// SelectPlayer 选拔球员方法,模拟从学生中选择一名球员加入校队
func (sft *SchoolFootballTeam) SelectPlayer(student Student) {
	sft.Captain = student
}

// ConductTraining 进行训练方法,模拟球队进行训练
func (sft SchoolFootballTeam) ConductTraining() {
	sft.Captain.Train()
}

// OrganizeMatch 组织比赛方法,模拟安排学生担任比赛角色并进行比赛
func (sft SchoolFootballTeam) OrganizeMatch() {
	sft.Captain.PlayRole("前锋")
	sft.Captain.PlayRole("后卫")
	sft.Captain.PlayRole("守门员")
}

func main() {
	// 创建校足球队
	team := &SchoolFootballTeam{}

	// 选拔低年级学生加入校队
	junior := JuniorStudent{}
	team.SelectPlayer(junior)

	// 进行训练和比赛
	team.ConductTraining()
	team.OrganizeMatch()

	// 重新选拔高年级学生加入校队
	senior := SeniorStudent{}
	team.SelectPlayer(senior)

	// 再次进行训练和比赛
	team.ConductTraining()
	team.OrganizeMatch()
}
代码说明

图片

Student 接口:定义了所有学生共有的行为,即Train()(训练)和PlayRole()(扮演角色)。这是我们的基类型,代表“学生”这个抽象概念。

JuniorStudent 结构体SeniorStudent 结构体:分别代表低年级学生和高年级学生,它们都是Student接口的实现。每个结构体都实现了Train()PlayRole()方法,表明低年级学生和高年级学生都能进行足球训练,并在比赛中担任各种角色。

SchoolFootballTeam 结构体:模拟学校足球队,包含一个Student类型的成员Captain,用于存储当前校队队长。这个结构体定义了SelectPlayer()(选拔球员)、ConductTraining()(进行训练)和OrganizeMatch()(组织比赛)三个方法。

main 函数:创建校足球队实例,先选拔低年级学生加入校队,进行训练和比赛;然后更换为高年级学生,再次进行训练和比赛。无论选择哪个年级的学生,训练和比赛过程均能顺利进行,体现了里氏替换原则。

通过这个示例,我们可以看到:

  • 子类型替换基类型:无论是低年级学生还是高年级学生,都能被选为校队队长(Captain),即在需要Student类型的地方,可以用任意子类型替换。
  • 不影响程序正常运行:无论是进行训练还是组织比赛,无论选用低年级学生还是高年级学生,程序都能正确执行相关方法,得到预期结果。
  • 代码灵活性和扩展性:如果未来新增其他年级的学生类型(如中级学生),只要它们实现Student接口,就可以无缝地加入到现有系统中,无需修改SchoolFootballTeam或其他已有代码,只需确保新子类正确地实现了基类定义的行为。

示例中,遵循了里氏替换原则,模拟了学校足球队选拔标准,展示了子类型(低年级学生、高年级学生)能够替换基类型(学生),并在需要学生的地方正常使用,保持了代码的可替代性和正确性。

接口隔离原则(ISP)

一句话概括

客户端不应该被迫依赖于他们不使用的接口方法,提倡将大型接口分解为多个更小和更具体的接口。

案例理解

想象一下,你在学校里负责组织一场文艺汇演,需要请同学们帮忙准备各项事务。一开始,你给每个同学发了一份长长的清单,上面写着各种任务,比如布置舞台、调试音响、编写剧本、排练舞蹈、制作海报、联系赞助商等等。这份清单就像一个包含了所有可能任务的“大接口”。

现在,假设小明同学非常擅长画画,对制作海报这项任务特别拿手,但他对调试音响这类技术活儿一窍不通。小红同学热爱表演,跳舞、演戏样样精通,但对联系赞助商这样的交际工作不太擅长。如果按照那份包含所有任务的“大接口”来分配工作,小明和小红就得被迫接受自己并不擅长甚至完全不会做的任务。

**接口隔离原则(ISP)**就像是解决这个问题的指导思想。它告诉我们,不应该让同学们承担他们根本用不到或者做不了的任务。换句话说,每个同学只应该拿到他们能够并且应该完成的任务清单

具体来说,我们把原来那份包含所有任务的“大接口”拆分成几个更小、更具体的“接口”:

  • 舞台布置接口:只包括与舞台搭建、灯光设置等相关的工作。
  • 音响调试接口:专注于音响设备的检查、调音等工作。
  • 剧本创作接口:负责构思剧情、撰写剧本。
  • 舞蹈排练接口:涵盖舞蹈编排、练习等任务。
  • 海报制作接口:专注设计和制作演出海报。
  • 赞助联络接口:涉及寻找赞助商、洽谈合作事宜。

现在,你可以根据每个同学的特长和兴趣,给他们分配合适的“接口”。小明拿到的是“海报制作接口”,他只需要关注如何设计出吸引人的海报。小红则拿到“舞蹈排练接口”和“剧本创作接口”,她可以专心致志地排练舞蹈和编写剧本。这样,每个同学都只负责自己擅长且愿意做的事情,避免了被迫接受不擅长的任务。

在计算机编程中,接口就像是任务清单,定义了一组方法(即任务)。一个类实现了接口,就意味着它承诺会执行接口中定义的所有方法。接口隔离原则就是建议我们不要创建包含大量方法的大接口,而是应该创建多个小而精的接口,每个接口只包含一组相关的方法。这样,不同的类可以根据自己的能力和职责,选择实现合适的接口,而不是被迫实现包含许多它们并不关心或无法有效处理的方法的大接口。

总结来说,接口隔离原则(ISP)就像在学校活动中给同学们分配适合他们能力和兴趣的特定任务,而不是让他们面对一份包含所有任务的冗长清单。在编程中,这意味着设计小巧、专注的接口,让类只依赖和实现它们真正需要用到的方法,避免不必要的复杂性和潜在的问题。这样既能让代码结构清晰,易于理解和维护,也能提高代码的复用性和系统的灵活性。

代码示例

结合上面的案例理解,使用 Go 语言实现接口隔离原则(ISP)的代码示例

package main

import (
	"fmt"
)

// 定义一个大型接口 SuperWorker,包含多种才艺各异的同学可能具备的技能
// 这个接口违背了ISP原则,因为它强迫任何实现它的类型必须实现所有方法,即使某个工人可能并不具备某些技能
type SuperWorker interface {
	SetUpStage() error
	TuneSoundSystem() error
	WriteScript() (string, error)
	ChoreographDance() error
	CreatePoster() (*Poster, error) // 假设Poster是已定义的海报结构体类型
	ContactSponsors() ([]SponsorAgreement, error) // 假设SponsorAgreement是已定义的赞助协议结构体类型
}

// 定义一系列专注于特定任务的小接口

// StageSetupInterface 舞台布置接口
type StageSetupInterface interface {
	SetUpStage() error
}

// SoundSystemInterface 音响调试接口
type SoundSystemInterface interface {
	TuneSoundSystem() error
}

// ScriptWriterInterface 剧本创作接口
type ScriptWriterInterface interface {
	WriteScript() (string, error)
}

// ChoreographerInterface 舞蹈排练接口
type ChoreographerInterface interface {
	ChoreographDance() error
}

// PosterDesignerInterface 海报制作接口
type PosterDesignerInterface interface {
	CreatePoster() (*Poster, error) // 假设Poster是已定义的海报结构体类型
}

// SponsorLiaisonInterface 赞助联络接口
type SponsorLiaisonInterface interface {
	ContactSponsors() ([]SponsorAgreement, error) // 假设SponsorAgreement是已定义的赞助协议结构体类型
}

// 同学们根据各自的特长实现相应的接口

// XiaoMing 小明,擅长制作海报
type XiaoMing struct{}

func (xm XiaoMing) CreatePoster() (*Poster, error) {
	// 实现制作海报的逻辑
	return &Poster{Title: "文艺汇演", Design: "精美插画风格"}, nil
}

// XiaoHong 小红,热爱表演,擅长舞蹈和剧本创作
type XiaoHong struct{}

func (xh XiaoHong) WriteScript() (string, error) {
	// 实现编写剧本的逻辑
	return "精彩剧本内容", nil
}

func (xh XiaoHong) ChoreographDance() error {
	// 实现舞蹈编排的逻辑
	return nil
}

func main() {
	// 根据同学们的特长分配任务
	xiaoMing := XiaoMing{}
	xiaoHong := XiaoHong{}

	// 小明专注于海报制作
	poster, err := xiaoMing.CreatePoster()
	if err != nil {
		fmt.Println("小明制作海报时遇到问题:", err)
	} else {
		fmt.Printf("小明成功制作出海报: %v\n", poster)
	}

	// 小红专注于剧本创作和舞蹈排练
	script, err := xiaoHong.WriteScript()
	if err != nil {
		fmt.Println("小红编写剧本时遇到问题:", err)
	} else {
		fmt.Printf("小红成功编写剧本: %s\n", script)
	}

	err = xiaoHong.ChoreographDance()
	if err != nil {
		fmt.Println("小红排练舞蹈时遇到问题:", err)
	} else {
		fmt.Println("小红成功排练舞蹈")
	}
}

// 以下为示例中假设存在的 Poster 和 SponsorAgreement 结构体定义,实际代码中需自行实现
type Poster struct {
	Title   string
	Design  string
	Artist  string
	Preview []byte // 海报预览图片数据
}

type SponsorAgreement struct {
	SponsorName    string
	AgreementTerms map[string]string // 赞助条款
	ContactPerson  string
}
代码说明

图片

首先,我们定义了一系列小接口,如StageSetupInterfaceSoundSystemInterface等,每个接口仅包含与其特定任务相关的单个方法。这对应于将原先包含所有任务的“大接口”拆分成多个小而精的“接口”。

接着,我们创建了XiaoMingXiaoHong两个结构体类型,分别代表小明和小红。他们根据自身的特长实现了对应的接口方法。小明实现了PosterDesignerInterface接口,专注于制作海报;小红实现了ScriptWriterInterfaceChoreographerInterface接口,负责剧本创作和舞蹈排练。

main函数中,我们实例化了小明和小红,并根据他们实现的接口调用相应的方法。小明只负责CreatePoster,小红则负责WriteScriptChoreographDance。这样,每个同学都只执行自己擅长且应该完成的任务,符合接口隔离原则。

通过以上代码示例,我们展示了如何在Go语言中运用接口隔离原则(ISP),将复杂的职责分解为一系列专注且独立的接口,并让不同的类(这里为同学们的角色)根据自身能力实现相应的接口,从而实现职责的合理分配和代码的清晰组织。这样的设计有助于提高代码的可读性、可维护性和系统的灵活性。

依赖倒置原则(DIP)

一句话概括

高层模块不应该依赖低层模块,二者都应该依赖于抽象。抽象不应依赖细节,而细节应该依赖于抽象。

案例理解

对于刚入门的学习设计模式、学习架构的人来说,这个依赖倒置原则说得就挺抽象的,要学好、运用好更加难。所以通过这个案例理解,相信你会有全新的理解。

故事比喻

想象你正在经营一家甜品店。甜品店有很多部门,比如采购部负责购买原材料,制作部负责烘烤蛋糕,销售部负责向顾客售卖。每个部门就像一个“模块”,有的在“高层”(比如销售部,直接面对顾客),有的在“低层”(如采购部,处理基础物资)。为了让大家协同工作,甜品店有一本《操作手册》,上面写明了各部门的工作流程和协作规则,这就好比“抽象”。

高层模块与低层模块

“高层模块”就像是销售部。他们直接与顾客打交道,决定了店铺的对外形象和销售额。他们不需要知道采购部是如何找到优质面粉供应商的,也不必了解制作部具体如何调整烤箱温度来烤出完美蛋糕。他们只需要知道:当顾客下单时,制作部会准备好蛋糕,采购部能确保原料充足。

“低层模块”则是采购部和制作部。他们专注于执行具体的任务,像是采购原材料、烘焙蛋糕。尽管他们的工作直接影响着甜品店的运营,但他们并不直接面对顾客,而是通过遵循《操作手册》与销售部对接。

依赖于抽象

现在,我们说“高层模块不应该依赖低层模块,二者都应该依赖于抽象”。这句话的意思是:

  • 销售部(高层模块)不应该直接指挥采购部去哪家店买面粉、制作部怎么烤蛋糕(依赖低层模块的细节)。相反,销售部应该依据《操作手册》的规定,比如“下单后2小时内蛋糕应准备完毕”、“每月初应提供下月所需原材料清单”(依赖抽象规则)来协调工作。
  • 同样,采购部和制作部(低层模块)也不应直接询问销售部明天会有多少顾客、需要准备什么口味的蛋糕(依赖高层模块的细节)。他们应该根据《操作手册》中诸如“收到销售部的订单后立即开始备料”、“按照指定口味和数量制作蛋糕”的指导来行动(依赖抽象规则)。

抽象不应依赖细节,细节应该依赖抽象

“抽象不应依赖细节”意味着,《操作手册》不应该详细写明采购部必须从某家特定商店购买面粉,或者制作部必须使用某个特定品牌的烤箱。这样的规定过于具体,一旦实际情况变化(比如供应商断货、烤箱故障),整个手册就需要修改,影响全局。

“细节应该依赖抽象”则是说,采购部在寻找面粉供应商时,应遵循《操作手册》中“选择质量可靠、价格合理的供应商”的原则,而不是死板地按照某一固定品牌或商店来采购。同样,制作部在调整烤箱温度时,应遵循“根据蛋糕配方和烤箱特性设定适宜温度”的指导,而不是拘泥于某个特定数值。

总结起来

依赖倒置原则就像甜品店各部门遵循《操作手册》协作一样:

  • 高层模块(如销售部)和低层模块(如采购部、制作部)都不直接依赖彼此的具体工作细节,而是依赖共同遵守的抽象规则(《操作手册》)。
  • 抽象规则(《操作手册》)不依赖任何特定的低层细节(如特定供应商、烤箱型号),保持灵活和通用。
  • 低层模块的具体工作(采购、烘焙)则应依据抽象规则来进行,确保整体运作有序且适应变化。

在软件开发中,这个原则指导我们编写代码时,尽量让不同部分依赖于稳定的接口(抽象)而非具体的实现(细节),使得系统更易于维护、扩展和适应变化。

代码示例

结合上面的案例理解,使用 Go 语言实现依赖倒置原则(DIP)的代码示例

package main

import (
	"fmt"
)

// Step 1: 定义抽象(接口)

// SweetRepository 是一个接口,代表甜品仓库的抽象行为。它定义了高层模块(如 SalesDepartment)期望的与甜品仓库交互的方法。
type SweetRepository interface {
	GetSweet(name string) (*Sweet, error)
	AddSweet(sweet *Sweet) error
}

// Sweet 是一个简单的甜品结构体,用于演示。
type Sweet struct {
	Name    string
	Flavor  string
	Recipe  string
}

// Step 2: 定义低层模块(实现接口)

// PurchaseDepartment 是 SweetRepository 接口的一个实现,它负责采购甜品原材料。
type PurchaseDepartment struct {
	ingredients map[string]string
}

func (pd *PurchaseDepartment) GetSweet(name string) (*Sweet, error) {
	// 假设这里从库存中获取甜品及其原材料信息
	sweet := &Sweet{Name: name, Flavor: "Chocolate", Recipe: "Ingredients: Cocoa powder, sugar, etc."}
	return sweet, nil
}

func (pd *PurchaseDepartment) AddSweet(sweet *Sweet) error {
	// 假设这里处理甜品入库操作
	return nil
}

// BakingDepartment 是 SweetRepository 接口的另一个实现,它负责烘烤甜品。
type BakingDepartment struct {
	bakedGoods map[string]string
}

func (bd *BakingDepartment) GetSweet(name string) (*Sweet, error) {
	// 假设这里从烘培成品中获取甜品信息
	sweet := &Sweet{Name: name, Flavor: "Vanilla", Recipe: "Ingredients: Vanilla extract, flour, etc."}
	return sweet, nil
}

func (bd *BakingDepartment) AddSweet(sweet *Sweet) error {
	// 假设这里处理甜品入库操作
	return nil
}

// Step 3: 定义高层模块(依赖抽象)

// SalesDepartment 是甜品店的销售部门,它直接面对顾客,依赖 SweetRepository 抽象与甜品仓库交互。
type SalesDepartment struct {
	repository SweetRepository
}

func (sd *SalesDepartment) SellSweet(name string) error {
	// 通过 SweetRepository 抽象获取甜品信息
	sweet, err := sd.repository.GetSweet(name)
	if err != nil {
		return err
	}

	// 假设这里处理向顾客售卖甜品的操作
	fmt.Printf("Selling %s, flavor: %s, recipe: %s\n", sweet.Name, sweet.Flavor, sweet.Recipe)

	return nil
}

// Step 4: 主程序:高层模块与低层模块通过抽象协作

func main() {
	// 创建 SalesDepartment 对象,传入 SweetRepository 的实现(这里是 PurchaseDepartment)
	salesDept := &SalesDepartment{repository: &PurchaseDepartment{ingredients: make(map[string]string)}}

	// 销售部门通过 SweetRepository 抽象与采购部协作,无需关心采购部的具体实现细节
	err := salesDept.SellSweet("ChocoCake")
	if err != nil {
		fmt.Println("Error selling sweet:", err)
	}

	// 更改 SalesDepartment 对象的 SweetRepository 实现为 BakingDepartment
	salesDept.repository = &BakingDepartment{bakedGoods: make(map[string]string)}

	// 销售部门继续通过 SweetRepository 抽象与烘培部协作,无需更改任何代码,体现了依赖倒置原则
	err = salesDept.SellSweet("VanillaCake")
	if err != nil {
		fmt.Println("Error selling sweet:", err)
	}
}
代码说明

图片

定义抽象(接口):我们定义了一个名为 SweetRepository 的接口,它包含了 GetSweet()AddSweet() 方法,代表了甜品仓库应该具备的获取和添加甜品的能力。此接口充当了甜品店各部门之间协作的“抽象”。

定义低层模块(实现接口):创建了两个具体的结构体类型:PurchaseDepartmentBakingDepartment。它们各自实现了 SweetRepository 接口规定的 GetSweet()AddSweet() 方法。这两个类型通过实现接口方法,成为了 SweetRepository 接口的子类型,代表了甜品店的采购部和烘培部,属于“低层模块”。

定义高层模块(依赖抽象):编写了 SalesDepartment 结构体,它直接面对顾客,依赖 SweetRepository 接口与甜品仓库交互。通过这种方式,销售部无需关心甜品仓库的具体实现(采购部或烘培部),只需通过 SweetRepository 接口提供的方法进行操作。

主程序:高层模块与低层模块通过抽象协作:在 main 函数中,我们创建了一个 SalesDepartment 对象,先传入 PurchaseDepartment 作为其 repository,然后销售部门通过 SweetRepository 抽象与采购部协作。接着,我们更改 SalesDepartment 对象的 repositoryBakingDepartment,销售部门无需更改任何代码,依然通过 SweetRepository 抽象与烘培部协作。这一过程展示了高层模块(销售部)不依赖低层模块(采购部、烘培部),而是依赖于抽象(SweetRepository 接口),实现了依赖倒置原则。

这段代码通过定义接口和实现接口的结构体,模拟了甜品店各部门之间的协作,展示了在 Go 语言中如何实现依赖倒置原则。高层模块(销售部)依赖于抽象接口(SweetRepository),低层模块(采购部、烘培部)实现接口成为抽象的合法替换者。在实际编程中,通过依赖抽象接口,使得高层模块与低层模块解耦,易于更换实现、扩展功能和维护系统。

迪米特法则( LoD)

一句话概括

又名「最少知道原则」,一个类不应知道自己操作的类的细节。

案例理解

想象一下,你在一个大班级里,每个同学都是一个“类”,大家各自负责不同的任务。为了让大家能够和谐相处,高效合作,班级里有一条重要的社交原则:每个人只跟自己最亲近的朋友交流必要的信息,不打听其他同学太多私事

现在,我们把这个原则搬到计算机编程的世界里:

**迪米特法则(最少知道原则)**就像是班级里的这条社交规则,它告诉我们在设计程序的时候,每个“类”(相当于一个同学)应当尽量只了解和它直接打交道的“类”(好朋友)的必要信息,而不去探究那些跟它关系不那么紧密的“类”(陌生同学)的详细情况。

具体来说:

**“一个类不应知道自己操作的类的细节”**意味着,当你设计一个类(比如“学生会主席”类)时,它只需要知道哪些类(如“宣传委员”、“学习委员”)是它直接合作的对象,并清楚这些直接伙伴能提供什么服务(如“发布通知”、“组织学习活动”)。它不需要知道这些伙伴是如何完成这些服务的具体步骤(比如“宣传委员”如何制作海报、联系广播站等内部细节)。

**“最少知道”**意味着,每个类尽量保持“低调”,只和少数几个直接相关的类建立联系。这样做的好处是:

  • 减少依赖:类与类之间的关系简单明了,一个类改动时,受影响的范围较小,不容易牵一发动全身。
  • 易于理解和维护:每个类专注自己的职责,不关心其他类的内部事务,代码更清晰,新人也能更快上手。
  • 增强隐私保护:每个类的内部细节得到保护,不易被无关的类误用或干扰,提高了系统的稳定性和安全性。

举个例子,假设我们要设计一个“图书馆管理系统”。按照迪米特法则:

  • “借阅服务”类只需知道“读者”类提供“借书”和“还书”功能,以及“图书库存”类提供查询和更新库存的方法。它不需要知道“读者”如何保管借阅卡,也不必了解“图书库存”如何物理存放书籍。
  • “读者”类只需要关心自己的借阅记录和罚款情况,不必知道图书馆是如何进行图书分类、采购新书等内部运作。

代码示例

结合上面的案例理解,使用 Go 语言实现迪米特法则( LoD)的代码示例

由于编写一个完整的图书馆管理系统涉及多个类和大量的代码,这里我将简化示例,仅展示如何按照迪米特法则设计主要的三个类:Reader(读者)、BookInventory(图书库存)和BorrowingService(借阅服务)

package main

import (
	"fmt"
)

// Reader 代表一个读者,包含姓名和借阅记录
type Reader struct {
	Name          string
	BorrowedBooks map[string]bool // 以书名作为键,借阅状态为值(true表示已借阅)
}

// BorrowBook 方法允许读者借阅一本书,如果图书库存中有这本书且读者未借阅,则成功借阅并更新状态
func (r *Reader) BorrowBook(bookName string, inventory BookInventory) error {
	if !inventory.HasBook(bookName) {
		return fmt.Errorf("Book '%s' not found in inventory", bookName)
	}
	if r.IsBookBorrowed(bookName) {
		return fmt.Errorf("Reader '%s' has already borrowed the book '%s'", r.Name, bookName)
	}
	r.BorrowedBooks[bookName] = true
	return nil
}

// ReturnBook 方法允许读者归还一本书,如果读者已借阅该书,则成功归还并更新状态
func (r *Reader) ReturnBook(bookName string) error {
	if !r.IsBookBorrowed(bookName) {
		return fmt.Errorf("Reader '%s' has not borrowed the book '%s'", r.Name, bookName)
	}
	delete(r.BorrowedBooks, bookName)
	return nil
}

// IsBookBorrowed 查询读者是否已借阅指定的书
func (r *Reader) IsBookBorrowed(bookName string) bool {
	return r.BorrowedBooks[bookName]
}

// BookInventory 代表图书库存,包含所有图书及其数量
type BookInventory struct {
	Books map[string]int // 以书名作为键,库存数量为值
}

// HasBook 查询库存中是否有指定的书
func (i *BookInventory) HasBook(bookName string) bool {
	_, exists := i.Books[bookName]
	return exists
}

// DecreaseBookCount 减少指定书的库存数量,若库存不足则返回错误
func (i *BookInventory) DecreaseBookCount(bookName string) error {
	count, exists := i.Books[bookName]
	if !exists || count <= 0 {
		return fmt.Errorf("Book '%s' not available in inventory", bookName)
	}
	i.Books[bookName] = count - 1
	return nil
}

// IncreaseBookCount 增加指定书的库存数量
func (i *BookInventory) IncreaseBookCount(bookName string) {
	i.Books[bookName]++
}

// BorrowingService 提供借阅服务,负责协调读者和图书库存的操作
type BorrowingService struct{}

// BorrowBook 为读者借阅一本书,处理借阅流程并更新相关状态
func (s *BorrowingService) BorrowBook(reader *Reader, bookName string, inventory BookInventory) error {
	err := reader.BorrowBook(bookName, inventory)
	if err != nil {
		return err
	}
	err = inventory.DecreaseBookCount(bookName)
	if err != nil {
		reader.ReturnBook(bookName) // 如果库存扣减失败,回滚借阅状态
		return err
	}
	return nil
}

// ReturnBook 为读者归还一本书,处理归还流程并更新相关状态
func (s *BorrowingService) ReturnBook(reader *Reader, bookName string, inventory BookInventory) error {
	err := reader.ReturnBook(bookName)
	if err != nil {
		return err
	}
	inventory.IncreaseBookCount(bookName)
	return nil
}

func main() {
	reader := &Reader{Name: "Alice", BorrowedBooks: make(map[string]bool)}
	inventory := &BookInventory{Books: map[string]int{"Book1": 1, "Book2": 2}}

	// 示例借阅和归还操作
	err := BorrowingService{}.BorrowBook(reader, "Book1", *inventory)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Printf("Reader '%s' successfully borrowed 'Book1'\n", reader.Name)
	}

	err = BorrowingService{}.ReturnBook(reader, "Book1", *inventory)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Printf("Reader '%s' successfully returned 'Book1'\n", reader.Name)
	}
}
代码说明

图片

Reader 类代表读者,包含姓名和借阅记录。它有两个方法:

  • BorrowBook:允许读者借阅一本书,前提是图书库存中有这本书且读者未借阅。此方法仅关心读者自身借阅状态的更新,不直接操作图书库存。
  • ReturnBook:允许读者归还一本书,前提是读者已借阅该书。同样,此方法仅更新读者自身借阅状态。

BookInventory 类代表图书库存,包含所有图书及其数量。它有三个方法:

  • HasBook:查询库存中是否有指定的书,仅返回存在与否的信息,不暴露库存数量。
  • DecreaseBookCount:减少指定书的库存数量,用于借阅操作。此方法内部处理库存扣减逻辑,对外部隐藏具体实现。
  • IncreaseBookCount:增加指定书的库存数量,用于归还操作。

BorrowingService 类提供借阅服务,负责协调读者和图书库存的操作。它有两个方法:

  • BorrowBook:为读者借阅一本书,首先调用读者的 BorrowBook 方法更新借阅状态,然后调用库存的 DecreaseBookCount 方法扣减库存。如果库存扣减失败,会回滚借阅状态。
  • ReturnBook:为读者归还一本书,类似地,先更新读者的借阅状态,再增加库存数量。

main 函数中创建了一个读者实例、一个图书库存实例,并演示了借阅和归还图书的过程。

以上代码遵循了迪米特法则,每个类仅与其直接相关的类交互,不关心其他类的内部实现细节。例如,Reader 类只知道图书库存是否有某本书,不知道库存的具体数量;BorrowingService 类协调读者和库存的操作,但并不直接访问或修改读者的借阅记录或库存的图书数量。这样的设计使得代码模块化、职责分明,易于理解和维护。

电商案例

一句话概括

设计组件时使其内部紧密相关,外部关系尽量独立,以提高复用性和降低相互影响的风险。

案例理解

想象一下,你要组织一场班级聚会,需要安排各种活动和分工。为了让大家玩得开心又省心,你遵循了“高内聚低耦合”的原则来规划活动。

高内聚:高内聚就像组建一个个“活动小组”,每个小组内部成员的任务紧密相关,共同完成一项活动。比如,组建一个“游戏策划小组”,负责挑选游戏、准备道具、讲解规则,所有工作都围绕“组织游戏”这一核心任务进行。再比如,成立一个“美食筹备小组”,负责订购零食、饮料,甚至动手制作甜品,他们的工作都是为了让大家在聚会时享用美食。

在软件架构设计中,高内聚指的是一个组件(或模块、类)内部的各个部分应当紧密关联,共同完成一项明确的任务。例如,一个“用户管理模块”可能包含用户注册、登录、信息修改等功能,这些功能都是围绕用户数据的处理展开,形成了高内聚的结构。这样设计的好处是,当需要修改或扩展与用户管理相关的内容时,我们只需关注这个模块内部,而不会影响到其他无关的部分。

低耦合:低耦合就像是让各个活动小组在完成各自任务时,尽量减少与其他小组的直接依赖和干扰。比如,“游戏策划小组”只需将游戏规则和所需道具告知大家,无需干涉“场地布置小组”如何装饰现场,也不必操心“美食筹备小组”如何准备食物。每个小组各司其职,独立完成任务,最后再合力呈现一场精彩的聚会。

在软件架构设计中,低耦合指的是不同组件(或模块、类)之间应尽量减少相互间的依赖和影响。例如,“用户管理模块”与“订单管理模块”虽然在业务上有关联(用户下单购买商品),但在设计时应尽量让它们独立工作。用户模块负责处理用户数据,订单模块负责处理订单数据,它们通过明确的接口(如API)进行必要的信息交换,而不是直接访问对方的内部细节。这样,当修改一个模块时,对另一个模块的影响较小,降低了出错的风险,也便于单独测试和维护。

总结高内聚低耦合原则就像是组织一场成功的班级聚会:每个活动小组内部任务紧密相关、高效协作(高内聚),而不同小组之间保持相对独立、减少相互干扰(低耦合)。在软件架构设计中,遵循这一原则可以使组件内部结构紧凑、功能明确(高内聚),同时降低组件之间的依赖关系,提高系统的灵活性、可维护性和复用性(低耦合)。这样一来,无论是修改现有功能,还是添加新功能,都能更轻松、更稳健地进行,就像组织一场又一场成功的班级聚会那样游刃有余。

概括高内聚低耦合

代码示例

结合上面的案例理解,使用 Go 语言实现高内聚低耦合的电商案例的代码示例

package main

import (
	"fmt"
)

// --- 用户管理模块 ---

// User 用户结构体,封装用户信息
type User struct {
	ID       int
	Name     string
	Email    string
	Password string
}

// UserService 用户服务接口,定义与用户管理相关的操作
type UserService interface {
	Register(name, email, password string) (*User, error)
	GetUserByEmail(email string) (*User, error)
	UpdateUserPassword(userID int, newPassword string) error
}

// ConcreteUserService 具体的用户服务实现,内部紧密相关,实现UserService接口
type ConcreteUserService struct {
	userRepository UserRepository // 引用UserRepository接口,降低耦合
}

// Register 实现UserService接口,注册新用户
func (us *ConcreteUserService) Register(name, email, password string) (*User, error) {
	// 注册逻辑...
}

// GetUserByEmail 实现UserService接口,根据邮箱获取用户信息
func (us *ConcreteUserService) GetUserByEmail(email string) (*User, error) {
	// 获取用户逻辑...
}

// UpdateUserPassword 实现UserService接口,更新用户密码
func (us *ConcreteUserService) UpdateUserPassword(userID int, newPassword string) error {
	// 更新密码逻辑...
}

// UserRepository 用户仓库接口,负责用户数据的持久化操作
type UserRepository interface {
	Create(user *User) error
	ReadByEmail(email string) (*User, error)
	UpdatePassword(userID int, newPassword string) error
}

// ConcreteUserRepository 具体的用户仓库实现,负责与数据库交互
type ConcreteUserRepository struct {
	db Database // 假设Database是数据库操作的接口
}

// Create 实现UserRepository接口,将用户数据保存到数据库
func (ur *ConcreteUserRepository) Create(user *User) error {
	// 数据库操作逻辑...
}

// ReadByEmail 实现UserRepository接口,从数据库查询用户信息
func (ur *ConcreteUserRepository) ReadByEmail(email string) (*User, error) {
	// 数据库查询逻辑...
}

// UpdatePassword 实现UserRepository接口,更新用户在数据库中的密码
func (ur *ConcreteUserRepository) UpdatePassword(userID int, newPassword string) error {
	// 数据库更新逻辑...
}

// --- 订单管理模块 ---

// Order 订单结构体,封装订单信息
type Order struct {
	ID         int
	UserID     int
	ProductID  int
	Quantity   int
	TotalPrice float64
}

// OrderService 订单服务接口,定义与订单管理相关的操作
type OrderService interface {
	CreateOrder(userID int, productID int, quantity int) (*Order, error)
	GetOrderByID(orderID int) (*Order, error)
	UpdateOrderStatus(orderID int, status string) error
}

// ConcreteOrderService 具体的订单服务实现,内部紧密相关,实现OrderService接口
type ConcreteOrderService struct {
	orderRepository OrderRepository // 引用OrderRepository接口,降低耦合
}

// CreateOrder 实现OrderService接口,创建新订单
func (os *ConcreteOrderService) CreateOrder(userID int, productID int, quantity int) (*Order, error) {
	// 创建订单逻辑...
}

// GetOrderByID 实现OrderService接口,根据ID获取订单信息
func (os *ConcreteOrderService) GetOrderByID(orderID int) (*Order, error) {
	// 获取订单逻辑...
}

// UpdateOrderStatus 实现OrderService接口,更新订单状态
func (os *ConcreteOrderService) UpdateOrderStatus(orderID int, status string) error {
	// 更新订单状态逻辑...
}

// OrderRepository 订单仓库接口,负责订单数据的持久化操作
type OrderRepository interface {
	Create(order *Order) error
	ReadByID(orderID int) (*Order, error)
	UpdateStatus(orderID int, status string) error
}

// ConcreteOrderRepository 具体的订单仓库实现,负责与数据库交互
type ConcreteOrderRepository struct {
	db Database // 假设Database是数据库操作的接口
}

// Create 实现OrderRepository接口,将订单数据保存到数据库
func (or *ConcreteOrderRepository) Create(order *Order) error {
	// 数据库操作逻辑...
}

// ReadByID 实现OrderRepository接口,从数据库查询订单信息
func (or *ConcreteOrderRepository) ReadByID(orderID int) (*Order, error) {
	// 数据库查询逻辑...
}

// UpdateStatus 实现OrderRepository接口,更新订单在数据库中的状态
func (or *ConcreteOrderRepository) UpdateStatus(orderID int, status string) error {
	// 数据库更新逻辑...
}

// --- 主程序 ---

func main() {
	// 初始化数据库连接、用户服务和订单服务
	// ...

	// 使用用户服务和订单服务进行操作
	// ...
}
代码说明

图片

高内聚

  • 用户管理模块:定义了UserService接口,封装了与用户管理相关的操作,如注册、查询和更新用户信息。具体实现ConcreteUserService内部紧密围绕这些操作,不涉及其他无关逻辑。此外,通过引入UserRepository接口来处理数据持久化,进一步细化职责,使得ConcreteUserService只需关注业务逻辑,而ConcreteUserRepository专注于与数据库交互。
  • 订单管理模块:类似地,定义了OrderService接口,封装了与订单管理相关的操作,如创建、查询和更新订单状态。ConcreteOrderService内部专注于这些操作的实现,通过引入OrderRepository接口处理数据持久化,保持了高内聚。

低耦合

  • 用户管理模块订单管理模块:两个模块分别定义了自己的服务接口(UserServiceOrderService)和仓库接口(UserRepositoryOrderRepository),它们之间没有直接依赖,各自独立完成任务。虽然业务上有关联(用户下单购买商品),但在设计上通过接口隔离,降低了耦合度。
  • 服务层数据访问层:在每个模块内部,服务层(如ConcreteUserServiceConcreteOrderService)与数据访问层(如ConcreteUserRepositoryConcreteOrderRepository)通过接口进行交互,服务层不直接访问数据库,而是通过调用仓库接口的方法来操作数据。这样,当数据库实现发生变化(如更换数据库类型、调整表结构)时,只需修改仓库的具体实现,不影响服务层的逻辑,实现了低耦合。

通过上述代码示例,我们展示了电商系统中“用户管理模块”与“订单管理模块”如何遵循高内聚低耦合原则进行设计。每个模块内部结构紧凑、功能明确(高内聚),而模块之间保持相对独立、减少相互干扰(低耦合),从而提高了系统的灵活性、可维护性和复用性。无论是修改现有功能,还是添加新功能,都能更轻松、更稳健地进行。

推荐一个免费学习网站https://itgogogo.cn,通过这里也能找到我
在这里插入图片描述
觉得有用,欢迎点赞、在看和关注,感谢🙏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

皇子谈技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值