文档章节

Go面向对象

秋风醉了
 秋风醉了
发布于 2017/11/01 21:12
字数 3165
阅读 74
收藏 0

Go面向对象

Go封装

Go语言中,封装有包范围的封装和结构体范围的封装。

在Java语言中,我们组织程序的方式一般是通过project-package-class。每个class,对应一个文件,文件名和class名相同。其实我觉得这样组织是很清晰也很直观的。

在Go语言中,只有一个package的概念。package就是一个文件夹。在这个文件夹下的所有文件,都是属于这个package的。这些文件可以任意起名字,只要在文件头加上package名字

package handler

那么这个文件就是属于这个package的。在package内部所有的变量是互相可见的,是不可以重复的。

你可以这样理解:文件夹(package)就是你封装的一个单元(比如你想封装一个Handler处理一些问题)。里边其实只有一个文件,但是为了管理方便,你把它拆成了好几个文件(FileHandler、ImageHandler、HTTPHandler、CommonUtils),但其实这些文件写成一个和写成几个,他们之间的变量都是互相可见的。

如果变量是大写字母开头命名,那么对包外可见。如果是小写则包外不可见。

Go中的另外一种封装,就是结构体struct。没错,类似C语言中的struct,我们把一些变量用一个struct封装在一起

type Dog struct {
	Name string
	Age  int64
	Sex  int
}

我们还可以给struct添加方法,做法就是把一个function指定给某个struct。

func (dog *Dog) bark() {

    fmt.Println("wangwang")

} 

 

继承

Java继承

Java语言中,继承通过extends关键字实现。有非常清晰的父类和子类的概念以及继承关系。Java不支持多继承。

Go继承

Go语言中其实并没有继承。但是Go中确实只是提供了一种伪继承,使用组合的方式,也就是结构体的嵌入,通过embedding实现的“伪”继承。

嵌入类型T

package animal

import "fmt"

type Person struct {
	Name string
	Age  uint8
}

func (p *Person) Say() {
	p.Name = "xiaoming@xxoo"
	fmt.Printf("I'm a person,name = %s,age = %d \n", p.Name, p.Age)
}

func (p Person) Work() {
	fmt.Println("work")
}

type Student struct {
	Person //嵌入普通类型
	Class string
}

测试方法,

package animal

import (
	"fmt"
	"testing"
)

func TestPerson_Say(t *testing.T) {

	s := Student{Person{}, "1年二班"}
	s.Name = "xiaoming"
	s.Age = 8
	fmt.Println(s.Name)
	s.Say()
	(&s).Say()
	fmt.Println("-------")
	s.Work()
	(&s).Work()
	fmt.Println(s.Name)
}

结构体Student嵌入了结构体Person,便自动拥有了Name 和 Age 属性,同时也能调用输入Person的方法 Say 和 Work。

嵌入指针类型*T

package animal

import "fmt"

type Person struct {
	Name string
	Age  uint8
}

func (p *Person) Say() {
	p.Name = "xiaoming@xxoo"
	fmt.Printf("I'm a person,name = %s,age = %d \n", p.Name, p.Age)
}

func (p Person) Work() {
	fmt.Println("work")
}

type Student struct {
	*Person //嵌入指针类型
	Class string
}

测试方法,

package animal

import (
	"fmt"
	"testing"
)

func TestPerson_Say(t *testing.T) {

	s := Student{&Person{}, "1年二班"}
	s.Name = "xiaoming"
	s.Age = 8
	fmt.Println(s.Name)
	s.Say()
	(&s).Say()
	fmt.Println("-------")
	s.Work()
	(&s).Work()
	fmt.Println(s.Name)
}

Student结构体嵌入了一个Person指针类型。

继承的总结

在Java的继承原则上,子类继承了父类,不光是子类可以复用父类的代码,而且子类是可以当做父类来使用的。参见面向对象六大原则之一的里氏替换原则。即在需要用到父类的地方,我用了一个子类,应该是可以正常工作的。

然而Go中的这种embedding,student和person完全是两个类型,如果在需要用person的地方直接放上一个student,编译是不通过的。

什么时候用继承,什么时候用组合呢?

  1. 除非考虑使用多态,否则优先使用组合。
  2. 要实现类似”多重继承“的设计的时候,使用组合。
  3. 要考虑多态又要考虑实现“多重继承”的时候,使用组合+接口。

 

多态

多态是面向对象编程中最重要的部分。

By the way,方法重载也是多态的一种。但是Go语言中是不支持方法重载的。

两种语言都支持方法重写(Go中的伪继承,Student如果重写了Person中的方法,默认是会使用Student的方法)。

不过要注意的是,在Java中重写父类的非抽象方法,已经违背了里氏替换原则。而Go语言中是没有抽象方法一说的。

 

