接口使用的最佳时机

news2024/10/7 18:21:20

1. 引言

接口在系统设计中,以及代码重构优化中,是一个不可或缺的工具,能够帮助我们写出可扩展,可维护性更强的程序。

在本文,我们将介绍什么是接口,在此基础上,通过一个例子来介绍接口的优点。但是接口也不是任何场景都可以随意使用的,我们会介绍接口使用的常见场景,同时也介绍了接口滥用可能带来的问题,以及一些接口滥用的特征,帮助我们及早发现接口滥用的情况。

2. 什么是接口

接口是一种工具,在识别出系统中变化部分时,帮助从系统模块中抽取出变化的部分,从而保证系统的稳定性,可维护性和可扩展性。接口充当了一种契约或规范,规定了类或模块应该提供的方法和行为,而不关心具体的实现细节。

接口通常用于面向对象编程语言中,如 JavaGo 等。在这些语言中,类可以实现一个或多个接口,并提供接口定义的方法的具体实现。通过使用接口,我们可以编写更灵活、可维护和可扩展的代码,同时将系统中的变化隔离开来。

接口的实现在不同的编程语言中可能会有所不同。以下简单展示接口在JavaGo 语言中的示例。在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;
    }
}

上面示例展示了JavaGo语言中接口的定义方式以及接口的实现方式,虽然具体实现方式各不相同,但它们都遵循了相似的概念,接口用于定义规范和契约,实现类则提供方法的具体实现来满足接口的要求。

3. 接口的优点

在识别出系统变化的部分后,接口能够帮助我们将系统中变化的部分抽取出来,基于此能够降低了模块间的耦合度,能够提高代码的可维护性和代码的模块化程度,有助于创建更灵活、可扩展和易于维护的代码。下面我们通过一个简单的例子来进行说明,详细讨论这些好处。

3.1 初始需求

假设我们在构建一个商城系统,其中一个相对复杂且重要的模块为商品价格的计算,计算购物车中各种商品的总价格。价格计算过程相对复杂,包括了基础价格、折扣、运费的计算,然后每一块内容都会有比较复杂的业务逻辑。

基于此设计了OrderProcessor结构体,其中的CalculateTotalPrice 实现商品价格的计算,设计了ShippingCalculator 来计算运费,同时还设计DiscountCalculator 来计算商品的折扣信息,通过这几部分的交互配合,共同来完成商家价格的计算。

image.png

下面我们通过一段代码来展示上面的计算流程:

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% 的折扣
}

