面向对象初探
在软件开发领域,你应该听到过过程式编程、面向对象编程、甚至函数式编程等软件开发方式。而面向对象编程更是在现今大行其道,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 } 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 () { } ... }
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 ) s2 := new (Student2) s2.Ps.Name = "fun2" s2.School = "Social University2" s1.Ps.SetAge(18 ) }
自由的结构体属性类型 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) p.SetName("fun" ) *p.SetName("func" ) 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 type IAnimal interface { Live() Dead() } type Monkey struct {}func (m *Monkey) Live() { fmt.Println("猴子活着吃香蕉!!!" ) } func (m *Monkey) Dead() { fmt.Println("香蕉有毒,猴子死了!!!" ) } type Cat struct {}func (c *Cat) Live() { fmt.Println("猫活着吃鱼!!!" ) } func (c *Cat) Dead() { fmt.Println("猫吃了河豚死翘翘!!!" ) } 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() } }
接口类型限定赋值 类型值赋值给接口类型
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() } 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 { }
而Go并不需要显式实现,类型只需实现特定接口的方法签名即可。当然这种极度宽松的实现方式有可能让你定义的类型“不小心”就实现了某些接口能力,一旦你的类型方法和某些接口方法签名一致时就会如此。
1 2 3 4 5 6 type F struct {}func (f *F) Write(p []byte ) (n int , err error ) { }
接口嵌入 和结构体类型类似,接口也可以嵌入其他接口,接口只能嵌入不能聚合!
以下演示接口嵌入的示例:
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("花儿茁壮成长!!!" ) } 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() } } }
类型断言 类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
1 2 3 4 5 plant, ok := being.(IPlant) plant := being.(IPlant)
面向接口编程和简单的依赖注入 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 package dataimport "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 } package appimport ( "faceinter/data" "fmt" ) type App interface { Sum(int , int ) int } type app struct { mydesql data.Sql } func NewAppIns (mydesql data.Sql) App { 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 } package mainimport ( "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) callAppFunc(appins) } OUTPUT: 打开了 127.0 .0 .1 :27017 1 + 2 = 3
依赖倒置原则 app中使用了接口data.Sql来定义数据库的连接,连接数据库依赖Sql的接口,而不依赖具体哪一种数据库的实现。 这样可以自由选择数据库,而不用动app层代码。只需要相关数据库实现了该接口,并通过db的私有构造函数传递给app的构造函数中的dbinterface。 好处:可扩展性好,以后添加其他数据库不影响app层的业务逻辑,即抽象不依赖细节。
电商推荐引擎(面向接口编程demo)
电商推荐引擎(面向接口编程demo):https://github.com/bingdang/go-interface
实现过程
对召回、排序、过滤分别定义接口
召回接口:recall/recaller.go
排序接口:sort/sorter.go
过滤接口:filter/filter.go
将三个步骤的接口封装到推荐引擎结构体中
实现纯接口的推荐框架
rec.go/Rec() Recommender的方法
对召回、排序、过滤进行具体的实现
召回实现:
按照热度召回:recall/hot_recall.go
按照size召回:recall/size_recall.go
排序实现:
按照好评率排序:sort/ratio_sort.go
按照size排序:sort/szie_sort.go
过滤的具体实现:
按照评价进行过滤:filter/ratio_filter.go
使用具体的实现,将具体的实现赋值给Recommender结构体
1 2 3 4 5 6 7 8 9 10 ~/go /src/go -interface main !1 ?1 ❯ go run ./ 2024 /06 /28 18 :03 :05 召回hot耗时0 ms,召回了5 个商品2024 /06 /28 18 :03 :05 召回szie耗时0 ms,召回了8 个商品2024 /06 /28 18 :03 :05 去重之后一共召回了8 个商品2024 /06 /28 18 :03 :05 排序耗时0 ms2024 /06 /28 18 :03 :05 过滤规则ratio耗时0 ms,过滤掉了4 个商品No.0 ,Id:7 ,Name:p7 No.1 ,Id:1 ,Name:p1 No.2 ,Id:2 ,Name:p2 No.3 ,Id:3 ,Name:p3