Go中的多态采用和JavaScript一样的鸭式辨型:如果一只鸟走路像鸭子,叫起来像鸭子,那么它就是一只鸭子。

在Java中,我们要显式的使用implements关键字,声明一个类实现了某个接口,才能将这个类当做这个接口的一个实现来使用。

在Go中,没有implements关键字。只要一个struct实现了某个接口规定的所有方法,就认为它实现了这个接口。

Go代码:

package animal

import "fmt"

type Animal interface {
	bark()
}

type Dog struct {
	Name string
	Age  int64
	Sex  int
}

func (dog *Dog) bark() {
	fmt.Println("wangwang")
}

如上代码,Dog实现了Animal接口,无需任何显式声明。

让我们先从一个简单的多态开始。猫和狗都是动物,猫叫起来是miaomiao的,狗叫起来是wagnwang的。

Java多态实现

package common.test;

public class AnimalTest {

    public static void main(String[] args) {
        Animal animal;
        animal = new Cat();
        animal.shout();

        animal = new Dog();
        animal.shout();
    }
}


abstract class Animal {
    abstract void shout();
}


class Cat extends Animal {
    public void shout() {
        System.out.println("miaomiao");
    }

}


class Dog extends Animal {
    public void shout() {
        System.out.println("wangwang");
    }
}

输出如下:

miaomiao
wangwang

但是我们在继承的部分已经说过了,Go的继承是伪继承,“子类”和“父类”并不是同一种类型。如果我们尝试通过继承来实现多态,是行不通的。

Go多态实现

Go里边的接口是鸭式辩型,代码如下,

package animal

import "fmt"

type Animal interface {
	shout()
}

type Cat struct {
}

type Dog struct {
}

func (c *Cat) shout() {
	fmt.Println("miaomiao")
}

func (d *Dog) shout() {
	fmt.Println("wangwang")
}

定义 Animal 接口 ,声明了一个方法 shout。

测试方法,

package animal

import "testing"

func TestAnimal_Shout(t *testing.T) {
	var animal Animal

	animal = &Cat{}
	animal.shout()

	animal = &Dog{}
	animal.shout()
}

输出如下,

miaomiao
wangwang

 

其实就算是在Java里,如果不考虑代码复用,我们也是首先推荐接口而不是抽象类的。

那我们为什么不直接都用接口呢?还要继承和抽象类干什么?这里我们来捋一捋一个老生常谈的问题:接口和抽象类的区别。

这里引用了知乎用户chao wang的观点。感兴趣的请前往他的回答

abstract class的核心在于,我知道一类物体的部分行为(和属性),但是不清楚另一部分的行为(和属性),所以我不能自己实例化(不知道的这部分)。如我们的例子,abstract class是Animal,那么我们可以定义他们胎生,恒定体温,run()等共同的行为,但是具体到“叫”这个行为时,得留着让非abstract的狗和猫等等子类具体实现。

interface的核心在于,我只知道这个物体能干什么,具体是什么不需要遵从类的继承关系。如果我们定一个Shouter interface,狗有狗的叫法,猫有猫的叫法,只要能叫的对象都可以有shout()方法,只要这个对象实现了Shouter接口,我们就能把它当shouter使用,让它叫。

所以abstract class和interface是不能互相替代的,interface不能定义(它只做了声明)共同的行为,事实上它也不能定义“非常量”的变量。而abstract class只是一种分类的抽象,它不能横跨类别来描述一类行为,它使得针对“别的分类方式”的抽象变得无法实现(所以需要接口来帮忙)。

考虑这样一个需求:猫和狗都会跑,并且它们跑起来没什么区别。我们并不想在Cat类和Dog类里边都实现一遍同样的run方法。所以我们引入一个父类:四足动物Quadruped

Java代码:

package common.test;

public class AnimalTest {

    public static void main(String[] args) {
        Animal animal;

        animal = new Cat();

        animal.shout();

        animal.run();

        animal = new Dog();

        animal.shout();

        animal.run();
    }
}


interface Animal {
    void shout();

    void run();
}


abstract class Quadruped implements Animal {

    abstract public void shout();

    public void run() {
        System.out.println("running with four legs");
    }

}


class Cat extends Quadruped {
    public void shout() {
        System.out.println("miaomiao");
    }

}


class Dog extends Quadruped {
    public void shout() {
        System.out.println("wangwang");
    }
}

输出如下,

miaomiao
running with four legs
wangwang
running with four legs

Go语言中是没有抽象类的,那我们尝试用Embedding来实现代码复用:

package animal

import "fmt"

type Animal interface {
	shout()
	run()
}

type Quadruped struct {
}

type Cat struct {
	Quadruped //嵌入类型
}

