SOLID原则:基于Go语言实现

date
Dec 2, 2023
slug
solid-principle-implemented-based-on-Go
status
Published
tags
编程开发
summary
SOLID 是面向对象编程和设计中的五个基本原则的首字母缩写,旨在提高软件开发的可维护性、灵活性和扩展性。本文将基于Go语言给出这个五个原则实现,加强对这五个原理的理解。
type
Post

简介

SOLID 是面向对象编程和设计中的五个基本原则的首字母缩写,旨在提高软件开发的可维护性、灵活性和扩展性。这五个原则是:
  1. 单一职责原则(Single Responsibility Principle)
  1. 开闭原则(Open/Closed Principle)
  1. 里氏替换原则(Liskov Substitution Principle)
  1. 接口隔离原则(Interface Segregation Principle)
  1. 依赖倒置原则(Dependency Inversion Principle)
本文将基于Go语言给出这个五个原则实现,加强对这五个原理的理解。

原理与代码示例

单一职责原则(Single Responsibility Principle)

单一职责原则(Single Responsibility Principle, SRP)指的是一个类应该只有一个引起变化的原因。这个原则主张将不同的功能分离到不同的类或模块中,以保证每个类或模块只专注于单一的任务或职责。遵循这个原则可以使代码更加清晰、易于维护和扩展。
 
一个很常见的场景就是系统设置或配置应该独立于业务逻辑。一个类负责读取和解析配置文件,而另一个类使用这些配置来执行业务逻辑。将配置读取解析与业务逻辑解耦,使得配置更具独立性,也使得配置更具有可维护性。
 
在下面的go语言代码示例中,我们构建了两个结构体:
  • ConfigManager:负责读取和解析配置文件。
  • Application:使用配置信息执行一些业务逻辑。
为了简化,我们假设配置仅包含一个简单的设置项,比如应用的运行模式(开发模式或生产模式),而业务逻辑根据这个模式进行不同的处理。
 
 
这样的设计确保了每个组件都只关注它们各自的职责,符合单一职责原则。在实际应用中,配置加载可能会涉及读取文件、解析 JSON 或 XML 等操作,这里为了简化示例,我使用了硬编码的配置值。

开闭原则(Open/Closed Principle)

开闭原则(Open/Closed Principle, OCP)是面向对象设计的核心原则之一,它指出软件实体(如类、模块、函数等)应该对扩展开放,对修改封闭。这意味着一个实体允许其行为被扩展,但不允许修改已有代码。这个原则的目的是使软件更容易维护、适应变化,并提高软件的可复用性。
 
如果熟悉设计模式的话,策略模式、装饰器模式和工厂模式等模式都体现了开闭原则。以策略模式为例,这种模式定义了一系列算法,并将每一个算法封装起来,使它们可以互相替换。
例如支付系统中,支持多种支付方式(如信用卡、PayPal、比特币等),每种支付方式都可以作为一个策略实现,而支付系统可以在不改变现有代码的基础上添加新的支付方法。
 
接下来我们以策略模式作为示例,来看它怎么体现开闭原则。假设我们有一个简单的日志系统,它可以通过不同的方式输出日志(例如,控制台、文件)。我们可以定义一个日志接口,然后为每种日志方式提供一个实现,从而在不修改现有代码的情况下轻松添加新的日志方式。
 
