R6类的多重继承

12

实际问题

R6不支持多重继承的情况下,我有哪些解决方案?

免责声明

我知道R主要是一种函数式语言。但是,它也具有非常强大的面向对象编程能力。此外:当你

  1. 知道你正在为C#,Java等面向对象语言进行原型设计时

  2. 你的应用程序原型需要是自给自足的(包括DB后端、业务逻辑和前端/UI)

  3. 你可以使用诸如R6和shiny之类的出色的“原型技术”

背景

我的面向Web应用的R原型需要同时具备“全栈”/自给自足的能力和尽可能接近设计模式/原则以及我们生产语言(C#/.NET)中使用的依赖注入容器简单DI在R中的概念验证)。

在这方面,我非常喜欢使用接口(或抽象类),以便解耦代码模块并符合依赖反转原则OOD SOLID原则(“叔叔鲍勃”的详细解释)。

尽管 R6 并不明确支持接口,但我仍然可以使用定义了“抽象方法”的 R6 类来完美地模仿它们(请参见下面的示例)。这有助于我向那些不太熟悉 R 的面向对象程序员传达我的软件设计。我努力减少他们的“概念转换工作”。
然而,为了实际从其他具体类(而非“抽象”-类似的模拟接口类)继承,我需要放弃在 R6Class 中对 inherit 的价值,这在实际上意味着需要定义两个类而不是一个。
示例
在依赖反转之前:
Foo 依赖于具体类 Bar。从面向对象设计原则的角度来看,这很糟糕,因为它会导致代码紧密耦合。
Bar <- R6Class("Bar",
  public = list(doSomething = function(n) private$x[1:n]),
  private = list(x = letters)
)
Foo <- R6Class("Foo",
  public = list(bar = Bar$new())
)
inst <- Foo$new()
> class(inst)
> class(inst$bar)
[1] "Bar" "R6" 

依赖倒置后:

FooBar 现在解耦了。两者都依赖于一个由类IBar模拟的接口。我可以在运行时决定将该接口的哪个实现插入到Foo实例中(通过属性注入实现:即Foo的字段bar)。

IBar <- R6Class("IBar",
  public = list(doSomething = function(n = 1) stop("I'm the inferace method"))
)
Bar <- R6Class("Bar", inherit = IBar,
  public = list(doSomething = function(n = 1) private$x[1:n]),
  private = list(x = letters)
)
Baz <- R6Class("Baz", inherit = IBar,
  public = list(doSomething = function(n = 1) private$x[1:n]),
  private = list(x = 1:24)
)
Foo <- R6Class("Foo",
  public = list(bar = IBar$new())
)

inst <- Foo$new()
inst$bar <- Bar$new()
> class(inst$bar)
[1] "Bar"  "IBar" "R6"  
> inst$bar$doSomething(5)
[1] "a" "b" "c" "d" "e"

inst$bar <- Baz$new()
[1] "Baz"  "IBar" "R6"  
> inst$bar$doSomething(5)
[1] 1 2 3 4 5

关于面向对象设计(OOD)的原因,这是为什么有意义的一点:Foo 应该完全不知道 field bar 中存储的对象是如何实现的。它所需要知道的只是可以在该对象上调用哪些方法。为了知道这一点,只需要知道 field bar 中的对象实现了哪个接口(在我们的情况下是具有 doSomething() 方法的 IBar 接口)。
使用从基类继承来简化设计:
到目前为止,一切都很好。但是,我还想通过定义某些具体基类来简化我的设计,让我的其他一些具体类可以从中继承。
BaseClass <- R6Class("BaseClass",
  public = list(doSomething = function(n = 1) private$x[1:n])
)
Bar <- R6Class("Bar", inherit = BaseClass,
  private = list(x = letters)
)
Baz <- R6Class("Bar", inherit = BaseClass,
  private = list(x = 1:24)
)

inst <- Foo$new()
inst$bar <- Bar$new()
> class(inst$bar)
[1] "Bar"       "BaseClass" "R6"   
> inst$bar$doSomething(5)
[1] "a" "b" "c" "d" "e"

inst$bar <- Baz$new()
> class(inst$bar)
[1] "Baz"       "BaseClass" "R6"       
> inst$bar$doSomething(5)
[1] 1 2 3 4 5

结合“接口实现”和基类继承:

这就是我需要多重继承的地方,因此像这样的代码会起作用(伪代码):

IBar <- R6Class("IBar",
  public = list(doSomething = function() stop("I'm the inferace method"))
)
BaseClass <- R6Class("BaseClass",
  public = list(doSomething = function(n = 1) private$x[1:n])
)
Bar <- R6Class("Bar", inherit = c(IBar, BaseClass),
  private = list(x = letters)
)
inst <- Foo$new()
inst$bar <- Bar$new()
class(inst$bar)
[1] "Bar"       "BaseClass" "IBar" "R6"

目前,我对inherit的价值已经被用于“仅仅”模拟接口实现,因此我失去了继承的“实际”好处,而这些好处本应用于我的具体类。

替代想法:

或者,明确支持接口和具体类之间的区别将是很好的。例如,可以采用以下方式:

Bar <- R6Class("Bar", implement = IBar, inherit = BaseClass,
  private = list(x = letters)
)

我认为它存在许多问题,特别是在原型设计方面:不要使用不适当的工具来原型设计一个严重依赖继承和接口的OOD系统。 - Konrad Rudolph
@KonradRudolph:你能详细说明一下原因吗? - Rappster
我不确定该详细说明什么。那么,让我问一个问题:为什么你要使用明显不足以进行原型设计的工具? - Konrad Rudolph
在我看来,R真正擅长的领域有两个:数据分析和原型设计。那么让我反问一下:如果您需要为与数据分析相关的功能创建原型,即使它是一种更依赖OOD而不是R的语言,为什么不使用R呢?我不明白为什么总是对于R要么是函数式的,要么是面向对象的这样非黑即白。也许我只是没有理解,但是R确实具有强大的面向对象特性(S3、S4、Ref Classes、R6)-那么充分利用它们有什么问题呢? - Rappster
1
@NickUlle 是的,确实有几个问题:1)相比于R6,我发现引用类速度非常慢。2)它们有更复杂的架构。3)它们只支持"S4",而使用R6时,您可以在S3和S4之间灵活切换(通过将R6类转换为正式的S4类来解决需要时的问题)。 - Rappster
显示剩余2条评论
2个回答

