在之前的文章中,我们探讨了如何在Go语言中使用组合模式来优化代码和降低耦合度:优化go代码,使用组合降低耦合度。 这个设计模式个人觉得比较实用,在工作中遇到一些比较复杂的场景也会使用到接口+组合的模式,但是今天遇到了一个问题,如果用组合时只显示了部分方法,在这些方法中互相调用,会出现什么结果呢?
首先定义一个任务接口和基础任务结构体:
type Task interface {
Start()
Stop()
}
type BaseTask struct {
ID int
Name string
}
func (t *BaseTask) Start() {
fmt.Println("BaseTask Start")
}
func (t *BaseTask) Stop() {
fmt.Println("BaseTask Stop")
}
再定义一个移动任务,把基础任务组合进去:
type MoveTask struct {
BaseTask
}
func NewMoveTask(id int, name string) *MoveTask {
return &MoveTask{
BaseTask: BaseTask{
ID: id,
Name: name,
},
}
}
func (t *MoveTask) Stop() {
fmt.Println("MoveTask Stop")
}
func TestTask(t *testing.T) {
moveTask := NewMoveTask(1, "MoveTask")
moveTask.Start()
moveTask.Stop()
}
大家肯定能猜到这里会输出什么,虽然MoveTask没有实现Start()方法,但因为MoveTask组合了匿名的BaseTask,因为BaseTask实现了Task接口,所以MoveTask也实现了Task接口的方法。而当调用Start()方法时,由于MoveTask没有实现Start()方法,所以会调用BaseTask的Start()方法。
BaseTask Start
MoveTask Stop
这在工作中确实能解决一些实际问题,比如Task可能会定义了多达10个方法,但每个任务并非是完全不同的,一些方法完全可以是相同的,那我们就可以写入BaseTask减少代码的重复编写,只需要关注于任务中特殊的方法即可。当然这也只是组合的好处之一,只是为了实现这一效果完全可以在接口中只声明有区别的方法,其他公共方法直接写成函数。
实际情况中成员方法可能会相互调用,我们也来测试一下:给Task增加了Print()方法,只在BaseTask中实现,然后在MoveTask的Stop()方法中调用Print(),很显然这是可行的。
type Task interface {
Start()
Stop()
Print(string)
}
func (t *BaseTask) Print(msg string) {
fmt.Println("BaseTask Print" + msg)
}
func (t *MoveTask) Stop() {
fmt.Println("MoveTask Stop")
t.Print("Stop")
}
// print
// BaseTask Start
// MoveTask Stop
// BaseTask PrintStop
在这种情况下,MoveTask的Stop()方法调用了BaseTask的Print()方法,因为没有在MoveTask中重写Print()方法。
再复杂一点,给MoveTask也实现了Print()方法,那么这时候还能调用到BaseTask的Print()方法吗?如果直接调用显然不行,因为此时MoveTask已经实现了Print()方法,所以会优先执行,如果想要调用BaseTask的Print()方法,需要显示调用t.BaseTask.Print()
func (t *MoveTask) Stop() {
fmt.Println("MoveTask Stop")
t.Print("Stop")
}
func (t *MoveTask) Print(msg string) {
fmt.Println("MoveTask Print" + msg)
}
// print
// BaseTask Start
// MoveTask Stop
// MoveTask PrintStop
func (t *MoveTask) Stop() {
fmt.Println("MoveTask Stop")
t.BaseTask.Print("Stop")
}
// print
// BaseTask Start
// MoveTask Stop
// aseTask PrintSto
所以如果是在重写的方法中调用组合对象的方法,只需要显式调用父类方法即可。那如果想要在未重写的方法中调用重写的方法呢?这就是我遇到的问题了。
type Task interface {
Start()
StartHandler()
Stop()
Print(string)
}
func (t *BaseTask) Start() {
fmt.Println("BaseTask Start")
t.StartHandler()
}
func (t *BaseTask) StartHandler() {
fmt.Println("BaseTask StartHandler")
}
type MoveTask struct {
BaseTask
}
func (t *MoveTask) StartHandler() {
fmt.Println("MoveTask StartHandler")
}
func TestTask(t *testing.T) {
moveTask := NewMoveTask(1, "MoveTask")
moveTask.Start()
}
// print
// BaseTask Start
// BaseTask StartHandler
结果出乎我的意料,如果MoveTask没有实现Start() 但是实现了StartHandler(),然后在BaseTask中调用StartHandler(),并不会执行MoveTask的StartHandler()。我甚至还拿这段代码分别询问了ChatGPT和Gemini 1.5Pro,它们都信誓旦旦地回复说会输出"MoveTask StartHandler"。直到我告诉它们输出结果,它们马上又打自己脸,说刚才是自己说错了。
其实我会有这个预期就是被所谓的"面向对象"编程思想给束缚了,我们上大学的时候都学过C++,没少做这种多态和覆盖的题,反复问你这种情况会输出什么,有的会执行父类的方法,有的会执行子类的方法,有的直接就会报错。在这个例子中我其实并不了解原理什么,我觉得是因为MoveTask压根没有Start方法,所以找到了匿名对象也就是BaseTask有这个方法,自然会执行BaseTask的Start方法,此时再去调用StartHandler方法时的主体是BaseTask,自然不会调用到MoveTask的StartHandler方法。那么如果就是想要执行MoveTask的StartHandler方法该怎么做呢?
type BaseTask struct {
SubClass Task
}
func (t *BaseTask) Start() {
fmt.Println("BaseTask Start")
t.SubClass.StartHandler()
}
func NewMoveTask(id int, name string) *MoveTask {
moveTask := &MoveTask{}
base := BaseTask{
SubClass: moveTask,
}
moveTask.BaseTask = base
return moveTask
}
// print
// BaseTask Start
// MoveTask StartHandler
聪明的你一定猜到了,给BaseTask加上一个子类对象SubClass,调用时同样显式调用SubClass的方法。目前我也是这样做达成了我想要的效果,最简单的做法就是MoveTask实现所有的方法,或者压根不用组合的方式,必须实现接口的所有方法。虽然通过增加子类引用的方式解决了方法调用的问题,但这样做是否存在潜在风险仍需进一步探讨。如果你有更好的解决方案或对本文有任何意见,欢迎在评论区交流讨论。