func (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 接口以及两个不同的折扣计算器实现:TypeADiscountCalculatorTypeBDiscountCalculatorOrderProcessorWithInterface 结构体依赖于 DiscountCalculatorInterface 接口,这使得我们可以根据商品的类型轻松切换不同的折扣策略。

3.4 实现对比

下面我们通过比较上面两种实现,探讨在识别出系统的变化后,让系统依赖一个接口,相对于依赖一个具体类的优点。

首先是对于系统的可扩展性,假设现在需要支持新的类型的折扣,如果引入了接口,只需实现新的折扣计算器并满足相同的接口要求,就可以完成预期的功能。如果我们还是依赖一个具体的类,此时要么在DiscountCalculator 中通过if...else 叠加业务逻辑,相对于接口的引入,代码的可扩展性相比接口的使用就大大降低了。

对于系统的可测试性,如果是定义了接口,我们不需要验证其他DiscountCalculator 的实现,只需要验证当前新增的处理器即可。如果是依赖一个具体的类,此时如果进行测试,就需要对所有分支进行覆盖,很容易疏漏。其次,我们也可以轻松模拟不同的折扣计算器实现,验证 OrderProcessor 的行为。

还有代码可读性和可维护性,接口提供了一种清晰的契约,我们可以将DiscountCalculator当作一个小的模块,OrderProcessor通过接口与该模块进行交互,这使得代码更易于理解和维护,因为接口充当了文档,明确了每个模块的预期行为。

最后,通过接口的定义,OrderProcessor将不再依赖具体的类,而是依赖一个抽象层,降低了系统的耦合度,不再需要关注折扣的计算,让折扣的计算变得更加灵活。

通过以上的讨论,我们认为如果识别出了系统的变化后,该模块可能存在多个不同方向的变化,应该尽量抽取出一个接口,这样能够提高系统的可扩展性,可测试性,代码的可读性以及可维护性都有一定程度的提高。

4. 何时使用接口

接口可以给我们带来一系列的优点,如松耦合,隔绝变化,提高代码的可扩展性等,但是滥用接口的话,反而会引入不必要的复杂性,并增加代码的理解和维护成本。

有一个核心的准则,尽量支持依赖具体的类,而不是抽取接口,不要为了使用接口而创造不必要的抽象,这可能会使代码变得混乱和难以理解。

如果真的使用接口,应该确定其在系统设计中起到促进松耦合和可维护性的作用,而不是增加复杂性。要在合适的场景下使用接口,并考虑接口设计的清晰性和可维护性。下面基于此,我们讨论一些接口可能适用的场景。

4.1 系统中存在变化部分

系统中存在变化的部分是使用接口的最核心场景之一 使用接口可以将这些变化部分从系统的其他部分隔离开来,使系统更具灵活性和可维护性。这种设计允许我们将变化的部分抽取为一个单独的模块,在变化时,只需要对该模块进行修改,而不必修改整个系统。接口充当了变化部分的契约,使不同的实现可以轻松地替换或添加,从而适应新的需求或变化的情况。

比如系统需要向用户发送邮件,可能不同的运营商提供了不同的API,然后我们系统中需要支持多个不同的运营商,在不同场景下使用不同运营商的接口。

此时我们通过定义接口,系统通过与该接口进行交互即可,而不需要关心底层的实现细节。如果将来要添加新的邮件服务提供商,只需创建一个新的类并实现接口即可,而不需要修改现有的代码。

这种方式使系统的变化部分与其余部分隔离开来,提高了系统的可维护性和可扩展性。此外,通过使用接口,我们可以创建模拟邮件发送器来验证系统的行为,更容易进行单元测试。

4.2 类库的可配置性

类库对外扩展和提供可配置性也是接口使用的重要场景之一。当开发一个类库或框架时,为了让用户能够轻松地扩展和自定义其行为,可以通过接口提供一组可配置的扩展点。这些扩展点允许用户提供自己的实现,以适应其特定需求。

举例来说,一个日志库可以定义一个接口 Logger,并允许用户提供他们自己的 Logger 实现。用户可以选择使用默认的日志记录实现,也可以创建一个自定义的实现,以将日志信息发送到不同的地方(例如文件、数据库、远程服务器等)。这种可配置性使用户能够根据其项目的要求自由选择和调整库的行为。

通过提供接口和可配置性,类库或框架可以更具通用性和灵活性,使用户能够根据其特定的用例和需求来定制和扩展库的功能,从而提高了库的可用性和适用性。这种模块化的设计方式有助于减少代码的重复,促进了代码的复用,同时也提供了更好的可扩展性和可维护性。

4.3 模块间的交互

系统划分不同模块并使用接口来进行交互也是一个重要的场景。当将系统划分为不同的模块或组件时,使用接口定义模块之间的契约和互动方式是一种良好的实践。每个模块可以实现所需的接口,并与其他模块进行交互,这使得模块之间的界限更加清晰,易于理解和维护。

使用接口可以降低模块之间的耦合度。这意味着每个模块不需要关心其他模块的具体实现细节,只需要遵循接口定义的契约。这种模块化的设计方式有助于将复杂的系统拆分为更小、更易管理的部分,并降低了系统开发和维护的复杂性。

4.4 单元测试的使用

在需要解除一个庞大的外部系统的依赖时。有时候我们并不是需要多个选择,而是某个外部依赖过重,我们测试或其他场景可能会选择 mock 一个外部依赖,以便降低测试系统的依赖。

比如依赖多个外部rpc,单元测试时需要屏蔽外部的依赖,此时就比较有必要使用接口,通过框架生成一个mock的实现,从而解除对外部的依赖。

5. 潜在的误用和滥用

5.1 接口滥用带来的问题

虽然接口在合适的场景中非常有用,但滥用接口可能会导致代码变得复杂、难以理解和难以维护。引入过多的接口可能会增加系统的复杂性,使代码难以理解。每个接口都需要额外的抽象和实现,这可能不是必要的。其次使用接口有时会引入额外的性能开销,因为运行时需要进行接口解析。在性能敏感的应用中,这可能是一个问题。

最重要的一个问题,接口的目标是提供一种通用的抽象,给系统提供可配置项,但有时候过度一般化可能会导致不必要的复杂性。在某些情况下,直接使用具体的类可能更加简单和清晰。

我们应该在确保接口是必要的情况下使用它们,以避免不必要的复杂性和耦合。接口的设计应该基于真正的需求和系统架构,而不是仅仅为了使用接口而使用接口。

5.2 如何识别接口是否滥用

对于识别接口是否滥用,可以通过下面几个方面来检查,如果满足了下面的某一个条件,此时大概率就出现了接口滥用的情况。

是否过早的抽象,在引入该接口时,系统中是否足够的不同实现来正当地支持这些接口。如果没有的话,此时大概率过早接口的引入,增加了复杂性,而不带来真正的好处。

是否所有类之间引入接口,无论是否有必要,在这种情况下,接口的数量可能会急剧增加,导致代码难以理解和维护,可能还是存在一定滥用的情况。

如果接口经常发生变化,那么实现这些接口的类可能需要频繁地进行修改,这会增加维护的难度,此时要么接口是不必要的,要么接口的设计是不合理的,需要重新设计。

总的来说, 我们需要确保真正需要接口时才引入它们。应该谨慎考虑每个接口的设计,确保它们具有明确的用途(如隔绝变化,模块间交互的契约,方便单元测试),并且不引入不必要的复杂性。根据实际需求和系统架构来合理地使用接口,而不是为了使用接口而使用接口。

6. 总结

在本文,我们介绍了什么是接口,接口是一种契约,一种协议,用于模块间的交互。

在此基础上,通过一个例子来介绍接口的优点,了解到接口可以提高代码的可扩展性,可维护性,以及降低系统之间的耦合度。

但是接口也不是任何场景都可以随意使用的,我们会介绍接口使用的常见场景,包括隔绝系统的变化部分,以及一些类库设计时对外提供配置项的场景。

最后我们还介绍了接口滥用可能带来的问题,以及一些比较明显的特征,帮助我们更早识别出系统设计的坏味道。

基于此,完成了对接口的完整介绍,希望对你有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/991518.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【2023高教社杯】A题 定日镜场的优化设计 问题分析及数学模型

【2023高教社杯】A题 定日镜场的优化设计 问题分析及数学模型 1 题目 构建以新能源为主体的新型电力系统,是我国实现“碳达峰”“碳中和”目标的一项重要措施。塔式太阳能光热发电是一种低碳环保的新型清洁能源技术[1]。 定日镜是塔式太阳能光热发电站(…

微电网的概念

微电网分布式控制理论与方法  顾伟等 微电网的概念和作用 微电网是由多种分布式电源、储能、负载以及相关监控保护装置构成的能够实现自我控制和管理的自治型电力系统,既可以与电网并网进行,也可以以孤岛运行。 分布式发电是指将容量在兆瓦以内的可再…

Elsevier出版社 | 优质好刊合集

【SciencePub学术】 爱思唯尔(Elsevier)是一家全球专业从事科学与医学的信息分析公司作为出版公司,成立于1880年,其产品包括《柳叶刀》、《四面体》和《细胞》等学术期刊,ScienceDirect电子期刊集, “趋势”(Trends)系列和“新见…

uniapp项目运行Missing script: “dev“, To see a list of scripts, run:

webstorm 打开项目根目录不对,打开到了项目上一级。 另外一个原因是,当前项目是Hbuilder 可视化界面创建的,不能在terminal直接脚本指令启动。 可以webstorm 安装支持uniapp项目插件,然后创建一个运行器,运行h5。 安…

vue 验证码 图片点击

实现登陆验证 图片依次点击功能 demo &#xff0c;上图可以根据demo修改&#xff0c;直接拿用 <template><div><div class"big-box" id"BigBox" :style"background-image:url( imgCodeUrl )"><div class"click-box…

C#,《小白学程序》第十八课:随机数(Random)第五,方差及标准方差(标准差)的计算方法与代码

1 文本格式 /// <summary> /// 《小白学程序》第十八课&#xff1a;随机数&#xff08;Random&#xff09;第五&#xff0c;方差及标准方差&#xff08;标准差&#xff09;的计算方法与代码 /// 方差 SUM(&#xff08;Xi - X)^2 ) / n i0...n-1 X Average of X[i] ///…

APP备案流程详细解读

背景介绍 2023年8月4日&#xff0c;工信部发布《工业和信息化部关于开展移动互联网应用程序备案工作的通知》。 在中华人民共和国境内从事互联网信息服务的APP主办者&#xff0c;应当依照《中华人民共和国反电信网络诈骗法》《互联网信息服务管理办法》&#xff08;国务院令第…

SpotBugs代码检查:在整数上进行没有起任何实际作用的位操作(INT_VACUOUS_BIT_OPERATION)

https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html#int-vacuous-bit-mask-operation-on-integer-value-int-vacuous-bit-operation 在整数上进行无用的与、异或操作&#xff0c;实质上没有做任何有用的工作。 例如&#xff1a;v & 0xffffffff 再例如&…

如何处理异步编程中的回调地狱问题?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 解决回调地狱问题的方法⭐使用 Promise⭐使用 async/await⭐ 使用回调函数库⭐模块化⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端…

微软研究院团队获得首届AI药物研发算法大赛总冠军

编者按&#xff1a;AI 药物研发是人工智能未来应用的重要方向之一。自新冠病毒&#xff08;SARS-CoV-2&#xff09;首次爆发以来&#xff0c;新冠病毒的小分子药物研发备受关注&#xff0c;于近期举行的首届 AI 药物研发算法大赛便聚焦于此。在比赛中&#xff0c;来自微软研究院…

go语言基础操作---七

socket简单介绍—套接字编程 什么是Socket Socket&#xff0c;英文含义是【插座、插孔】&#xff0c;一般称之为套接字&#xff0c;用于描述IP地址和端口。可以实现不同程序间的数据通信。 Socket起源于Unix&#xff0c;而Unix基本哲学之一就是“一切皆文件”&#xff0c;都可…

【漏洞复现】天OA存在任意文件上传漏洞

漏洞描述 华天动力协同办公系统将先进的管理思想、管理模式和软件技术、网络技术相结合,为用户提供了低成本、高效能的协同办公和管理平台。睿智的管理者通过使用华天动力协同办公平台,在加强规范工作流程、强化团队执行、推动精细管理、促进营业增长等工作中取得了良好的成…

linux系统中驱动框架基本分析

大家好&#xff0c;今天分享一篇Linux驱动软件设计思想的文章。由于文章较长&#xff0c;可以先收藏后再慢慢看。 一、Linux驱动的软件架构 1.1 出发点 为适应多种体系架构的硬件&#xff0c;增强系统的可重用和跨平台能力。 1.2 分离思想 为达到一个驱动最好一行都不改就…

Spring全家桶相关注解总结

spring相关 Controller 【控制器】效验有效参数的合法性&#xff08;相当于安检系统&#xff09; Service 【服务】业务组装&#xff08;客服中心&#xff09; Repository 【数据持久层】实际业务处理&#xff08;实际办理的业务&#xff09; Component 【组件】工具类…

代码随想录 -- day42 -- 01背包问题、416. 分割等和子集

01背包问题 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品只能用一次&#xff0c;求解将哪些物品装入背包里物品价值总和最大 416. 分割等和子集 思路&#xff1a; 前提条件&#xff1a; 我们要求的是让两…

力扣每日一题---207. 课程表

Problem: 207. 课程表 文章目录 解题方法复杂度Code 解题方法 y总的 Topsort 模板题 复杂度 时间复杂度: 添加时间复杂度, 示例&#xff1a; O ( n ) O(n) O(n) 空间复杂度: 添加空间复杂度, 示例&#xff1a; O ( n ) O(n) O(n) Code class Solution {int res 0; public…

【python自动化】playwright长截图切换标签页JS注入实战

前言 当前教程使用的playwright版本为1.37.0,selenium版本为3.141.0 官方文档&#xff1a;https://playwright.dev/python/docs/screenshots 本教程目录如下 文章目录 前言playwright各类截图源码阅读ElementHandle类下的截图Page类下的截图Locator类下的截图 Playwright快速…

JetBrains设置文件名格式

如题&#xff0c;在使用CLion创建C类时&#xff0c;希望创建的文件名符合Google编码规范。设置如下图所示&#xff1a; 创建的C类是PascalCase格式&#xff0c;对应的文件名是pascal-case格式。

例举onekey一键还原如何使用

onekey一键还原怎么使用呢​​​​​​​?随着数字化的发展&#xff0c;现在电脑已成为人们工作学习娱乐的必备工具&#xff0c;想要放心的使用电脑&#xff0c;不仅需要杀毒软件&#xff0c;还需要一款一键还原软件。接下来&#xff0c;我就教大家如何使用onekey一键还原。还…

word文档如何引用参考文献

参考 word文档如何引用参考文献 说明