8

对于那些感兴趣的人:

我经过深思熟虑后意识到,我真正想要/需要的不是 纯粹的多重继承,而是更好地模拟接口/抽象类的使用,同时不放弃 inherit

所以我尝试着R6 进行了一些调整,这样我就可以在调用 R6Class 时区分 inheritimplement

可能有很多理由认为这是一个坏主意,但目前它可以完成工作;-)

您可以从我的分支中获取已调整版本

示例

devtools::install_github("rappster/R6", ref = "feat_interface")
library(R6)

正确实现接口和“标准继承”:

IFoo <- R6Class("IFoo",
  public = list(foo = function() stop("I'm the inferace method"))
)
BaseClass <- R6Class("BaseClass",
  public = list(foo = function(n = 1) private$x[1:n])
)
Foo <- R6Class("Foo", implement = IFoo, inherit = BaseClass,
  private = list(x = letters)
)

> Foo$new()
<Foo>
  Implements interface: <IFoo>
  Inherits from: <BaseClass>
  Public:
    clone: function (deep = FALSE) 
    foo: function (n = 1) 
  Private:
    x: a b c d e f g h i j k l m n o p q r s t u v w x y z

当接口未正确实现(即方法未实现)时:
 Bar <- R6Class("Bar", implement = IFoo,
    private = list(x = letters)
  )
> Bar$new()
Error in Bar$new() : 

Non-implemented interface method: foo

依赖注入的概念验证

这是一个简单的草案,详细阐述了在R6中实现接口和依赖反转的动机和可能的实现方法。


我发现了这个,我真的很喜欢你对它所做的事情。然而:引入一个新的东西会更好吗:R6Interface,与指定公共、私有、活动成员列表的R6类不同,R6Interface将接受一个列表的列表,指示每个字段所需的公共函数/字段和签名。然后,“接口实现”检查可以确保所有方法都使用正确的签名实现。 - Chris Knoll

6
加强版:我认为在原型设计面向对象语言(如C#、Java等)时,模仿面向对象设计原则和行为没有什么不好的地方。
问题在于你需要问这个问题,因为R只是一个无法满足你需要的原型设计面向对象系统的不充分工具。
或者只是原型设计与数据分析有关的解决方案,而不是那些不符合范例的API方面。
话虽如此,R的优势在于你可以编写自己的对象系统;毕竟,这就是R6的用途。R6恰恰不适合你的目的,但没有什么能阻止你实现自己的系统。特别是,S3已经允许多重继承,只是它不支持编码接口(而是以临时方式发生)。
但是,没有什么能阻止你提供一个包装函数来执行这种编码。例如,你可以实现一组函数interfaceclass(注意名称冲突),可以按如下方式使用:
interface(Printable,
    print = prototype(x, ...))

interface(Comparable,
    compare_to = prototype(x, y))

class(Foo,
    implements = c(Printable, Comparable),
    private = list(x = 1),
    print = function (x, ...) base::print(x$x, ...),
    compare_to = function (x, y) sign(x$x - y$x))

这将生成(例如):
print.Foo = function (x, ...) base::print(x$x, ...)

compare_to = function (x, y) UseMethod('compare_to')

compare_to.foo = function (x, y) sign(x$x - y$x)

Foo = function ()
    structure(list(x = 1), class = c('Foo', 'Printable', 'Comparable'))

...等等。事实上,S4也做了类似的事情(但是在我看来做得不好)。


1
感谢您花时间表达您的意见并勾勒出您的方法,我很感激。但是,我没有看到任何与我关于R6的实际问题相关的内容。对于所有面向对象原型相关的事情,R6到目前为止都为我提供了很好的帮助。多重继承是我真正缺少的唯一功能。 - Rappster
@Rappster 我同意R6是一个非常出色的面向对象系统。但正如你所观察到的,它并不支持多重继承,我认为没有办法在不改变R6内部结构的情况下添加它。虽然这本身并不是一个坏选择,但我对其内部结构了解不够,无法发表评论。 - Konrad Rudolph
好的,无论如何还是谢谢!有人黑进了组成R6类的环境层,并以某种方式实现了多重继承,但这似乎需要大量的代码来进行逆向工程/理解,而且确实是一种非常肮脏的黑客行为:https://github.com/wch/R6/issues/9 - Rappster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接