type Dog struct {
	Quadruped // 嵌入类型
}

func (q *Quadruped) run() {
	fmt.Println("running with four legs")
}

func (c *Cat) shout() {
	fmt.Println("miaomiao")
}

func (d *Dog) shout() {
	fmt.Println("wangwang")
}

测试方法,

package animal

import "testing"

func TestAnimal_Shout(t *testing.T) {
	var animal Animal

	animal = &Cat{}

	animal.shout()

	animal.run()

	animal = &Dog{}

	animal.shout()

	animal.run()
}

输出如下,

=== RUN   TestAnimal_Shout
miaomiao
running with four legs
wangwang
running with four legs
--- PASS: TestAnimal_Shout (0.00s)
PASS

但是由于Go语言并没有抽象类,所以Quadruped是可以被实例化的。但是它并没有shout方法,所以它并不能被当做Animal使用,尴尬。当然我们可以给Quadruped加上shout方法,那么我们如何保证Quadruped类不会被错误的实例化并使用呢?

换句话说,我期望通过对抽象类的非抽象方法的继承来实现代码的复用,通过接口和抽象方法来实现(符合里氏替换原则的)多态,那么如果有一个非抽象的父类出现(其实Java里也很容易出现),很可能会破坏这一规则。

 

其实Go语言是有它自己的编程逻辑的,我这里也只是通过Java的角度来解读Go语言中如何实现初步的面向对象。关于Go中的类型转换和类型断言,留在以后探讨吧。

 

Golang组合(伪继承)的使用案例

通过组合实现好于继承,这里可以用一个案例再次证明一下。假设有一个Java线程类:

class Runner {
    private String name;

    public Runner(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void run(Task task) {
        task.run();
    }

    public void runAll(Task[] tasks) {
        for (Task task : tasks) {
            run(task);
        }
    }
}

RunCounter继承Runner:

class RunCounter extends Runner {
    private int count;

    public RunCounter(String message) {
        super(message);
        this.count = 0;
    }

    @Override
    public void run(Task task) {
        count++;
        super.run(task);
    }

    @Override
    public void runAll(Task[] tasks) {
        count += tasks.length;
        super.runAll(tasks);
    }

    public int getCount() {
        return count;
    }
}

通过如下代码调用:

RunCounter runner = new RunCounter("my runner");

Task[] tasks = {new Task("one"), new Task("two"), new Task("three")};
runner.runAll(tasks);

System.out.printf("%s ran %d tasks\n", runner.getName(), runner.getCount());

运行结果是:

running one
running two
running three
my runner ran 6 tasks

竟然有6个线程任务在跑。而我们是想指定三个啊。这是因为继承导致了弱封装,封装性不强,产生了紧耦合,导致不可思议的Bug:

解决方案是组合Composition:

class RunCounter {
    private Runner runner;
    private int count;

    public RunCounter(String message) {
        this.runner = new Runner(message);
        this.count = 0;
    }

    public void run(Task task) {
        count++;
        runner.run(task);
    }

    public void runAll(Task[] tasks) {
        count += tasks.length;
        runner.runAll(tasks);
    }

    public int getCount() {
        return count;
    }