在这个示例中,Logger 接口允许我们通过多种方式实现日志记录功能。我们定义了两种日志方式:ConsoleLoggerFileLoggerApplication 类使用 Logger 接口来记录日志,因此我们可以在不修改 Application 类的情况下,添加更多的 Logger 实现。这正体现了开闭原则——对扩展开放(可以添加更多的 Logger 实现),对修改封闭(不需要修改现有的 Application 类和 Logger

里氏替换原则(Liskov Substitution Principle, LSP)

里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的重要原则之一,这个原则指出,如果一个程序中的对象可以被它的基类对象替换而程序的功能不受影响,那么这个对象应该是它的子类的实例。简单来说,子类应该能够扩展父类的功能,而不改变父类原有的功能。
 
继承、多态和重写其实就是典型的里氏替换原则的体现。熟悉面向对象几个原则,这个原则应该不用过多介绍了,这里就简单给出个示例代码。
 
 
在这个例子中,Duck 类继承自 Bird 类。虽然 Duck 重写了 Fly 方法,但重写后的行为仍然符合 BirdFly 方法的一般预期。因此,Duck 的实例可以在任何需要 Bird 类型的地方使用,不会破坏程序的正确性。这符合里氏替换原则的要求。通过这种方式,代码保持了良好的灵活性和可扩展性。

接口隔离原则(Interface Segregation Principle)

接口隔离原则(Interface Segregation Principle, ISP)是面向对象设计原则之一,强调“客户端不应该依赖于它不使用的接口”,其中的“客户端”,可以理解为接口的调用者或者使用者。这个原则建议将大的、庞杂的接口拆分成更小、更具体的接口,以便客户端只需要知道和使用它们真正需要的方法。这样可以减少客户端依赖,提高系统的灵活性和可维护性。
 
例如假设我们要设计多功能设备(如打印机),一个打印机可能有打印、扫描和复印等功能。当我们设计接口的时候,应该为每个功能创建单独的接口,而不是创建一个包含所有这些功能的单一接口。参考下面的代码示例。
 
 
在这个示例中,PrinterScanner 是两个独立的接口,分别定义了打印和扫描的功能。MyPrinterMyScanner 分别实现了这些接口。MultiFunctionMachine 是一个组合设备,它通过嵌入 PrinterScanner 接口来实现多功能。这样的设计允许客户端只依赖于它们需要的功能,符合接口隔离原则。

依赖倒置原则(Dependency Inversion Principle)

依赖倒置原则(Dependency Inversion Principle, DIP)是面向对象设计的重要原则之一,强调抽象不应该依赖于细节,细节应该依赖于抽象。换句话说,高层模块(定义复杂的业务规则)不应该依赖于低层模块(具体的实现细节),两者都应该依赖于抽象(接口或抽象类)。这个原则的目的是减少高层和低层模块之间的直接依赖,从而提高系统的可维护性和灵活性。
 
以下是一个依赖倒置原则的示例,演示了一个简单的消息发送系统,其中消息的发送方式可以多样化(比如邮件、短信等),而高层模块依赖于一个抽象的发送接口。
 
 
在这个例子中,Notification 类不直接依赖于 EmailSenderSMSSender 的具体实现,而是依赖于 MessageSender 接口。这样可以在不修改 Notification 类的情况下,添加新的消息发送方式,只需确保它们实现了 MessageSender 接口。这种设计提高了系统的灵活性和可维护性,符合依赖倒置原则。

拓展

前面提到SOLID最后一个原则是依赖倒置原则,还有两个概念经常会这个原则一起提到。
  • 控制反转(Inversion Of Control,IoC)
  • 依赖注入(Dependency Injection,DI)

控制反转(Inversion Of Control,IoC)

控制反转是一种设计原则,用于减少计算机代码之间的耦合。在传统的程序设计中,程序的流程由应用程序本身控制。在使用控制反转的设计中,这种流程控制被反转,转移到外部容器或框架上。
 
核心思想:在控制反转中,自定义的代码不直接调用框架/库的代码,而是框架/库调用自定义的代码。这种方式主要有以下几种形式:
  1. 事件驱动编程:用户定义的代码响应外部事件(如用户输入、系统消息)而被调用。
  1. 依赖注入:对象的依赖(比如服务、配置)由外部系统(通常是框架或容器)注入。
  1. 模板方法设计模式:用户的代码扩展框架的操作,框架则在适当的时候调用这些操作。
 
在接下来的在这个例子中,我们将创建一个简单的事件框架,它允许注册事件处理函数,并在特定事件发生时由框架调用这些函数,而不是由用户的代码直接调用。
 
 
在这个示例中,EventManager 作为一个简单的事件管理框架,允许注册(订阅)事件和关联的处理函数。当一个事件被触发时(在这个例子中是 "userCreated" 事件),EventManager 调用所有注册到该事件的处理函数。
用户定义的函数 onUserCreated 被注册为 "userCreated" 事件的处理函数。当 Publish 方法被调用并触发 "userCreated" 事件时,EventManager 负责调用 onUserCreated 函数,这就是控制反转的体现:不是用户代码调用处理函数,而是框架在适当的时候调用用户提供的代码。

依赖注入(Dependency Injection, DI

依赖注入是控制反转的一种特殊形式,用于实现组件或对象之间的依赖关系。在这种模式下,一个组件不再负责寻找或创建它所需要的资源(依赖),而是由外部(例如框架、容器或其他组件)提供。
 
工作原理:在依赖注入中,对象的依赖项(如服务、配置数据)通过构造函数、方法或属性直接提供给对象,而不是由对象自己创建。
优点
  • 降低耦合度:组件之间的依赖关系更加清晰,易于管理。
  • 提高可测试性:通过替换依赖项,可以轻松地对组件进行单元测试。
  • 增加灵活性和可重用性:组件更加独立,易于在不同环境中重用。
 
假设有一个应用程序,其中包含一个需要发送通知的类。在不使用依赖注入的情况下,这个类可能直接创建一个特定类型的通知发送器(如邮件发送器)。但在使用依赖注入的情况下,通知发送器作为一个依赖被注入到类中,使得可以灵活地更换不同的通知发送方式。
 
 
在这个例子中,使用依赖注入的方式增加了代码的灵活性,使得 Notification 类可以与任何实现了 MessageSender 接口的发送器协作,而不仅限于电子邮件发送器。这样的设计既减少了耦合,也提高了代码的可测试性和可维护性。

总结

以上便是SOLID原则的解释和相关示例,这些示例在我们日常的开发中都挺常见,即使是简单的功能,也可以有不同的实现方式,在平时应该多去思考如何将系统的不同部分解耦,使得每个部分都更加独立,容易理解和修改。只有掌握这些基本功才能在未来自己负责复杂的业务系统时设计代码逻辑游刃有余。