Go 的面向对象编程技巧
❝
可以遗憾,但不要后悔。
我们留在这里,从来不是身不由己。
——— 而是选择在这里经历生活
❞
面向对象三要素
Go
语言的面向对象编程有三个重要的思想:封装、继承和多态。
- 封装
Go
语言通过 struct
结构体的方式来实现封装,结构体可以包含各种类型的变量和方法,可以将一组相关的变量和方法封装在一起。使用首字母大小写控制变量和方法的访问权限,实现了信息隐藏和访问控制。
- 继承
Go
语言中没有传统的继承机制,但是可以使用嵌入式类型来实现类似继承的效果,将一个类型嵌入到另一个类型中,从而继承嵌入类型的方法和属性。嵌入式类型的特点是可以直接访问嵌入类型的属性和方法,不需要通过接口或者其他方式进行转换。在 Go
语言中,可以通过 struct
嵌套和 interface
嵌套来实现继承的效果。
- 多态
Go
语言通过接口来实现多态,一个类型只需要实现了接口的所有方法,就可以被赋值给该接口类型的变量。这样可以实现类似于面向对象语言中的多态性。多态性使得程序可以根据上下文环境自动选择合适的方法实现,提高了代码的灵活性和可复用性。
代码实战
常规函数写法
在这个示例中,函数和结构体是分离的,函数接收结构体指针类型作为参数,需要手动传递结构体的指针。尽管这种方式有一定的缺陷,调用会比较麻烦,但它更加符合基于过程式编程思想的设计理念,即将一个大问题拆分成多个小问题,并通过函数解决这些小问题。适用于初学者对于代码的简单操作。优点就只有易于理解。
package test
import (
"fmt"
"testing"
)
type Mobile struct {
User string `json:"user"`
Brand string `json:"brand"`
Prise float64 `json:"prise"`
}
func CallUp(m *Mobile) {
fmt.Printf("%s is using 💲%.2f mobile phone to make a call.\n", m.User, m.Prise)
}
func Storage(m *Mobile) {
fmt.Printf("%s is using a %s mobile phone to transfer data.\n", m.User, m.Brand)
}
func Charge(m *Mobile) string {
return fmt.Sprintf("%s is charging a %s phone.\n", m.User, m.Brand)
}
func Game(m *Mobile, name string) {
fmt.Printf("%s is playing the game of '%s'.\n", m.User, name)
}
func TestExample(t *testing.T) {
iPhone := Mobile{
User: "Tom",
Brand: "iPhone 15 Pro MAX",
Prise: 12688.00,
}
CallUp(&iPhone)
Game(&iPhone, "Card")
Storage(&iPhone)
fmt.Printf(Charge(&iPhone))
}
调用结构体类型上的方法
调用结构体类型上的方法体现了面向对象编程的封装思想。封装的核心是将数据和行为打包在一起,通过公开和私有的方式来隐藏实现细节。这样可以使得代码更加模块化、安全、易于维护,并且更加符合现实世界中的抽象模型。
相比于上面的函数调用,调用结构体类型上的方法可以使调用方法时不必手动传递结构体实例对象,只需聚焦于方法参数本身,提高了代码的可读性和易用性。这也符合面向对象编程的简洁性和代码重用性的思想。
package test
import (
"fmt"
"testing"
)
type Mobile struct {
User string `json:"user"`
Brand string `json:"brand"`
Prise float64 `json:"prise"`
}
func NewMobile(user string, brand string, prise float64) *Mobile {
return &Mobile{User: user, Brand: brand, Prise: prise}
}
func (m *Mobile) CallUp() {
fmt.Printf("%s is using 💲%.2f mobile phone to make a call.\n", m.User, m.Prise)
}
func (m *Mobile) Storage() {
fmt.Printf("%s is using a %s mobile phone to transfer data.\n", m.User, m.Brand)
}
func (m *Mobile) Charge() string {
return fmt.Sprintf("%s is charging a %s phone.\n", m.User, m.Brand)
}
func (m *Mobile) Game(name string) {
fmt.Printf("%s is playing the game of '%s'.\n", m.User, name)
}
func TestExample(t *testing.T) {
applePhone := NewMobile("Tom", "iPhone 15 Pro MAX", 12688.00)
applePhone.CallUp()
applePhone.Game("Card")
applePhone.Storage()
fmt.Printf(applePhone.Charge())
}
调用接口类型上的方法
接口实例: 是指定义一个接口类型,并将具体的结构体类型的实例传递给它。
调用接口类型上的方法,将接口与结构体类型分开,使接口具有更广泛的适用性。使用 “接口实例” 可以实现更灵活的代码设计,因为可以在运行时动态地选择要使用的实现类型。
同时,由于接口只关心方法的签名,而不关心具体实现方式,因此可以将不同的结构体类型传递给同一个接口,从而实现面向对象思想的多态性。
在这个示例中,定义了一个 USB
接口和 PlayBoy
接口,它们都包含各自的方法。在测试函数中调用这两个接口的方法时需要分别调用。这两个接口之间没有直接的联系或关联,它们是相互独立的。如果你想将这两个接口组合在一起,可以使用 “嵌入式接口”。
package test
import (
"fmt"
"testing"
)
var (
applePhone, huaweiPhone *Mobile
)
func init() {
applePhone = NewMobile("Tom", "iPhone 15 Pro MAX", 12688.00)
huaweiPhone = NewMobile("John", "Huawei Meta 40 Pro", 8888.00)
}
type USB interface {
Storage()
Charge() string
}
type PlayBoy interface {
Game(name string)
}
type Mobile struct {
User string `json:"user"`
Brand string `json:"brand"`
Prise float64 `json:"prise"`
}
func NewMobile(user string, brand string, prise float64) *Mobile {
return &Mobile{User: user, Brand: brand, Prise: prise}
}
func (m *Mobile) CallUp() {
fmt.Printf("%s is using 💲%.2f mobile phone to make a call.\n", m.User, m.Prise)
}
func (m *Mobile) Storage() {
fmt.Printf("%s is using a %s mobile phone to transfer data.\n", m.User, m.Brand)
}
func (m *Mobile) Charge() string {
return fmt.Sprintf("%s is charging a %s phone.\n", m.User, m.Brand)
}
func (m *Mobile) Game(name string) {
fmt.Printf("%s is playing the game of '%s'.\n", m.User, name)
}
func TestExample(t *testing.T) {
USB.Storage(applePhone)
fmt.Printf(USB.Charge(huaweiPhone))
PlayBoy.Game(huaweiPhone, "LOL")
}
嵌入式接口
嵌入式接口: 是一种将一个接口嵌入到另一个接口中的技术,嵌入的接口中的所有方法都会被继承到当前接口中。通过接口的嵌套,可以将多个接口组合成一个更大的接口,从而使代码更加简洁、灵活。这也体现了面向对象编程中的继承特性。
在这个示例中,定义了一个 IPhone
接口,它嵌入了 USB
接口和 PlayBoy
接口,以及 CallUp()
方法。 从而可以使用这三个接口中的所有方法。通过这种方式,我们可以将不同的接口组合成一个更大的接口,以便更方便地使用这些方法。在测试函数中,我们创建了一个 Mobile
类型的实例,并将其转换为 IPhone
类型的接口实例 p
,然后可以使用 p
调用 Mobile
结构体中实现的 CallUp()
、Game()
、Storage()
和 Charge()
方法。
package test
import (
"fmt"
"testing"
)
type IPhone interface {
USB
PlayBoy
CallUp()
}
type USB interface {
Storage()
Charge() string
}
type PlayBoy interface {
Game(name string)
}
type Mobile struct {
User string `json:"user"`
Brand string `json:"brand"`
Prise float64 `json:"prise"`
}
func (m *Mobile) CallUp() {
fmt.Printf("%s is using 💲%.2f mobile phone to make a call.\n", m.User, m.Prise)
}
func (m *Mobile) Storage() {
fmt.Printf("%s is using a %s mobile phone to transfer data.\n", m.User, m.Brand)
}
func (m *Mobile) Charge() string {
return fmt.Sprintf("%s is charging a %s phone.\n", m.User, m.Brand)
}
func (m *Mobile) Game(name string) {
fmt.Printf("%s is playing the game of '%s'.\n", m.User, name)
}
func TestExample(t *testing.T) {
newMobile := &Mobile{User: "John", Brand: "Huawei Meta 40 Pro", Prise: 8888.00}
var p IPhone = newMobile
p.CallUp()
p.Game("Card")
p.Storage()
fmt.Printf(p.Charge())
}
调用对比
代码示例:
package test
import (
"fmt"
"testing"
)
type IO interface {
Reader() IO
Writer(string) IO
}
type Disk struct {
storage string
}
func (d *Disk) Reader() IO {
fmt.Println(d.storage)
return d
}
func (d *Disk) Writer(s string) IO {
d.storage = s
return d
}
func TestExample(t *testing.T) {
disk := Disk{
storage: "Hi Bro~",
}
// 方式1
fmt.Println(disk.storage)
// 方式2
disk.Writer("Bonjour").Reader()
// 方式3
IO(&disk).Writer("Hola").Reader()
// 方式4
IO.Writer(&disk, "What's up, man?").Reader()
// 补充
var io IO = &disk // 创建了interface变量,并将其赋值为指向Disk类型的指针
io.Writer("你好").Reader()
// 总之,以上几种方式实现的效果是相同的,但它们的语法和用法略有不同。
}
方式对比:
序号 | 代码 | 描述 |
---|---|---|
方式1 | fmt.Println(disk.storage) | 直接打印struct实例的属性值(私有不建议直接使用)。 |
方式2 | disk.Writer("xxx").Reader() | 使用原生struct上绑定的方法,链式调用,将多个操作连接在一起,代码比较简洁。 |
方式3 | IO(&disk).Writer("xxx").Reader() | 通过将struct实例转换成接口类型来调用接口方法,适用于需要使用不同的实现类型来满足接口方法的情况,同样使用了方法链的方式,代码较为直观。 |
方式4 | IO.Writer(&disk, "xxx").Reader() | 直接调用接口类型的方法,传递实现类型的struct实例作为第一个参数传入,同样使用了方法链的方式,代码看起来也很简单。 |
💡 除了方式1
某些场景下不建议,方式2-4
这三种方式都可以使用,具体选择哪一种方式取决于个人的编码习惯和项目要求,可根据实际需求进行选择。
编程范式与风格的进化 🧬
当我们深入学习编程之后,了解不同的编程范式和风格是非常重要的。本篇章介绍了过程式、面向对象、函数式、面向接口等不同的编程范式和风格,以及它们的优缺点和适用场景。
面向过程编程
💡 以下是展示过程式编程的几段代码,分别为 Linux Bash Shell、脚本语言 Perl、动态语言 Python 以及 GC 静态语言 Go。
这些代码演示了过程式编程的特点,它们都以过程为中心,通过定义全局变量和函数来处理数据和完成任务。
无论是 “面向过程编程” 的编程范式,亦或是 “过程式编程” 的编程风格,都仅适合新手村,此地不宜久留。这种方式的程序代码比较直观,但是代码可重用性比较差,随着程序规模的扩大,代码会变得越来越难以维护。
Bash Shell
Bash 是一种 Unix/Linux 操作系统下的命令行 shell。它可以通过命令行解释器或脚本文件来使用,用于执行各种系统管理、文件处理和自动化任务等。Bash 具有很多强大的功能,包括管道、重定向、变量扩展、命令替换、条件语句、循环、函数等。通过这些功能,Bash 可以方便地完成各种任务,比如文件搜索、文本处理、数据操作、进程管理、系统配置等等。Bash 是 Linux 操作系统中最常用的 shell,因为它易于学习和使用,同时也有丰富的文档和社区支持。
我们开启 Bash
的 Shell options
,这样可以让脚本在运行时更加严谨,有助于捕捉错误,可以避免一些潜在的错误。
#!/bin/bash
set -eu # 在脚本出现错误或存在未使用变量时终止运行
declare -g disk="旧数据" # 声明了全局变量
function Read() {
echo "${disk}"
}
function Write() {
disk="$1"
return
}
function main() {
# 读
Read # 旧数据
# 写
Write "新数据" # 新数据
# 读
Read # 新数据
}
main
#EOF
Perl
Perl 是一种跨平台的脚本语言,广泛应用于 Linux 和 Unix 系统中的系统管理、文本处理、网络编程等方面。它的设计初衷是为了更方便地处理文本数据,因此具有强大的正则表达式(Regular expression)支持。作为一种很古老的语言,Perl 的语法和语言特性对其他编程语言产生了影响,尤其是在正则表达式方面。
在 Perl
中,我们通过全局变量来实现修改数据的功能,例如:
#!/usr/bin/perl
use strict;
use warnings;
my $disk = "旧数据";
sub Read {
return $disk;
}
sub Write {
my $new_disk = $_[0];
$disk = $new_disk;
return;
}
sub main {
# 读
print Read() . "\n"; # 旧数据
# 写
Write("新数据");
# 读
print Read() . "\n"; # 新数据
}
main();
Python
Python 是一门高级编程语言,具有简单易学、可读性强、语法简洁、面向对象等特点。Python 自问世以来,其简洁易读、易于维护和丰富的生态系统得到了广泛的认可。
Perl 与 Python 在某些方面类似,比如都具有强大的文本处理和正则表达式功能。但是,Python 在易用性、可读性、模块化等方面更具优势。此外,Python 的语法设计也更加符合人类思维习惯,让开发者能够更加便捷地编写高质量的代码。因此,Python 的出现使得 Perl 逐渐退出了世界舞台,尤其是在 Web 开发等领域中,Python 的应用越来越广泛,成为了更受欢迎的编程语言之一。
在 Python
中,我们通过全局变量来实现修改数据的功能,例如:
# -*- coding: utf-8 -*-
disk = "旧数据"
def read():
print(disk)
def write(s: str) -> bool:
global disk
disk = s
return True
if __name__ == '__main__':
# 读
read() # "旧数据"
# 写
write("新数据")
# 读
read() # "新数据"
Go
为什么选择 Go?
- 高性能:Go 的运行速度非常快,编译速度也很快,这使得它非常适合处理高并发的网络应用程序。
- 并发支持:Go 内置支持并发编程,使用 Goroutine 和 Channel 实现,这使得编写高并发的程序变得简单和容易。
- 内存管理:Go 的内存管理由垃圾回收器自动处理,程序员不需要手动管理内存,避免了一些常见的内存问题,如内存泄漏和野指针。
- 跨平台支持:Go 可以在多种操作系统上编译和运行,包括 Windows、Linux、macOS 等,这使得它非常适合构建跨平台应用程序。
- 静态类型:Go 是一种静态类型语言,编译器会在编译时进行类型检查,这可以帮助程序员避免一些常见的类型错误。
- 简单易学:Go 的语法和结构非常简单,学习曲线较为平缓,即使没有很多编程经验的程序员也能够快速上手。
综上所述,Go 是一种非常适合构建高性能、高并发、跨平台应用程序的语言,同时它还具有简单易学、内存管理自动化等优点,因此被越来越多的开发者所青睐。
在 Go
语言中,我们通过全局变量来实现修改数据的功能,例如:
package main
var (
disk = "旧数据"
)
func read() {
println(disk)
}
func write(s string) bool {
disk = s
return true
}
func main() {
// 读
read() // "旧数据"
// 写
write("新数据")
// 读
read() // "新数据"
}
面向对象编程
💡 面向对象编程是一种重要的编程思想,它的核心是将数据和方法绑定在一起形成对象,实现更加模块化和可维护的代码结构。在面向对象编程中,通常使用绑定方法的方式来修改对象的属性和字段,这种写法更加符合面向对象编程的思想,也更加便于扩展和维护。相比之下,直接修改原生字段可能会导致代码可读性和可维护性降低,通常不是很建议使用。
在像 Java、C# 这样的面向对象语言中,绑定方法是非常常见的写法。在 Python 和 Go 中,虽然语言本身并不是完全面向对象的语言,但是也可以通过绑定方法的方式来实现对象的属性和字段的修改。在 Go 中,虽然没有像 Java 那样的方法重载,但是使用绑定方法的方式仍然是开发人员最常用和偏爱的方式之一,因为它能够让代码更加简洁、清晰易懂,也更加方便调用。
阶段一
阶段一:直接修改属性和字段。在这一阶段,Linux Shell 脚本将被抛弃,因为它不具备这样的能力。相反,像 Java 和 C# 这样的面向对象语言被广泛使用。在本文中,我们演示了 Python 和 Go(使用 Struct 模拟面向对象)的写法。
Python
在 Python
中,我们通常通过修改对象的属性值来实现对数据的修改,例如:
# -*- coding: utf-8 -*-
class Storage(object):
def __init__(self, disk: str):
self.disk = disk
if __name__ == '__main__':
# 初始化类
store = Storage("旧数据")
# 读
print(store.disk) # "旧数据"
# 写
store.disk = "新数据"
print(store.disk) # "新数据"
Go
在 Go
语言中,我们通常通过修改结构体指针字段的值来实现对数据的修改,例如:
package main
type Storage struct {
disk string
}
func main() {
// 初始化指针结构体
store := &Storage{
disk: "旧数据",
}
// 读
println(store.disk) // "旧数据"
// 写
store.disk = "新数据"
println(store.disk) // "新数据"
}
阶段二
阶段二:使用绑定方法。在这一阶段,对象可以拥有方法来修改属性和字段。在 Python 和 Go 中,我们演示了如何创建绑定方法。
Python
这段代码展示了 Python
中的一些面向对象编程的特性,包括属性的访问控制、@property
装饰器以及私有属性的实现。同时,它也演示了 Python
中简洁的语法和灵活的调用方式。
# -*- coding: utf-8 -*-
class Storage(object):
def __init__(self, disk: str):
self._disk = disk # 一个受保护的属性,用于存储数据,但是不建议外部直接访问。
self.__disk = disk # 一个私有的属性,用于隐藏原始属性。
@property
def read(self) -> str:
""" 除了 self (其他编程语言的this) 外,没有多余的入参,可通过绑定方法变只读属性的 property 装饰器来进行调用 """
return self.__disk
def write(self, s: str) -> bool:
self.__disk = s
return True
if __name__ == '__main__':
# 初始化类
store = Storage("旧数据")
# 读
print(store.read) # "旧数据"
# 写
store.write("新数据")
# 读
print(store.read) # "新数据"
# Python加餐知识(了解即可,不重要):
print(store._disk) # 外部访问 obj._disk 属性是没问题的!""
print(store.__disk) # 如果外部直接访问 obj.__disk 会报错!AttributeError: 'Storage' object has no attribute '__disk'
print(store._Storage__disk) # Python 没有实际意义上的私有变量,可通过 _{CLASS_NAME}__{attr_name} 来调用,不建议!
Go
这段代码演示了如何在 Go
中使用结构体和方法来实现面向对象编程的思想。
package main
type Storage struct {
disk string
}
// 为结构体绑定方法
// 需要注意的是,这些方法的接收者是指针类型的结构体,这意味着它们可以修改结构体的属性值。
func (s *Storage) Read() {
println(s.disk)
}
func (s *Storage) Write(v string) {
s.disk = v
}
func main() {
// 初始化结构体
store := &Storage{
disk: "旧数据",
}
// 读
store.Read() // "旧数据"
// 写
store.Write("新数据")
// 读
store.Read() // "新数据"
}
阶段三
阶段三:使用链式调用。在这一阶段,我们使用链式调用对前面的代码进行了改进。链式调用是一种编码风格,其中在同一行中依次调用多个方法,将每个方法的结果传递给下一个方法,以形成一条链。要求每个方法都返回当前对象实例,以便在同一行中调用其他方法。这种风格可以使代码更加简洁,易于阅读,且适用于许多场景。
Python
这段代码展示了 Python
中基于链式调用的面向对象编程方式。首先定义了一个 Storage
类,其中包含了读和写的方法,通过在方法的返回值中返回 self
,实现了链式调用的方式。
提示:解释器语法没问题,可以正常使用,但 PyCharm IDE
可能不太给力,.
不出来链式的方法。
# -*- coding: utf-8 -*-
from typing import Any
class Storage(object):
def __init__(self, disk: str) -> None:
self._disk = disk
def read(self) -> Any:
print(self._disk)
return self
def write(self, s: str) -> Any:
self._disk = s
return self
if __name__ == '__main__':
# 实例化类 "旧数据" "新数据"
Storage("旧数据").read().write("新数据").read()
Go
这段代码展示了 Go
语言中基于链式调用的面向对象编程方式。
额外提一下,在 Go
语言中,构造函数是一种特殊的函数,用于创建和初始化类的对象。构造函数通常用于初始化对象的成员变量和数据成员,以确保对象在创建后处于一种良好的状态。类比在 Python
中,构造函数是以 __init__
方法的形式实现的。
package main
type Storage struct {
disk string
}
// 构造函数初始化结构体
func NewStorage(disk string) *Storage {
return &Storage{disk: disk}
}
// 由于返回了结构体指针本身,所以具有链式调用的属性
func (s *Storage) Read() *Storage {
println(s.disk)
return s
}
func (s *Storage) Write(v string) *Storage {
s.disk = v
return s
}
func main() {
// 构造函数 "旧数据" "新数据"
NewStorage("旧数据").Read().Write("新数据").Read()
}
函数式编程
💡 当谈到函数式编程时,通常涉及到以下几个核心思想:
- 函数是第一类对象,可以作为参数、返回值等。
- 避免副作用,即避免修改状态或共享状态。
- 不可变性,即不修改已有的数据,而是创建新的数据。
- 高阶函数,即接受函数为参数或返回函数的函数。
Python
下面是一个简单的 Python
示例,演示了一些函数式编程的概念:
# -*- coding: utf-8 -*-
# 函数是第一类对象
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def apply(func, a, b):
return func(a, b)
print(apply(add, 2, 3)) # 输出 5
print(apply(multiply, 2, 3)) # 输出 6
# 避免副作用
def sum(numbers):
total = 0
for number in numbers:
total += number
return total
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # 输出 15
# 不可变性
def increment(numbers):
return [number + 1 for number in numbers]
numbers = [1, 2, 3, 4, 5]
print(increment(numbers)) # 输出 [2, 3, 4, 5, 6]
# 高阶函数
def multiply_by(factor):
def multiply(number):
return number * factor
return multiply
double = multiply_by(2)
print(double(3)) # 输出 6
triple = multiply_by(3)
print(triple(3)) # 输出 9
Go
函数选项模式是一种使用函数选项作为参数来配置函数或方法的方式,它并不是严格的函数式编程。但是它借鉴了函数式编程的思想,将可选参数封装在函数选项中,并将其作为函数或方法的参数传递,这种方式使得函数或方法的调用更加清晰、灵活和可扩展。
下面我们用 Go
的函数选项来改写 Storage
示例,演示如何使用函数式编程思想对结构体进行操作。
package main
type Storage struct {
disk string
}
func New(s string) *Storage {
return &Storage{disk: s}
}
// 定义了 StorageFunc 新类型,它表示一个操作 *Storage 的函数,其输入为 *Storage,输出为 *Storage。
type StorageFunc func(*Storage) *Storage
type StorageChain []StorageFunc
// 定义了 Read 函数,它返回 StorageFunc 类型的函数,表示对 *Storage 的读操作。
func Read() StorageFunc {
return func(s *Storage) *Storage {
println(s.disk)
return s
}
}
// 定义了 Write 函数,它返回 StorageFunc 类型的函数,表示对 *Storage 的写操作。
func Write(v string) StorageFunc {
return func(s *Storage) *Storage {
s.disk = v
return s
}
}
// 定义了 Apply 函数,它接收一个 *Storage 和一系列的 StorageFunc,然后对 *Storage 应用这些函数。
func (s *Storage) Apply(funs ...StorageFunc) {
for _, fn := range StorageChain(funs) {
fn(s)
}
}
func main() {
store := New("旧数据")
store.Apply(
Read(),
Write("新数据"),
Read(),
)
}
面向接口编程
💡 当我们使用接口编程时,通常需要遵循以下几个原则:
- 面向接口编程,而非具体实现。即我们的代码应该依赖于接口,而非具体实现。这样可以增强代码的灵活性和可扩展性,方便后续更换或者新增实现。
- 接口应该尽可能地小。接口的设计应该尽可能地小,只包含必要的方法。这样可以避免接口臃肿和不必要的复杂性,使得实现更加清晰明了。
- 接口应该尽可能地通用。接口的设计应该尽可能地通用,不依赖于具体实现细节。这样可以提高代码的可复用性和可测试性,使得代码更加健壮和易于维护。
- 接口的实现应该尽可能地简单。接口的实现应该尽可能地简单,避免不必要的复杂性和耦合。同时,实现应该符合接口的约定,保证代码的正确性和稳定性。
总之,面向接口编程是一种良好的编程实践,可以提高代码的质量和可维护性。在实际开发中,我们应该尽可能地遵循接口编程的原则,根据实际需求设计合适的接口,提高代码的可复用性和可扩展性,让我们的代码更加健壮和易于维护。
在 Go 语言中,实现和调用方式有很多种,具体选择使用哪种方式取决于个人的编码习惯以及项目的要求。因此,根据实际需求进行选择。请记住,代码是死的,人是活的,不要思维定势!
Python
在这个示例中,我们使用了 Python
的 ABC
模块来定义了一个 StorageInterface
接口类,并在 Storage
类中继承了它,实现了 read
和 write
方法,来达到了面向接口编程的目的。
# -*- coding: utf-8 -*-
from abc import ABC, abstractmethod
class IStorage(ABC):
"""
abstractmethod 是 Python 语言中的一个装饰器,用于声明一个抽象方法,
即一个没有具体实现的方法,需要在子类中进行实现。
"""
@abstractmethod
def read(self) -> str:
pass
@abstractmethod
def write(self, s: str) -> bool:
pass
class Storage(IStorage):
def __init__(self, disk: str):
self._disk = disk
@property
def read(self) -> str:
return self._disk
def write(self, s: str) -> bool:
self._disk = s
return True
if __name__ == '__main__':
# 实例化类
store = Storage("旧数据")
# 读
print(store.read) # "旧数据"
# 写
store.write("新数据")
# 读
print(store.read) # "新数据"
Go
Go
语言和其他像 Java
这类语言相比,有一个非常不同的地方,那就是在处理接口方面。在 Java
中,你需要在你的实现类上声明你实现了哪个接口,而在 Go
中,只要你实现了接口中的方法,就默认你实现了这个接口,不需要再显式地声明。因此,在某种程度上,Go
更容易使用面向接口编程。
实现1:
package test
import (
"testing"
)
// 接口,定义实现的方法
type IStorage interface {
Read() *Storage
Write(v string) *Storage
}
// 结构体
type Storage struct {
disk string
}
// 结构体方法,实现了 IStorage 的 Read() 方法
func (s *Storage) Read() *Storage {
println(s.disk)
return s
}
// 结构体方法,实现了 IStorage 的 Write() 方法
func (s *Storage) Write(v string) *Storage {
s.disk = v
return s
}
// 用法1: 每次调用接口方法时都进行了转换,这样做会导致性能和代码可读性的下降,不推荐!
func TestUsage1(t *testing.T) {
store := Storage{disk: "旧数据"}
IStorage(&store).Read()
IStorage(&store).Write("新数据")
IStorage(&store).Read()
}
// 用法2: 问题同上,不推荐!
func TestUsage2(t *testing.T) {
store := Storage{disk: "旧数据"}
IStorage.Read(&store)
IStorage.Write(&store, "新数据")
IStorage.Read(&store)
}
// 用法3: 可以在第一次转换之后在后面的操作中直接使用接口类型,避免了重复的类型转换。
// 这是一个Go中相对比较好的实现方式。
func TestUsage3(t *testing.T) {
// 拆解写法
store := Storage{disk: "旧数据"}
var Storager IStorage = &store
Storager.Read()
Storager.Write("新数据")
Storager.Read()
}
// 用法4: 第一种方式和第二种方式实际上是等效的,只是语法上略有不同。
// 如果你想更加强调链式调用的方式,可以选择方式1。
// 如果你更喜欢传统的函数调用方式,可以选择方式2。
func TestUsage4(t *testing.T) {
// 链式调用,只传一遍结构体指针即可,后面会自动类型断言
store := Storage{disk: "旧数据"}
// 方式1
// 先将指针转换为接口类型 IStorage,然后直接链式调用该接口的方法,即 Read(),Write(),Read(),因为接口类型的方法在调用时会自动进行类型断言。
IStorage(&store).Read().Write("新数据").Read()
// 方式2
// 直接调用接口类型的方法,即 IStorage.Read(&store),然后链式调用其它方法,即 Write("新数据"),Read()。
IStorage.Read(&store).Write("新数据").Read()
}
实现2:
package main
// 在这种实现方式中,IStorage 接口的方法返回值都是 IStorage,
// 因此每个方法的返回值可以是其实现的结构体指针,也可以是其他实现了该接口的结构体指针。
// 这种方式可以避免使用具体的类型返回值,更加灵活。
// 总的来说,这种方式也是比较地道和常见的实现方式之一,可以根据实际需要选择使用。
type IStorage interface {
Read() IStorage
Write(v string) IStorage
}
type Storage struct {
disk string
}
// 通过工厂函数 Storager 来创建实现了 IStorage 接口的结构体指针,
// 这样可以避免直接创建结构体指针,增加代码的可读性。
func Storager(disk string) IStorage {
return &Storage{disk: disk}
}
func (s *Storage) Read() IStorage {
println(s.disk)
return s
}
func (s *Storage) Write(v string) IStorage {
s.disk = v
return s
}
func main() {
Storager("旧数据").Read().Write("新数据").Read()
}
Rust
在 Rust
中,接口使用 trait
来定义,实现接口的结构体需要使用 impl trait_name
来实现 trait
中的方法。辅助函数 create_storage
返回的是 Box<dyn IStorage>
类型,表示可以存储实现了 IStorage
接口的任何类型,这也是 Rust 中的动态多态性。使用时直接调用 trait
中的方法即可,类似于其他语言中的方法链式调用。
// 定义接口
trait IStorage {
fn read(&mut self) -> &mut dyn IStorage;
fn write(&mut self, v: &str) -> &mut dyn IStorage;
}
// 实现接口的结构体
struct Storage {
disk: String,
}
impl IStorage for Storage {
fn read(&mut self) -> &mut dyn IStorage {
println!("{}", self.disk);
self
}
fn write(&mut self, v: &str) -> &mut dyn IStorage {
self.disk = String::from(v);
self
}
}
// 辅助函数,用于创建 Storage 对象
fn create_storage(disk: &str) -> Box<dyn IStorage> {
Box::new(Storage {
disk: String::from(disk),
})
}
// 使用接口
fn main() {
create_storage("旧数据").read().write("新数据").read();
}