    public String getName() {
        return runner.getName();
    }
}

虽然解决了问题,但是缺点是需要在RunCounter显式定义Runner的方法:

public String getName() { return runner.getName(); }

导致很多重复,也会引入Bug。

在Go中没有继承,天然是组合,直接实现如下:

package main

import "fmt"

type Task struct {
}

func (t *Task) Run() { fmt.Println("hello world") }

type Runner struct{ name string }

func (r *Runner) Name() string { return r.name }

func (r *Runner) Run(t Task) {
	t.Run()
}

func (r *Runner) RunAll(ts []Task) {
	for _, t := range ts {
		r.Run(t)
	}
}

type RunCounter struct {
	runner Runner
	count  int
}

func NewRunCounter(name string) *RunCounter {
	return &RunCounter{runner: Runner{name}}
}

func (r *RunCounter) Run(t Task) {
	r.count++
	r.runner.Run(t)
}

func (r *RunCounter) RunAll(ts []Task) {
	r.count += len(ts)
	r.runner.RunAll(ts)
}

func (r *RunCounter) Count() int {
	return r.count
}

func (r *RunCounter) Name() string {
	return r.runner.Name()
}

虽然这里也有

func (r *RunCounter) Name() string

这个方法,但是我们可以去除它,首先来看看Go语言的Struct embedding,也就是struct嵌入。被嵌入的类型的方法和字段在嵌入者类型中定义实现。虽然类似继承,但是被嵌入者不知道它被嵌入了。例如一个类型Person:

type Person struct{ Name string }

func (p Person) Introduce() { fmt.Println("Hi, I'm", p.Name) }

我们能定义Employee 嵌入了Person:

type Employee struct {
	Person // Struct embedding
	EmployeeID int
}

这样所有的Person字段方法都适用Employee:

var e Employee
e.Name = "Peter"
e.EmployeeID = 1234

e.Introduce()

现在我们使用struct嵌入来优化前面的RunCounter:

type RunCounter2 struct {
	Runner
	count int
}

func NewRunCounter2(name string) *RunCounter2 {
	return &RunCounter2{Runner{name}, 0}
}

func (r *RunCounter2) Run(t Task) {
	r.count++
	r.Runner.Run(t)
}

func (r *RunCounter2) RunAll(ts []Task) {
	r.count += len(ts)
	r.Runner.RunAll(ts)
}

func (r *RunCounter2) Count() int { return r.count }

嵌入是不是像继承呢?但它不是,而是更好的组合Compistion。

从某种角度上说,struct嵌入类似依赖注入(DI),通过组合+依赖注入替代了以前的继承。

===========END===========

本文转载自:https://my.oschina.net/u/1029640/blog/3046237

下一篇: Go net/rpc包
秋风醉了
粉丝 253
博文 532
码字总数 405755
作品 0
朝阳
程序员
私信 提问
加载中

评论(0)

黑马程序员.bobo.DAY.7

Day-7 1.面向对象(继承-概述) /* 将学生和工人的共性描述提取出来,单独进行描述,只要让学生和公认与当杜描述的这个类有关系,就可以 继承:1,提高了代码的复用性.2,让类与类之间产生了关系,才有...

BobbyLou
2015/05/14
22
0
《详解PHP面向对象》系列技术文章整理收藏

《详解PHP面向对象》系列技术文章整理收藏 1PHP面向对象之旅:类和对象 http://www.lai18.com/content/425094.html 2PHP面向对象之旅:类的属性 http://www.lai18.com/content/425093.html 3...

开元中国2015
2015/06/27
66
0
黑马程序员.bobo.DAY.5

------- android培训、java培训、期待与您交流! ---------- DAY-5 1.面向对象(概述) 1.1理解面向对象 面向对象是相对面向过程而言 面向对象和面向过程是一种思想 面向过程 强调的功能行为 ...

BobbyLou
2015/05/14
24
0
OOA、OOD和OOP的定义及之间的关系

什么是面向对象分析(OOA)? "面向对象分析是一种分析方法,这种方法利用从问题域的词汇表中找到的类和对象来分析需求。" 什么是面向对象设计(OOD)? “面向对象设计是一种设计方法,包括面向对...

雲克
2013/01/22
7.2K
0
黑马程序员.bobo.DAY.6

/*静态:static用法:是一个修饰符,用于修饰成员(成员变量,成员函数),当成员被静态修饰后,就多了一个调用方式,除了可以被对象调用外,可以直接被类名调用,类名.静态成员 static 特点:1,随着类的...

BobbyLou
2015/05/14
9
0

没有更多内容

加载失败,请刷新页面

加载更多

在两个日期之间查找对象MongoDB

我一直在围绕在mongodb中存储推文,每个对象看起来像这样: {"_id" : ObjectId("4c02c58de500fe1be1000005"),"contributors" : null,"text" : "Hello world","user" : { "following......

javail
23分钟前
35
0
《aelf经济和治理白皮书》重磅发布:为DAPP提供治理高效、价值驱动的生态环境

2020年2月17日,aelf正式发布《aelf经济和治理白皮书》,这是aelf继项目白皮书后,在aelf网络经济模型和治理模式方面的权威论述。 《aelf经济和治理白皮书》描述了aelf生态中各个角色及利益的...

AELF开发者社区
34分钟前
44
0
EditText的首字母大写

我正在开发一个小小的个人待办事项列表应用程序,到目前为止,一切都运行良好。 我想知道一个小怪癖。 每当我去添加一个新项目时,我都会看到一个带有EditText视图的Dialog。 当我选择EditT...

技术盛宴
38分钟前
30
0
战疫 | 高德工程师如何在3天上线“医护专车”

新冠状病毒肺炎疫情突袭,无数医护人员放弃与家人团聚,明知凶险,仍然奋战在一线。但因为武汉公交、地铁、网约车停运,医护人员上下班很难。白衣天使疾呼打车难。 (截图摘自《财经国家周刊...

amap_tech
46分钟前
41
0
img在IE中无法按比例显示

在IE浏览器中使用img标签当给img标签设置width:98%时,显示时还是会把img的原始高度显示出来 解决方式给父标签设置width,但width不能使用100%需要指定一个值 <div style="width:900px;"> ...

有理想的鸭子
46分钟前
45
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部