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

如何在 Go 中实现一个抽象类?由于 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)
}

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

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

【问题讨论】:

    标签: oop interface go


    【解决方案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:=&ConcreteDaemonA{a, 0}
      a.Daemon = r
      return r
    }
    
    
    type ConcreteDaemonB struct { 
    *AbstractDaemon
    bar int
    }
    
    func newConcreteDaemonB() *ConcreteDaemonB {
      a:=&AbstractDaemon{}
      r:=&ConcreteDaemonB{a, 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 := Daemon{task: 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 := Daemon{task: 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 := &concreteObject{object: 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 = &factory{ctors: 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 := alpha{name: "test"}
          b := &beta{alpha: a}
          b.common(b)
      }
      
      
        Output:
          common called
          work called
          Name is test
      

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

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

      【讨论】:

        猜你喜欢
        • 2010-10-05
        • 2013-11-12
        • 2020-03-22
        • 1970-01-01
        • 1970-01-01
        • 2012-12-15
        • 2019-08-26
        • 1970-01-01
        相关资源
        最近更新 更多