如何在 Go 中实现一个抽象类?

Posted

技术标签:

【中文标题】如何在 Go 中实现一个抽象类?【英文标题】:How to implement an abstract class in Go? 【发布时间】:2015-07-27 11:14:31 【问题描述】:

由于 Go 不允许我们在接口中有字段,这将是一个无状态对象。那么,换句话说,Go 中的方法是否可以有某种默认实现?

考虑一个例子:

type Daemon interface 
    start(time.Duration)
    doWork()


func (daemon *Daemon) start(duration time.Duration) 
    ticker := time.NewTicker(duration)

    // this will call daemon.doWork() periodically  
    go func() 
        for 
            <- ticker.C
            daemon.doWork()
        
    ()


type ConcreteDaemonA struct  foo int 
type ConcreteDaemonB struct  bar int 

func (daemon *ConcreteDaemonA) doWork() 
    daemon.foo++
    fmt.Println("A: ", daemon.foo)


func (daemon *ConcreteDaemonB) doWork() 
    daemon.bar--
    fmt.Println("B: ", daemon.bar)


func main() 
    dA := new(ConcreteDaemonA)
    dB := new(ConcreteDaemonB)

    start(dA, 1 * time.Second)
    start(dB, 5 * time.Second)

    time.Sleep(100 * time.Second)

这将无法编译,因为无法将接口用作接收器。

事实上,我已经回答了我的问题(请参阅下面的答案)。但是,这是实现这种逻辑的惯用方式吗?除了语言的简单性之外,还有什么理由不使用默认实现?

【问题讨论】:

【参考方案1】:

其他答案为您的问题提供了替代方案,但是他们提出了不使用抽象类/结构的解决方案,我想如果您有兴趣使用类似抽象类的解决方案,这里是您问题的非常精确的解决方案:

Go plaground

package main

import (
    "fmt"
    "time"
)

type Daemon interface 
    start(time.Duration)
    doWork()


type AbstractDaemon struct 
    Daemon


func (a *AbstractDaemon) start(duration time.Duration) 
    ticker := time.NewTicker(duration)

    // this will call daemon.doWork() periodically  
    go func() 
        for 
            <- ticker.C
            a.doWork()
        
    ()




type ConcreteDaemonA struct  
*AbstractDaemon
foo int


func newConcreteDaemonA() *ConcreteDaemonA 
  a:=&AbstractDaemon
  r:=&ConcreteDaemonAa, 0
  a.Daemon = r
  return r



type ConcreteDaemonB struct  
*AbstractDaemon
bar int


func newConcreteDaemonB() *ConcreteDaemonB 
  a:=&AbstractDaemon
  r:=&ConcreteDaemonBa, 0
  a.Daemon = r
  return r




func (a *ConcreteDaemonA) doWork() 
    a.foo++
    fmt.Println("A: ", a.foo)


func (b *ConcreteDaemonB) doWork() 
    b.bar--
    fmt.Println("B: ", b.bar)



func main() 
    var dA  Daemon = newConcreteDaemonA()
    var dB  Daemon = newConcreteDaemonB()

    dA.start(1 * time.Second)
    dB.start(5 * time.Second)

    time.Sleep(100 * time.Second)

如果这仍然不明显,如何在 go-lang 中使用抽象类/多继承,这里有详细的帖子。 Abstract Classes In Go

【讨论】:

你能修复代码块中的缩进吗?【参考方案2】:

如果你想提供一个“默认”实现(对于Daemon.start()),那不是接口的特性(至少在Go中不是)。这是具体(非接口)类型的特征。

所以 Daemon 在你的情况下应该是一个具体的类型,方便的是 struct 因为你希望它有字段。并且要完成的任务可以是接口类型的值,或者在简单的情况下只是一个函数值(简单的情况意味着它只有一个方法)。

带接口类型

在Go Playground 上试用完整的应用程序。

type Task interface 
    doWork()


type Daemon struct 
    task Task


