cover

面向对象初探

在软件开发领域,你应该听到过过程式编程、面向对象编程、甚至函数式编程等软件开发方式。而面向对象编程更是在现今大行其道,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)

OOP

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

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)

rec

电商推荐引擎(面向接口编程demo):https://github.com/bingdang/go-interface

实现过程

  1. 对召回、排序、过滤分别定义接口
    • 召回接口:recall/recaller.go
    • 排序接口:sort/sorter.go
    • 过滤接口:filter/filter.go
  2. 将三个步骤的接口封装到推荐引擎结构体中
    • rec.go/Recommender 结构体
  3. 实现纯接口的推荐框架
    • rec.go/Rec() Recommender的方法
  4. 对召回、排序、过滤进行具体的实现
    • 召回实现:
      • 按照热度召回:recall/hot_recall.go
      • 按照size召回:recall/size_recall.go
    • 排序实现:
      • 按照好评率排序:sort/ratio_sort.go
      • 按照size排序:sort/szie_sort.go
    • 过滤的具体实现:
      • 按照评价进行过滤:filter/ratio_filter.go
  5. 使用具体的实现,将具体的实现赋值给Recommender结构体
    • rec.go/main()
~/go/src/go-interface main !1 ?1go 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
Golang-面向“对象”:面向接口
转载前请阅读本站 版权协议,文章著作权归 饼铛 所有,转载请注明出处。
评论

目录