nc (dc DiscountCalculator) calculateTypeBDiscount(product Product) float64 {
// 计算 TypeB 商品的折扣
return product.BasePrice * 0.15 // 例如,假设 TypeB 商品有 15% 的折扣
}
func (dc DiscountCalculator) calculateDefaultDiscount(product Product) float64 {
// 默认折扣逻辑,如果商品类型未匹配到其他情况
return product.BasePrice // 默认不打折
}
在这里,我们计算商品折扣,直接使用DiscountCalculator
来实现,根据商品的类型应用不同的折扣逻辑。这里使用了 switch
语句来确定应该应用哪种折扣。这种实现方式虽然在一个类中处理了所有的逻辑,但它可能会导致 DiscountCalculator
类变得庞大且难以维护,特别是当折扣逻辑变得更加复杂或需要频繁更改时。
3.3 抽象接口
下面我们给出一个使用接口的实现,将不同的折扣逻辑封装到不同的实现中,以下是使用接口的示例实现:
type OrderProcessor struct {
// 计算商品价格,直接依赖接口
discountCalculator DiscountCalculatorInterface
taxCalculator TaxCalculator
shippingCalculator ShippingCalculator
}
// 定义折扣计算器接口
type DiscountCalculatorInterface interface {
CalculateDiscount(product Product) float64
}
// 定义一个具体的折扣计算器实现
type TypeADiscountCalculator struct{}
func (dc TypeADiscountCalculator) CalculateDiscount(product Product) float64 {
// 计算 TypeA 商品的折扣
return product.BasePrice * 0.1 // 例如,假设 TypeA 商品有 10% 的折扣
}
// 定义另一个具体的折扣计算器实现
type TypeBDiscountCalculator struct{}
func (dc TypeBDiscountCalculator) CalculateDiscount(product Product) float64 {
// 计算 TypeB 商品的折扣
return product.BasePrice * 0.15 // 例如,假设 TypeB 商品有 15% 的折扣
}
上述示例中,我们定义了一个 DiscountCalculatorInterface
接口以及两个不同的折扣计算器实现:TypeADiscountCalculator
和 TypeBDiscountCalculator
。 OrderProcessorWithInterface
结构体依赖于 DiscountCalculatorInterface
接口,这使得我们可以根据商品的类型轻松切换不同的折扣策略。
3.4 实现对比
下面我们通过比较上面两种实现,探讨在识别出系统的变化后,让系统依赖一个接口,相对于依赖一个具体类的优点。
首先是对于系统的可扩展性,假设现在需要支持新的类型的折扣,如果引入了接口,只需实现新的折扣计算器并满足相同的接口要求,就可以完成预期的功能。如果我们还是依赖一个具体的类,此时要么在DiscountCalculator
中通过if...else
叠加业务逻辑,相对于接口的引入,代码的可扩展性相比接口的使用就大大降低了。
对于系统的可测试性,如果是定义了接口,我们不需要验证其他DiscountCalculator
的实现,只需要验证当前新增的处理器即可。如果是依赖一个具体的类,此时如果进行测试,就需要对所有分支进行覆盖,很容易疏漏。其次,我们也可以轻松模拟不同的折扣计算器实现,验证 OrderProcessor
的行为。
还有代码可读性和可维护性,接口提供了一种清晰的契约,我们可以将DiscountCalculator
当作一个小的模块,OrderProcessor
通过接口与该模块进行交互,这使得代码更易于理解和维护,因为接口充当了文档,明确了每个模块的预期行为。
最后,通过接口的定义,OrderProcessor
将不再依赖具体的类,而是依赖一个抽象层,降低了系统的耦合度,不再需要关注折扣的计算,让折扣的计算变得更加灵活。
通过以上的讨论,我们认为如果识别出了系统的变化后,该模块可能存在多个不同方向的变化,应该尽量抽取出一个接口,这样能够提高系统的可扩展性,可测试性,代码的可读性以及可维护性都有一定程度的提高。
4. 何时使用接口
接口可以给我们带来一系列的优点,如松耦合,隔绝变化,提高代码的可扩展性等,但是滥用接口的话,反而会引入不必要的复杂性,并增加代码的理解和维护成本。
有一个核心的准则,尽量支持依赖具体的类,而不是抽取接口,不要为了使用接口而创造不必要的抽象,这可能会使代码变得混乱和难以理解。
如果真的使用接口,应该确定其在系统设计中起到促进松耦合和可维护性的作用,而不是增加复杂性。要在合适的场景下使用接口,并考虑接口设计的清晰性和可维护性。下面基于此,我们讨论一些接口可能适用的场景。
4.1 系统中存在变化部分
系统中存在变化的部分是使用接口的最核心场景之一 。 使用接口可以将这些变化部分从系统的其他部分隔离开来,使系统更具灵活性和可维护性。这种设计允许我们将变化的部分抽取为一个单独的模块,在变化时,只需要对该模块进行修改,而不必修改整个系统。接口充当了变化部分的契约,使不同的实现可以轻松地替换或添加,从而适应新的需求或变化的情况。
比如系统需要向用户发送邮件,可能不同的运营商提供了不同的API,然后我们系统中需要支持多个不同的运营商,在不同场景下使用不同运营商的接口。
此时我们通过定义接口,系统通过与该接口进行交互即可,而不需要关心底层的实现细节。如果将来要添加新的邮件服务提供商,只需创建一个新的类并实现接口即可,而不需要修改现有的代码。
这种方式使系统的变化部分与其余部分隔离开来,提高了系统的可维护性和可扩展性。此外,通过使用接口,我们可以创建模拟邮件发送器来验证系统的行为,更容易进行单元测试。
4.2 类库的可配置性
类库对外扩展和提供可配置性也是接口使用的重要场景之一。当开发一个类库或框架时,为了让用户能够轻松地扩展和自定义其行为,可以通过接口提供一组可配置的扩展点。这些扩展点允许用户提供自己的实现,以适应其特定需求。
举例来说,一个日志库可以定义一个接口 Logger
,并允许用户提供他们自己的 Logger
实现。用户可以选择使用默认的日志记录实现,也可以创建一个自定义的实现,以将日志信息发送到不同的地方(例如文件、数据库、远程服务器等)。这种可配置性使用户能够根据其项目的要求自由选择和调整库的行为。
通过提供接口和可配置性,类库或框架可以更具通用性和灵活性,使用户能够根据其特定的用例和需求来定制和扩展库的功能,从而提高了库的可用性和适用性。这种模块化的设计方式有助于减少代