func (d *Daemon) start(t time.Duration) 
    ticker := time.NewTicker(t)
    // this will call task.doWork() periodically
    go func() 
        for 
            <-ticker.C
            d.task.doWork()
        
    ()


type MyTask struct

func (m MyTask) doWork() 
    fmt.Println("Doing my work")


func main() 
    d := Daemontask: MyTask
    d.start(time.Millisecond*300)

    time.Sleep(time.Second * 2)

带函数值

在这个简单的例子中,这个更短。在Go Playground 上试试吧。

type Daemon struct 
    task func()


func (d *Daemon) start(t time.Duration) 
    ticker := time.NewTicker(t)
    // this will call task() periodically
    go func() 
        for 
            <-ticker.C
            d.task()
        
    ()


func main() 
    d := Daemontask: func() 
        fmt.Println("Doing my work")
    
    d.start(time.Millisecond * 300)

    time.Sleep(time.Second * 2)

【讨论】:

函数值解决方案在语义上最接近抽象类的 Java 或 C# 概念。这应该是公认的答案。【参考方案3】:

一个简单的解决方案是将daemon *Daemon 移动到参数列表(从而从界面中删除start(...)):

type Daemon interface 
    // start(time.Duration)
    doWork()


func start(daemon Daemon, duration time.Duration)  ... 

func main() 
    ...
    start(dA, 1 * time.Second)
    start(dB, 5 * time.Second)
    ...

【讨论】:

这不仅仅是简单,而是接口的想法和目的。我建议让您的答案更加有力。 这与其他语言的抽象类概念不太相似,但它可能是最惯用的 Go 解决方案(例如,它是许多核心 IO API 的工作方式)。 【参考方案4】:

可以在 go 中实现抽象类。

定义:

type abstractObject interface
    print()


type object struct
    a int
    abstractObject

现在object 是一个抽象类,就像java 的。

你可以继承它并使用它的成员:

type concreteObject struct
    *object


(o *concreteObject) print() 
    fmt.Println(o.a)


func newConcreteObject(o *object) 
    obj := &concreteObjectobject: o
    o.abstractObject = obj // all magics are in this statement.

并使用objectconcreteObject 的方法:

o := &object
newConcereteObject(o)
o.print()

并将抽象对象转换为具体对象:

concObj := o.abstractObject.(*concreteObject)

就像其他 OOP 语言一样。

【讨论】:

【参考方案5】:

如果您不需要工厂,Max Malysh 的解决方案在某些情况下会起作用。然而,Adrian Witas 给出的解决方案可能会导致循环依赖问题。

这是我实现抽象类的简单方法,尊重循环依赖和良好的工厂模式。

假设我们的组件具有以下包结构

component
  base
    types.go
    abstract.go
  impl1
    impl.go
  impl2
    impl.go
  types.go
  factory.go

定义组件的定义,本例将在此处定义:

component/types.go

package component

type IComponent interface
    B() int
    A() int
    Sum() int
    Average() int

现在假设我们要创建一个仅实现 SumAverage 的抽象类,但在这个抽象实现中,我们希望能够使用返回的值由实现的 AB

为此,我们应该为抽象实现的抽象成员定义另一个接口

component/base/types.go

package base

type IAbstractComponentMembers 
    A() int
    B() int

然后我们可以继续实现抽象“类”

component/base/abstract.go

package base

type AbstractComponent struct 
    IAbstractComponentsMember


func (a *AbstractComponent) Sum() int 
    return a.A() + a.B()


func (a *AbstractComponent) Average() int 
    return a.Sum() / 2

现在我们继续实现

component/impl1/impl.go // 假设 impl2

类似
package impl1

type ComponentImpl1 struct 
    base.AbstractComponent


func (c *ComponentImpl1) A() int 
    return 2


func (c *ComponentImpl1) A() int 
    return 4


// Here is how we would build this component
func New() *ComponentImpl1 
    impl1 := &ComponentImpl1
    abs:=&base.AbstractComponent
        IAbstractComponentsMember: impl1,
    
    impl1.AbstractComponent = abs
    return impl1

