面向对象初探

在软件开发领域,你应该听到过过程式编程、面向对象编程、甚至函数式编程等软件开发方式。而面向对象编程更是在现今大行其道,JAVA就是面向对象语言的代表,在JAVA中一切皆对象,它让编程中的一切元素、甚至设计方式都标准化,这更有利于大型应用的编写。

什么是面向对象编程?

面向对象编程,简称OOP。在OOP的理念下,任何事物无论简单还是复杂都可以用对象表示,每个对象都包含属性和方法,属性表示对象是什么?有什么特征?方法表示对象能做什么?有什么能力?任何应用的构建都转化成对象关系的设计,这演化成一套标准化的面向对象设计模式。

类和对象
要理解OOP,首先要理解类和对象的关系,类是设计层面的概念,而对象则是程序运行时的概念,OOP程序设计基于类的设计,类在程序运行时实例化为对象实现真正的业务逻辑。简而言之,所谓类可以理解成对象的模板,你编写一个类,在运行时需要实例化才能在程序调用栈中传递。

属性和方法
在过程式编程中,我们熟悉变量和函数,使用这些基本元素我们实现业务逻辑。而属性和方法是对象内部的特征,咋一看他们很像,其实本质上是将实现特定功能的函数和变量封装成一个整体,即对象。一个对象包含一系列的属性和方法专注于实现某种特定功能。

接口
接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能),接口是多态实现的基础。

面向对象三大特性

封装

将实现特定功能的属性和方法抽象封装成类,提供public/private/protected访问修饰符,控制外部访问的可见性。

  • 对业务相近的变量和函数封装在类/结构体中
  • 变量=>类/结构体的属性
  • 函数=>类/结构体的方法

继承

对已经实现的类或接口提供重用或扩展的能力,子类可完全继承父类的所有能力。继承的过程,就是从一般到特殊的过程,其过程可以通过继承和组合来实现。

  • 继承的类拥有父类/父结构体的全部属性和方法
  • 接口继承则是一行代码拥有父类接口的全部抽象方法
  • 继承可以节约大量重复代码
  • 继承的目的:
    • 提高代码的复用度
    • 拓展出新的属性和方法
    • 改进父类/结构体的方法
    • 以覆写父类/结构体的方法类实现

多态

多态是指一个父类/接口可以拥有多种具体的子类实现形态;多态的好处是可以根据业务需要去方便地调度子类们的共性和个性

面向对象编程的优缺点

优点

高效:面向对象设计结构、模块清晰的应用,有利于大型应用的开发,团队成员各自维护局部模块,降低了成员开发的复杂性。
易维护:由于高内聚低耦合,各个模块的维护都是局部的,这非常方便定位问题。
易扩展:继承、封装、多态的特性,以及标准化的设计模式设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。

缺点

俗话说,“如果你手上只有锤子,那么你看什么都是钉子”,OOP把一切都当成对象,但现实世界是复杂的,虽然设计模式就是解决应用中抽象的设计问题的。这把编码阶段的复杂性提到设计阶段。

相对过程式编程,面向对象编程的程序结构性能有所下降;
提高系统设计的复杂度;

Go的面向“对象”

了解了面向对象编程的思想,我们再来看Go的面向对象,严格来说,Go并非面向对象编程语言,Go有自己的设计理念,面向对象只是一种软件开发方法,Go有自己的支持方式。

没有类和对象,只有类型和值

传统的JAVA类和对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//类定义
public class Person {
private String name
...
//构造方法
public Person( String name){
this.name = name
}
//公开方法
public String getName (){
return this.name
}
...
}

//类实例化成对象
Person p = new Person("name")
//调用并打印公开方法
System.out.print(p.getName())

Go的类型和值:

  • 通过定义结构体类型的方式实现类似类的结构
  • 没有构造方法,直接使用NewXXX()工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//类型定义
type Preson struct {
name string
}
//类型方法
func (p *Person) SetName(name string) {
p.name = name
}

//类型方法
func (p *Person) GetName() string {
return p.name
}

//工厂方法
func NewPerson(name string) *Person{
p := new(Person)
p.SetName(name)
return p
}

//获取person类型的值指针
p := NewPerson("name")
fmt.Println(p.GetName())

聚合和嵌入优于继承

传统的JAVA继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student extend Person{
private String school

//构造方法
public Student(){
super() //直接使用父类的构造方法
}

public void doSomething(){
//block
}
...
}

Go的聚合和嵌入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type Preson struct {
Name string
age int
}
func (p *Person) SetAge(age int){
p.age = age
}

//嵌入
type Student1 struct {
Person //匿名字段为嵌入类型
School string
}

//聚合
type Student2 struct {
Ps Person //命名字段为聚合类型
School string
}

