面向对象初探
在软件开发领域,你应该听到过过程式编程、面向对象编程、甚至函数式编程等软件开发方式。而面向对象编程更是在现今大行其道,JAVA就是面向对象语言的代表,在JAVA中一切皆对象,它让编程中的一切元素、甚至设计方式都标准化,这更有利于大型应用的编写。
什么是面向对象编程?
面向对象编程,简称OOP。在OOP的理念下,任何事物无论简单还是复杂都可以用对象表示,每个对象都包含属性和方法,属性表示对象是什么?有什么特征?方法表示对象能做什么?有什么能力?任何应用的构建都转化成对象关系的设计,这演化成一套标准化的面向对象设计模式。
类和对象 要理解OOP,首先要理解类和对象的关系,类是设计层面的概念,而对象则是程序运行时的概念,OOP程序设计基于类的设计,类在程序运行时实例化为对象实现真正的业务逻辑。简而言之,所谓类可以理解成对象的模板,你编写一个类,在运行时需要实例化才能在程序调用栈中传递。
属性和方法 在过程式编程中,我们熟悉变量和函数,使用这些基本元素我们实现业务逻辑。而属性和方法是对象内部的特征,咋一看他们很像,其实本质上是将实现特定功能的函数和变量封装成一个整体,即对象。一个对象包含一系列的属性和方法专注于实现某种特定功能。
接口 接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能),接口是多态实现的基础。
面向对象三大特性
封装
将实现特定功能的属性和方法抽象封装成类,提供public/private/protected访问修饰符,控制外部访问的可见性。
- 对业务相近的变量和函数封装在类/结构体中
- 变量=>类/结构体的属性
- 函数=>类/结构体的方法
继承
对已经实现的类或接口提供重用或扩展的能力,子类可完全继承父类的所有能力。继承的过程,就是从一般到特殊的过程,其过程可以通过继承和组合来实现。
- 继承的类拥有父类/父结构体的全部属性和方法
- 接口继承则是一行代码拥有父类接口的全部抽象方法
- 继承可以节约大量重复代码
- 继承的目的:
- 提高代码的复用度
- 拓展出新的属性和方法
- 改进父类/结构体的方法
- 以覆写父类/结构体的方法类实现
多态
多态是指一个父类/接口可以拥有多种具体的子类实现形态;多态的好处是可以根据业务需要去方便地调度子类们的共性和个性
面向对象编程的优缺点
优点
高效:面向对象设计结构、模块清晰的应用,有利于大型应用的开发,团队成员各自维护局部模块,降低了成员开发的复杂性。 易维护:由于高内聚低耦合,各个模块的维护都是局部的,这非常方便定位问题。 易扩展:继承、封装、多态的特性,以及标准化的设计模式设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
缺点
俗话说,“如果你手上只有锤子,那么你看什么都是钉子”,OOP把一切都当成对象,但现实世界是复杂的,虽然设计模式就是解决应用中抽象的设计问题的。这把编码阶段的复杂性提到设计阶段。
相对过程式编程,面向对象编程的程序结构性能有所下降; 提高系统设计的复杂度;
Go的面向“对象”
了解了面向对象编程的思想,我们再来看Go的面向对象,严格来说,Go并非面向对象编程语言,Go有自己的设计理念,面向对象只是一种软件开发方法,Go有自己的支持方式。
没有类和对象,只有类型和值
传统的JAVA类和对象:
//类定义
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()工厂方法
//类型定义
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继承:
public class Student extend Person{
private String school
//构造方法
public Student(){
super() //直接使用父类的构造方法
}
public void doSomething(){
//block
}
...
}Go的聚合和嵌入:
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}
}自由的结构体属性类型
//接口
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的类型方法在外部任意地方,只要定义的方法接收者为该类型,那定义的方法就是该类型的方法。
//类型定义
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的访问控制基于包,包内的成员变量、常量、类型、函数基于命名首字母的大小写控制。 其结构体类型的属性方法也类似,基于命名首字母的大小写控制。
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接口也是类型定义,其内部声明了其规定的方法签名:
//任何实现了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:
//猴子活着吃香蕉!!!
//香蕉有毒,猴子死了!!!
//猫活着吃鱼!!!
//猫吃了河豚死翘翘!!!接口类型限定赋值
类型值赋值给接口类型
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语言中,实现接口需要类显式实现,例如:
public class Person implements IPerson {
//block
}而Go并不需要显式实现,类型只需实现特定接口的方法签名即可。当然这种极度宽松的实现方式有可能让你定义的类型“不小心”就实现了某些接口能力,一旦你的类型方法和某些接口方法签名一致时就会如此。
//该类型实现了标准包的io.Writer接口
type F struct {}
func (f *F) Write(p []byte) (n int, err error) {
//block
}接口嵌入
和结构体类型类似,接口也可以嵌入其他接口,接口只能嵌入不能聚合!
以下演示接口嵌入的示例:
//生物接口
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:
//老虎活着称大王!!!
//老虎捕猎!!!
//老虎战斗死了!!!
//花儿享受阳光!!!
//花儿茁壮成长!!!
//花儿落下死了!!!类型断言
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
//试着转一下类型,不成功也不会报错
plant, ok := being.(IPlant)
//转一下类型,不成功就会panic
plant := being.(IPlant)
面向接口编程和简单的依赖注入
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层的业务逻辑,即抽象不依赖细节。
电商推荐引擎(面向接口编程demo)

电商推荐引擎(面向接口编程demo):https://github.com/bingdang/go-interface
实现过程
- 对召回、排序、过滤分别定义接口
- 召回接口:
recall/recaller.go - 排序接口:
sort/sorter.go - 过滤接口:
filter/filter.go
- 召回接口:
- 将三个步骤的接口封装到推荐引擎结构体中
rec.go/Recommender 结构体
- 实现纯接口的推荐框架
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结构体
rec.go/main()
~/go/src/go-interface main !1 ?1 ❯ go run ./
2024/06/28 18:03:05 召回hot耗时0ms,召回了5个商品
2024/06/28 18:03:05 召回szie耗时0ms,召回了8个商品
2024/06/28 18:03:05 去重之后一共召回了8个商品
2024/06/28 18:03:05 排序耗时0ms
2024/06/28 18:03:05 过滤规则ratio耗时0ms,过滤掉了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