我们为此使用单独的接口而不是使用 Adrian Witas 示例的原因是,如果我们在这种情况下使用相同的接口,如果我们导入 base impl* 中的 package 来使用抽象“类”,并且我们在 components 包中导入 impl* 包,以便工厂可以注册他们,我们会找到一个循环引用。

所以我们可以有这样的工厂实现

component/factory.go

package component

// Default component implementation to use
const defaultName = "impl1"
var instance *Factory

type Factory struct 
    // Map of constructors for the components
    ctors map[string]func() IComponent


func (f *factory) New() IComponent 
    ret, _ := f.Create(defaultName)
    return ret


func (f *factory) Create(name string) (IComponent, error) 
    ctor, ok := f.ctors[name]
    if !ok 
        return nil, errors.New("component not found")
    
    return ctor(), nil


func (f *factory) Register(name string, constructor func() IComponent) 
    f.ctors[name] = constructor


func Factory() *Factory 
    if instance == nil 
        instance = &factoryctors: map[string]func() IComponent
    
    return instance


// Here we register the implementations in the factory
func init() 
    Factory().Register("impl1", func() IComponent  return impl1.New() )
    Factory().Register("impl2", func() IComponent  return impl2.New() )

【讨论】:

@"Juan Carlos Diaz" 请详细说明递归,你的意思是包循环引用(它们是不同的东西),我假设包组织超出了这个问题的范围,呈现的抽象类使用接口可以解决问题,所以我认为您提出的解决方案与我提出的解决方案没有太大不同,除了您已将其分解为不同的包,并添加了工厂结构 @AdrianWitas 你说得对,我的意思是循环引用,已编辑。但是,如果您使用工厂模式,您的解决方案实际上会遇到循环问题,请尝试 @"Juan Carlos Diaz",我没有看到任何包循环引用问题:请参阅一个包中的工厂扩展 play.golang.org/p/Tu0RNmRWHsa ,因此如前所述,所提供的解决方案之间的唯一区别是委托给包(它有自己的价值)和加工厂。感谢纠正。 @AdrianWitas 当然,您不会看到该游乐场链接有任何循环问题,您在单个文件、单个包中执行所有操作,这根本不是真正的工厂场景。 @AdrianWitas。我想回复解释,但 cmets 的字符数有限,所以我在这里添加了我的 cmets play.golang.org/p/Qi6sKbOF7Ob 如果您有任何建议,请告诉我。我这样做的方式是根据您的建议,但将抽象成员包装在自己的界面中进行了微小的更改【参考方案6】:

抽象类的功能有以下要求 1. 不能直接创建抽象类的实例 2. 它应该提供默认的字段和方法。

接口和结构的组合可以用来满足以上两个要求。例如我们可以在下面看到

package main

import "fmt"

//Abstract Interface
type iAlpha interface 
    work()
    common(iAlpha)


//Abstract Concrete Type
type alpha struct 
    name string


func (a *alpha) common(i iAlpha) 
    fmt.Println("common called")
    i.work()


//Implementing Type
type beta struct 
    alpha


func (b *beta) work() 
    fmt.Println("work called")
    fmt.Printf("Name is %s\n", b.name)


func main() 
    a := alphaname: "test"
    b := &betaalpha: a
    b.common(b)



  Output:
    common called
    work called
    Name is test

这里要提到的一点是,所有默认方法都应该以 iAlpha 作为第一个参数,如果默认方法需要调用任何未实现的方法,它们将在此接口上调用。这与我们在上面的常用方法中所做的相同 - i.work()。

来源:https://golangbyexample.com/go-abstract-class/

【讨论】:

以上是关于如何在 Go 中实现一个抽象类?的主要内容,如果未能解决你的问题,请参考以下文章

使用奇怪的重复模板模式 (CRTP) 在抽象基类中实现赋值运算符

如何强制继承类在 C# 中实现静态方法?

如何声明模型实现的抽象类和方法? [复制]

如何从 C++ 基类继承抽象行为

在某些派生类中实现虚函数的正确方法是啥?

具有抽象继承的后缀运算符++