func OOPDemo() {
s1 := new(Student1)
s1.Name = "fun1" //嵌入的类型可直接使用其内部属性,更像继承
s1.School = "Social University1"
s1.SetAge(18) //可以直接使用Person的方法

s2 := new(Student2)
s2.Ps.Name = "fun2" //聚合的类型需要先访问属性值名,在访问属性值内部的属性
s2.School = "Social University2"
s1.Ps.SetAge(18) //可以间接使用Person的方法

//OUTPUT:
//s1: &{{fun1} Social University1}
//s2: &{{fun2} Social University2}
}

自由的结构体属性类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//接口
type IPerson interface {
SetName(string)
GetName() string
}
//自定义函数
type MyFuncType func(int) int

//大杂烩结构体
type Something struct {
a int //基本数据类型
b []byte //切片
p Person //结构体
s *Student //指针
i IPerson //接口
f MyFuncType //自定义函数类型
any interface{} //任意类型
}

独立的方法定义更灵活

Go的类型方法在外部任意地方,只要定义的方法接收者为该类型,那定义的方法就是该类型的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//类型定义
type Preson struct {
name string
}
//类型方法,接收者为该类型的指针
func (p *Person) SetName(name string) {
p.name = name
}
//类型方法,接收者为该类型的值
func (p Person) GetName() string {
return p.name
}

//调用示例
p := new(Person)
//SetName()方法接收者为指针,使用指针类型或值类型去调用都可以
p.SetName("fun") //可以
*p.SetName("func") //指针取值后再去调用也可以。

//GetName()方法接收者为值类型,所以调用该方法只能为值
p.GetName() //不可以
*p.GetName() //可以

方法接收者一般有两种情况:

  • 接收者为指针:允许该类型的指针和值调用该方法;
  • 接收者为值:只允许该类型的值调用该方法。

一般无特殊需要,建议把接收者直接设置为指针类型

没有显式public/private/protected,只有隐式大小写控制

Go的访问控制基于包,包内的成员变量、常量、类型、函数基于命名首字母的大小写控制。
其结构体类型的属性方法也类似,基于命名首字母的大小写控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Preson struct {
Name string //外部可见并可修改
age int //仅内部可见内部方法修改
}

//类型方法,外部可见
func (p *Person) SetAge(age int) {
//调用内部方法
p.setage(name)
}
//类型方法,内部可见
func (p *Person) setage(age int) {
p.age = age
}

Go面向接口编程

严格意义讲,Go因没有对象概念,所以并非面向对象编程语言,但因其对OOP的深刻理解,使其设计理念更切合“面向接口”,接口的多态特性使其在设计高内聚低耦合的系统发挥更重要的作用。面向接口概念是面向对象的衍生,在多年的开发积累中,人们发现针对接口设计系统可以让系统扩展性和维护性更好。因此,Go的OOP针对接口设计,可以说接口是头等的类型也不为过。

在Go语言中,接口拥有举足轻重的地位,而面向接口编程也是Go语言核心的设计理念。接口是高度抽象的概念,它是一种类型可由type关键字声明,接口内部声明一个或多个方法签名,因此不能实例化,一般创建一个类型为接口的变量,它可以被赋值为任何满足该接口声明的实际类型的值,作为类型传递。

接口 —— 实现鸭子类型

当它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。

接口本身是类型,但它却不关心类型,它只关心行为,如果类型T的行为(实现的方法)和定义的接口I声明的方法签名符合,那么类型T就实现了I的接口。在方法参数传递或各种类型校验中,T就是I的实现。

一个Go接口也是类型定义,其内部声明了其规定的方法签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//任何实现了IAnimal签名方法的类型都属于IAnimal类型
type IAnimal interface {
Live()
Dead()
}

//Humen结构体
type Monkey struct {}
func (m *Monkey) Live() {
fmt.Println("猴子活着吃香蕉!!!")
}
func (m *Monkey) Dead() {
fmt.Println("香蕉有毒,猴子死了!!!")
}
//Cat结构体
type Cat struct {}
func (c *Cat) Live() {
fmt.Println("猫活着吃鱼!!!")
}
func (c *Cat) Dead() {
fmt.Println("猫吃了河豚死翘翘!!!")
}

//演示:
//声明一个IAnimal的切片
var zoo []IAnimal
func addAnimal(animal IAnimal) {
zoo = append(zoo,animal)
}
func OOPDemo02(){
m := new(Monkey)
c := new(Cat)
addAnimal(m)
addAnimal(c)

//遍历
for _,animal := range zoo {
animal.Live()
animal.Dead()
}
}

//OUTPUT:
//猴子活着吃香蕉!!!
//香蕉有毒,猴子死了!!!
//猫活着吃鱼!!!
//猫吃了河豚死翘翘!!!

接口类型限定赋值

类型值赋值给接口类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type IPerson interface{
GetName()
SetName()
}

//任何实现GetName()、SetName()方法的类型都可赋值给person
var person IPerson


接口类型赋值给另一接口类型

type Writer interface{ //父接口
Write(buf []byte) (n int,err error)
}
type ReadWriter interface{ //子接口
Read(buf []byte) (n int,err error)
Write(buf []byte) (n int,err error)
}
var file1 ReadWriter=new(File) //子接口实例
var file2 Writer=file1 //子接口实例赋值给父接口

