在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言如果说最纯粹的面向对象语言,我觉得是Java无疑。而且Java语言的面向对象也是很直观,很容易理解的。class是基础,其他都是要写在class里的。 最近学习了Go语言,有了一些对比和思考。虽然我还没有完全领悟Go语言“Less is more”的编程哲学,思考的方式还是习惯从Java的角度出发,但是我还是深深的喜欢上了这门语言。 这篇文章仅是我学习过程中的一些想法,欢迎留言探讨,批评指正。
Java中的封装Java语言中,封装是自然而来的,也是强制的。你所写的代码,都要属于某个类,某个class文件。类的属性封装了数据,方法则是对这些数据的操作。通过private和public来控制数据的可访问性。 每个类(java文件),自然的就是一个对象的模板。 Go中的封装Go语言并不是完全面向对象的。其实Go语言中并没有类和对象的概念。 首先,Go语言是完全可以写成面向过程风格的。Go语言中有很多的function是不属于任何对象的。(以前我写过一些ABAP语言,ABAP是从面向过程转为支持面向对象的语言,所以也是有类似的function的)。 然后,Go语言中,封装有包范围的封装和结构体范围的封装。 在Java语言中,我们组织程序的方式一般是通过project-package-class。每个class,对应一个文件,文件名和class名相同。其实我觉得这样组织是很清晰也很直观的。 在Go语言中,只有一个package的概念。package就是一个文件夹。在这个文件夹下的所有文件,都是属于这个package的。这些文件可以任意起名字,只要在文件头加上package名字 package handler
那么这个文件就是属于这个package的。在package内部所有的变量是互相可见的,是不可以重复的。 你可以这样理解:文件夹(package)就是你封装的一个单元(比如你想封装一个Handler处理一些问题)。里边其实只有一个文件,但是为了管理方便,你把它拆成了好几个文件(FileHandler、ImageHandler、HTTPHandler、CommonUtils),但其实这些文件写成一个和写成几个,他们之间的变量都是互相可见的。 如果变量是大写字母开头命名,那么对包外可见。如果是小写则包外不可见。 其实一开始我是很不习惯这种封装方式的,因为写Java的时候是难以想象一个文件里的变量在另一个文件里也可见的。 Go中的另外一种封装,就是结构体struct。没错,类似C语言中的struct,我们把一些变量用一个struct封装在一起。 type Dog struct { Name string Age int64 Sex int }
我们还可以给struct添加方法,做法就是把一个function指定给某个struct。 func (dog *Dog) bark() { fmt.Println("wangwang") }
这时候看起来是不是很有面向对象的感觉了?起码我们有对象(struct)和方法(绑定到struct的function)了,是不是?具体的Go语法不在这里过多探讨。 继承封装只是基础,为继承和多态提供可能。继承和多态才是面向对象最有意思也最有用的地方。 Java中的继承Java语言中,继承通过extends关键字实现。有非常清晰的父类和子类的概念以及继承关系。Java不支持多继承。 Go中的继承Go语言中其实并没有继承。看到这里你可能会说:什么鬼?面向对象语言里没有继承?好吧其实一开始我也是懵逼的。但是Go中确实只是提供了一种伪继承,通过embedding实现的“伪”继承。 type father struct { Name string Age int } type son struct { father hobby string } type son2 struct { someFather father hobby string }
如上代码所示,在son中声明一个匿名的father类型结构体,那么son伪继承了father,而son2则仅仅是把father作为一个属性使用。 son中可以直接使用father中的Name、Age等属性,不需要写成son.father.Name,直接写成son.Name即可。如果father有方法,也遵循同理。 但为什么说是伪继承呢? 在Java的继承原则上,子类继承了父类,不光是子类可以复用父类的代码,而且子类是可以当做父类来使用的。参见面向对象六大原则之一的里氏替换原则。即在需要用到父类的地方,我用了一个子类,应该是可以正常工作的。 然而Go中的这种embedding,son和father完全是两个类型,如果在需要用father的地方直接放上一个son,编译是不通过的。 关于Go语言中的这种伪继承,我还踩过一个深坑,分享在这里。 看起来Go语言中的继承是不是更像一种提供了语法糖的has-a的关系,并不是is-a的关系。说到这里,可能有的人会说Go语言这是搞什么,没有继承还怎么愉快的玩耍。又有的人可能觉得:没错,就是要干掉继承,组合优于继承。 其实关于继承或是组合的问题,我查了很多说法,目前我个人认同如下观点: 继承VS组合
那么什么时候用继承,什么时候用组合呢?
多态我认为多态是面向对象编程中最重要的部分。 By the way,方法重载也是多态的一种。但是Go语言中是不支持方法重载的。 两种语言都支持方法重写(Go中的伪继承,son如果重写了father中的方法,默认是会使用son的方法的)。 不过要注意的是,在Java中重写父类的非抽象方法,已经违背了里氏替换原则。而Go语言中是没有抽象方法一说的。
Go中的多态采用和JavaScript一样的鸭式辩型:如果一只鸟走路像鸭子,叫起来像鸭子,那么它就是一只鸭子。 在Java中,我们要显式的使用implements关键字,声明一个类实现了某个接口,才能将这个类当做这个接口的一个实现来使用。在Go中,没有implements关键字。只要一个struct实现了某个接口规定的所有方法,就认为它实现了这个接口。 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代码: import java.io.*; class test { public static void main (String[] args) throws java.lang.Exception { 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代码: package main import ( "fmt" ) func main() { var animal Animal animal = &Cat{} animal.shout() animal = &Dog{} animal.shout() } type Animal struct { } type Cat struct { //伪继承 Animal } type Dog struct { //伪继承 Animal } func (a *Animal) shout() { //Go has no abstract method } func (c *Cat) shout() { fmt.Println("miaomiao") } func (d *Dog) shout() { fmt.Println("wangwang") }
上边的代码是编译报错的。输出如下: # command-line-arguments dome/demo.Go:9:9: cannot use Cat literal (type *Cat) as type Animal in assignment dome/demo.Go:11:9: cannot use Dog literal (type *Dog) as type Animal in assignment
其实就算是在Java里,如果不考虑代码复用,我们也是首先推荐接口而不是抽象类的。那么我们把上边的实现改进一下。 Java代码: import java.io.*; class test { public static void main (String[] args) throws java.lang.Exception { Animal animal; animal= new Cat(); animal.shout(); animal = new Dog(); animal.shout(); } } interface Animal{ void shout(); } class Cat implements Animal{ public void shout(){ System.out.println("miaomiao"); } } class Dog implements Animal{ public void shout(){ System.out.println("wangwang"); } }
输出如下: miaomiao wangwang
Go里边的接口是鸭式辩型,代码如下: package main import ( "fmt" ) func main() { var animal Animal animal = &Cat{} animal.shout() animal = &Dog{} animal.shout() } type Animal interface { shout() } type Cat struct { } type Dog struct { } func (c *Cat) shout() { fmt.Println("miaomiao") } func (d *Dog) shout() { fmt.Println("wangwang") }
输出如下: miaomiao wangwang
看起来很棒对不对。那我们为什么不直接都用接口呢?还要继承和抽象类干什么?这里我们来捋一捋一个老生常谈的问题:接口和抽象类的区别。 这里引用了知乎用户chao wang的观点。感兴趣的请前往他的回答。
考虑这样一个需求:猫和狗都会跑,并且它们跑起来没什么区别。我们并不想在Cat类和Dog类里边都实现一遍同样的run方法。所以我们引入一个父类:四足动物Quadruped Java代码: import java.io.*; class test { public static void main (String[] args) throws java.lang.Exception { 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 main import ( "fmt" ) func main() { var animal Animal animal = &Cat{} animal.shout() animal.run() animal = &Dog{} animal.shout() animal.run() } 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") }
输出如下: miaomiao running with four legs wangwang running with four legs
但是由于Go语言并没有抽象类,所以Quadruped是可以被实例化的。但是它并没有shout方法,所以它并不能被当做Animal使用,尴尬。当然我们可以给Quadruped加上shout方法,那么我们如何保证Quadruped类不会被错误的实例化并使用呢? 换句话说,我期望通过对抽象类的非抽象方法的继承来实现代码的复用,通过接口和抽象方法来实现(符合里氏替换原则的)多态,那么如果有一个非抽象的父类出现(其实Java里也很容易出现),很可能会破坏这一规则。
其实Go语言是有它自己的编程逻辑的,我这里也只是通过Java的角度来解读Go语言中如何实现初步的面向对象。关于Go中的类型转换和类型断言,留在以后探讨吧。 如果本文对你有帮助,请点赞鼓励一下吧^_^
|
请发表评论