对不同类型的数据进行分组
重要的是要记住,在 Go 中,子类型或子类的概念真的不存在,这些设计模式应该被避免。
以下是不应该遵循或实施的反模式:
type Animal struct { Name string IsMammal bool }
动物类型被声明为一个基础类型,它试图定义所有动物共有的数据。我也尝试向动物提供一些常见的行为。
func (a *Animal) Speak() { fmt.Println("UGH!", "My name is", a.Name, ", it is", a.IsMammal, "I am a mammal") }
大多数动物都有以这种或那种方式说话的能力。然而,试图将这种常见的行为仅仅适用于一种动物并没有任何意义。在这一点上,我不知道这种动物发出什么声音,所以我写了 UGH
type Dog struct { Animal PackFactor int }
现在,真正的问题开始了。我试图用嵌入的方式使狗拥有动物的一切,而且更多。从表面上看,这似乎是可行的,但也会有问题。既然如此,狗确实有一种特殊的说话方式
func (d *Dog) Speak() { fmt.Println("Woof!", "My name is", d.Name, ", it is", d.IsMammal, "I am a mammal with a pack factor of", d.PackFactor) }
在说话方法的实现中,我可以把 UGH 换成 Woof 。这就是 具体到狗的说话方式
type Cat struct { Animal ClimbFactor int }
如果我打算养一只代表动物的狗,那么我就必须养一只猫。使用嵌入法,猫是动物的一切,而且更多。
func (c *Cat) Speak() { fmt.Println("Meow!", "My name is", c.Name, ", it is", c.IsMammal, "I am a mammal with a climb factor of", c.ClimbFactor) }
在实现 Speak
的方法中,我可以把 UGH 改成 Meow 。这就是具体到猫的说话方式
一切看起来都很好,看起来嵌入提供的功能与其他语言中的继承相同。然后我试着把狗和猫分组,因为它们有共同的 DNA,都是动物。
// This does not compile, a Dog and Cat are not an Animal. animals := []Animal{ Dog{ Animal: Animal{ Name: "Fido", IsMammal: true, }, PackFactor: 5, }, Cat{ Animal: Animal{ Name: "Milo", IsMammal: true, }, ClimbFactor: 4, }, } for _, animal := range animals { animal.Speak() }
当我尝试这样做时,编译器抱怨狗和猫不是动物,这是真的。 嵌入与继承不同,这是我需要远离的模式。 狗是狗,猫是猫,动物是动物。 我不能把狗和猫当作动物来传递,因为它们不是。
这种机制也不是很灵活。 它需要开发人员进行配置,除非我可以访问代码并且可以随着时间的推移进行配置更改,否则这不是很灵活
如果这不是我们构建 Dog 和 Cat 集合的方式,我们如何在 Go 中做到这一点? 这不是通过共同的 DNA 进行分组,而是通过共同的行为进行分组。 行为是关键。
type Speaker interface { Speak() }
如果我使用一个接口,那么我可以定义我想要对不同类型的数据进行分组的通用行为方法集:
speakers := []Speaker{ &Dog{ Animal: Animal{ Name: "Fido", IsMammal: true, }, PackFactor: 5, }, &Cat{ Animal: Animal{ Name: "Milo", IsMammal: true, }, ClimbFactor: 4, }, } for _, speaker := range speakers { speaker.Speak() }
在新的代码中,我现在可以根据狗和猫的共同行为集将它们分组。的行为,也就是狗和猫能说话的事实,把它们归为一类。
事实上,Animal 类型确实是类型污染,因为声明一个类型只是为了共享一组公共状态是一种方式,应该避免.
type Dog struct { Name string IsMammal bool PackFactor int } type Cat struct { Name string IsMammal bool ClimbFactor int }
在这种特殊情况下,我宁愿看到动物类型被删除,字段被复制并粘贴到狗和猫类型中。稍后我将有关于更好的模式的说明,以消除这些情况的发生。
以下是原始代码中的代码异味:
- Animal 类型提供了一个可重用状态的抽象层。
- 程序永远不需要创建或单独使用 Animal 类型的值。
- 泛化了 Animal 类型的 Speak 方法的实现。
- 永远不会调用 Animal 类型的 Speak 方法。
总结
关于声明类型的指南:
- 声明代表新事物或独特事物的类型。
- 不要仅仅为了可读性而创建别名。
- 验证任何类型的值是单独创建或使用的。
- 嵌入类型不是因为我需要状态,而是因为我们需要行为。
- 如果我不考虑行为,我就会将自己锁定在未来不进行级联代码更改就无法成长的设计中。
- 作为现有类型的别名或抽象的问题类型。
- 唯一目的是共享公共状态的问题类型。