非侵入式接口

上面演示我们看到,接口的运用在编码中是非侵入式的,在经典的OOP语言中,实现接口需要类显式实现,例如:

1
2
3
public class Person implements IPerson {
//block
}

而Go并不需要显式实现,类型只需实现特定接口的方法签名即可。当然这种极度宽松的实现方式有可能让你定义的类型“不小心”就实现了某些接口能力,一旦你的类型方法和某些接口方法签名一致时就会如此。

1
2
3
4
5
6
//该类型实现了标准包的io.Writer接口
type F struct {}

func (f *F) Write(p []byte) (n int, err error) {
//block
}

接口嵌入

和结构体类型类似,接口也可以嵌入其他接口,接口只能嵌入不能聚合!

以下演示接口嵌入的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//生物接口
type IBeing interface{
Live()
Dead()
}
//动物接口嵌入生物接口
type IAnimal interface {
IBeing
Hunting()
}
//植物接口嵌入生物接口
type IPlant interface {
IBeing
Growing()
}

type Tiger struct {}
func (t *Tiger) Live() {
fmt.Println("老虎活着称大王!!!")
}
func (t *Tiger) Dead() {
fmt.Println("老虎战斗死了!!!")
}
func (t *Tiger) Hunting() {
fmt.Println("老虎捕猎!!!")
}

type Flower struct{}
func (t *Flower) Live() {
fmt.Println("花儿享受阳光!!!")
}
func (t *Flower) Dead() {
fmt.Println("花儿落下死了!!!")
}
func (t *Flower) Growing() {
fmt.Println("花儿茁壮成长!!!")
}


//声明一个interface{}的切片
var earth []interface{}

func addBeing(b interface{}) {
earth = append(earth, b)
}
func OOPDemo03() {
tiger := new(Tiger)
flower := new(Flower)
addBeing(tiger)
addBeing(flower)

//遍历
for _, being := range earth {
if animal, ok := being.(IAnimal); ok {
animal.Live()
//如果是动物则捕猎
animal.Hunting()
animal.Dead()
}
if plant, ok := being.(IPlant); ok {
plant.Live()
//如果是植物则成长
plant.Growing()
plant.Dead()

}

}
}

//OUTPUT:
//老虎活着称大王!!!
//老虎捕猎!!!
//老虎战斗死了!!!

//花儿享受阳光!!!
//花儿茁壮成长!!!
//花儿落下死了!!!

类型断言

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言

1
2
3
4
5
//试着转一下类型,不成功也不会报错
plant, ok := being.(IPlant)

//转一下类型,不成功就会panic
plant := being.(IPlant)

OOP

面向接口编程和简单的依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
fei@feideMacBook-Pro faceinter % tree ./
./
├── app
│   └── app.go
├── data
│   └── data.go
└── main.go

//data层
package data

import "fmt"

type Sql interface {
Open() error
}

type mysql struct {
url string
}

func Newmysql(url string) *mysql {
return &mysql{url: url}
}

func (sql *mysql) Open() error {
fmt.Println("打开了", sql.url)
return nil
}

type mgdb struct {
url string
}

func Newmgdb(url string) *mgdb {
return &mgdb{url: url}
}

func (sql *mgdb) Open() error {
fmt.Println("打开了", sql.url)
return nil
}

//app层
package app

import (
"faceinter/data"
"fmt"
)

type App interface {
Sum(int, int) int
}

type app struct {
mydesql data.Sql //依赖interface类型
}

func NewAppIns(mydesql data.Sql) App { //不依赖具体实现将mydesql字段当作依赖传递进来
return &app{
mydesql: mydesql,
}
}

func (app1 *app) Sum(a, b int) int {
err := app1.mydesql.Open()
if err != nil {
panic(err)
}
fmt.Printf("%v + %v = %v\n", a, b, a+b)
return a + b
}

//main.go
package main

import (
"faceinter/app"
"faceinter/data"
)

// 依赖定义的接口而不依赖实现
func callAppFunc(ap app.App) {
ap.Sum(1, 2)
}

func main() {
newsql := data.Newmgdb("127.0.0.1:27017")
appins := app.NewAppIns(newsql) //app层依赖data层,通过data层私有构造函数做依赖注入

//将实现传递进去
callAppFunc(appins)
}


OUTPUT:
打开了 127.0.0.1:27017
1 + 2 = 3

依赖倒置原则
app中使用了接口data.Sql来定义数据库的连接,连接数据库依赖Sql的接口,而不依赖具体哪一种数据库的实现。
这样可以自由选择数据库,而不用动app层代码。只需要相关数据库实现了该接口,并通过db的私有构造函数传递给app的构造函数中的dbinterface。
好处:可扩展性好,以后添加其他数据库不影响app层的业务逻辑,即抽象不依赖细节。