1. 引言
接口在系统设计中,以及代码重构优化中,是一个不可或缺的工具,能够帮助我们写出可扩展,可维护性更强的程序。
在本文,我们将介绍什么是接口,在此基础上,通过一个例子来介绍接口的优点。但是接口也不是任何场景都可以随意使用的,我们会介绍接口使用的常见场景,同时也介绍了接口滥用可能带来的问题,以及一些接口滥用的特征,帮助我们及早发现接口滥用的情况。
2. 什么是接口
接口是一种工具,在识别出系统中变化部分时,帮助从系统模块中抽取出变化的部分,从而保证系统的稳定性,可维护性和可扩展性。接口充当了一种契约或规范,规定了类或模块应该提供的方法和行为,而不关心具体的实现细节。
接口通常用于面向对象编程语言中,如 Java
和 Go
等。在这些语言中,类可以实现一个或多个接口,并提供接口定义的方法的具体实现。通过使用接口,我们可以编写更灵活、可维护和可扩展的代码,同时将系统中的变化隔离开来。
接口的实现在不同的编程语言中可能会有所不同。以下简单展示接口在Java
和 Go
语言中的示例。在Go
语言中,接口是一组方法签名的集合。实现接口时,类不需要显式声明实现了哪个接口,只要一个类型实现了接口中的所有方法,就被视为实现了该接口。
// 定义一个接口
type Shape interface {
Area() float64
Perimeter() float64
}
// 实现接口的类型
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
在Java
语言中,接口使用 interface
定义,同时包含所有的方法签名。类需要通过使用 implements
关键字来实现接口,并提供接口中定义的方法的具体实现。
// 定义一个接口
interface Shape {
double area();
double perimeter();
}
// 实现接口的类
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
上面示例展示了Java
和 Go
语言中接口的定义方式以及接口的实现方式,虽然具体实现方式各不相同,但它们都遵循了相似的概念,接口用于定义规范和契约,实现类则提供方法的具体实现来满足接口的要求。
3. 接口的优点
在识别出系统变化的部分后,接口能够帮助我们将系统中变化的部分抽取出来,基于此能够降低了模块间的耦合度,能够提高代码的可维护性和代码的模块化程度,有助于创建更灵活、可扩展和易于维护的代码。下面我们通过一个简单的例子来进行说明,详细讨论这些好处。
3.1 初始需求
假设我们在构建一个商城系统,其中一个相对复杂且重要的模块为商品价格的计算,计算购物车中各种商品的总价格。价格计算过程相对复杂,包括了基础价格、折扣、运费的计算,然后每一块内容都会有比较复杂的业务逻辑。
基于此设计了OrderProcessor
结构体,其中的CalculateTotalPrice
实现商品价格的计算,设计了ShippingCalculator
来计算运费,同时还设计DiscountCalculator
来计算商品的折扣信息,通过这几部分的交互配合,共同来完成商家价格的计算。
下面我们通过一段代码来展示上面的计算流程:
type OrderProcessor struct {
discountCalculator DiscountCalculator
taxCalculator TaxCalculator
}
// 计算总价格
func (tpc OrderProcessor) CalculateTotalPrice(products []Product) float64 {
total := 0.0
for _, item := range cart {
// 获取商品的基础价格
basePrice := item.BasePrice
// 获取适用于商品的折扣
discount := tpc.discountCalculator.CalculateDiscount(item)
// 计算运费
shippingCost := tpc.shippingCalculator.CalculateShippingCost(item)
// 计算商品的最终价格(基础价格 - 折扣 + 税费 + 运费)
finalPrice := basePrice - discount + shippingCost
total += finalPrice
}
return total
}
// 运费计算
type ShippingCalculator struct {}
func (sc ShippingCalculator) CalculateShippingCost(product Product) float64 {
return 0.0
}
// 折扣计算
type DiscountCalculator struct {}
func (dc DiscountCalculator) CalculateDiscount(product Product) float64 {
return 0.0
}
如果这里需求没有发生变化,这个流程可以很好得运转下去。假设这里需要根据商品的类型来应用不同的折扣,之后要怎么支持呢,可以对变化的部分抽取出一个接口,也可以不抽取,都可以支持,我们比较一下没有使用接口和使用接口的两种实现方式的区别。
3.2 不抽象接口
首先是不使用接口的实现,这里我们直接在DiscountCalculator
中叠加逻辑,支持不同类型商品的折扣:
type DiscountCalculator struct{}
func (dc DiscountCalculator) CalculateDiscount(product Product) float64 {
// 根据商品类型应用不同的折扣逻辑
switch product.Type {
case "TypeA":
return dc.calculateTypeADiscount(product)
case "TypeB":
return dc.calculateTypeBDiscount(product)
default:
return dc.calculateDefaultDiscount(product)
}
}
func (dc DiscountCalculator) calculateTypeADiscount(product Product) float64 {
// 计算 TypeA 商品的折扣
return product.BasePrice * 0.1 // 例如,假设 TypeA 商品有 10% 的折扣
}
fu