[Kotlin翻译]在Kotlin中模拟mixins

1,024 阅读9分钟

本文由 简悦SimpRead 转码,原文地址 www.linkedin.com

我写这篇文章的主要灵感是一种对行动的共同部分进行建模的方法 ......

我写这篇文章的主要灵感来自于在Android应用程序中对动作的共同部分进行建模的方法--我最近经常看到--所有的共同逻辑(有时还有状态)都被移到一个叫做BaseAction的抽象类中,然后应用程序中的其他动作都会扩展它。

为什么这种设计可能是坏的?首先,它很有可能成为一个 "通用包" ,用于实现系统中有限部分所需的任何功能。所以你可能有活动1活动2,比方说,它们有35%的相似性。所以这35%的行为归属于BaseActivity,它成为上述两个Activity的父类。但是将来你会添加Activity3,Activity4,Activity5,它们将是不同流程的一部分,不会有这35%的共同逻辑--每个活动仍然会继承你系统中某些有限部分的共同点。其结果是,例如活动3将继承方法、函数和字段,而这对**活动1来说只有意义。

这种方法的另一个问题是,我们可以称之为 "泄露的模板方法" ,它发生在你有一些抽象的过程,但并不适用于所有的活动,比如这个。

abstract class BaseActivity : Activity() {


   // we expecting that it is common procedure to 
   // set display some text content
   // and we are forcing this behavior
   protected abstract fun provideHelpFilePath(): String

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setSomeTextSomewhere(provideHelpFilePath())
   }


}

// works here
class BusinessActivity : BaseActivity() {
    override fun provideHelpFilePath(): String = "/pathToBusinesssHelp"
    
}

但是,你可以有一个不属于公共进程的活动

class SomeMinorActivity : BaseActivity(){

    override fun provideHelpFilePath(): String  = "Why you forcing me to override this?"
}

当然,我们现在可以将我们的BaseActivity分成两个独立的基类 BaseActivityWithBusinessProcessSupportBaseActivityWithoutHelp ,甚至可以创建 GrandBaseActivity ,这将是每个活动的根,但你可以看到它要去哪里。这个问题可能仍然会发生在这种层次结构的每一级,并且为代码的特定部分提供特定抽象逻辑的抽象类的数量可能开始急剧上升。

只有在需要的时候才以某种方式添加部分功能,这将是一件好事。我们不能用抽象类来解决这个问题,因为你只能扩展一个。但是我们可以继承多个接口。我们能不能以某种方式利用它们来混合功能?为了获得灵感,让我们来看看其他语言是如何解决这个问题的。

"BaseActivity "能否(以某种方式)被组成?

有一种语言似乎正在实现所有可以在JVM上实现的功能--向Scala问好。

让我们来看看这个简化的Activity,它有两个 "框架方法 "和一个trait,提供象征性的日志功能。

abstract class Activity{
  def onStart(): Unit = {}

  def onDestroy():Unit = {}
}

trait SomeLogger{
  def debug(msg:String): Unit = println(s"[debug] $msg")
}

现在,什么是特质?它是一种类似于接口的东西,具有默认的方法实现,在Scala中,有时还带有字段。在下面的例子中,我们还可以观察到一个特点。

看看下面的DefaultLifecycleLogging。它有这样一句奇怪的话:self:Activity with SomeLogger 。 这意味着它只能被继承/混合到一个完全满足两个要求的类中。

  1. 该类可以被看作是一个活动
  2. 它有混合的 SomeLogger 特质。

因为这两个要求,我们可以在我们的trait中假设 "它是一个活动,并且可以访问SomeLogger的方法"。所以在活动的重写方法onStart中,我们可以很容易地使用SomeLogger的 debug 方法。

trait DefaultLifecycleLogging {
  self:Activity with SomeLogger => val messageOnDestroy : String

  override def onStart(): Unit = {
    debug("STARTED in LifecycleLogging")
  }

  override def onDestroy(): Unit = {
    debug(s"DESTROYED in LifecycleLogging with message = $messageOnDestroy")
  }
}

当我们连接所有的元素时,这就是特质如何被用来组成特定的 "基本活动行为"。我们有BusinessActivity,它明确地使用了DefaultLifecycleLogging功能,并提供了 messageOnDestroy 的强制实现。在SomeSmallActivity中,我们并不关心DefaultLifecycleLogging......而且更重要的是,通过不组成上述特质,我们并不被迫实现 messageOnDestroy! 所以这很好地说明了这种组合比单一的抽象类更方便使用

class BusinessActivity extends  
Activity with SomeLogger with DefaultLifecycleLogging  {

  override val messageOnDestroy: String = "End of business"


  override def onStart(): Unit = {
    debug("start in BusinessActivity")
  }

}

class SomeSmallActivity extends Activity with SomeLogger {

  override def onStart(): Unit = {
    debug("No need to override any unnecessary variables here")
  }
}

当我们运行我们的示例代码时,我们应该看到onStart被直接从BusinessActivity中调用,而default版本的onDestroy是由组成trait提供的。

 val a:Activity=new BusinessActivity()

  a.onStart()
  a.onDestroy()

}

///
[debug] start in BusinessActivity
[debug] DESTROYED in LifecycleLogging with message = End of business

既然我们对Kotlin的解决方案感兴趣,为什么还要展示Scala的例子?很多时候,程序员会把语言限制和设计限制搞错。Scala和Kotlin都被编译成了java字节码,所以从技术上来说,两者都应该能够产生类似的可执行代码。在语言层面上,Kotlin当然没有Scala的特性,但即使我们没有工具,也许我们仍然能够模拟其机制。

但在此之前,我们再来看看在移动开发领域迅速普及的语言中的混合器的例子。

另一个插图--在Flutter中组成 "BaseWidget"

Flutter是为Android(和iOS)编写应用程序的一个相对新鲜的选择。Flutter中的主要构件是一个Widget-,它与Activity完全不同,但对这个例子来说并不重要。

但仍有可能出现这样的情况:我们希望有一个类似于BaseWidget的东西,并具有一些共同的功能......我们会遇到前面提到的同样的问题,即 "有时 "使用的逻辑被每个构建块所继承,即使它不是必须的。

例如,我们希望有一个小型的实用功能,以简化更新widget状态的onClick函数的创建。如果我用一个标准的方式来写这样的匿名函数,它可能看起来像这样。

Widget build(BuildContext context) => Container(
      child: Center(
        child: RaisedButton(
          child: Text("value $_howMany"),
          onPressed: (){
            setState(() {
              _howMany++;
            });
          }  
      
        ),
      ),
    );

缩进的程度急剧上升。在外部声明这个函数是个好主意,这里只需使用函数引用,但这个问题会在每个onClick函数中出现。

因此,解决方案是创建一个专门的mixin,它可以充实基础widget类。Flutter使用Dart作为编程语言,Dart对混合器有本地支持。同时,语言支持混合器的 "自我意识",所以我们可以像之前用scala traits做的那样--明确说明它是混合到一个属于或继承于Widget State的类

mixin StateUpdater<T extends StatefulWidget> on State<T> {
  VoidCallback update(VoidCallback callback) {
    return () => setState(callback);
  }
}

在这个例子中,mixin就像scala中的trait,而 "on State "表示它只能被混合......以及在State上。

所以现在看看我们的widget中的onClick在经过上述mixin的丰富后是什么样子的

class AppBody extends StatefulWidget {
  @override
  _AppBodyState createState() => _AppBodyState();
}

class _AppBodyState extends State<AppBody> with StateUpdater {
  int _howMany = 1;

  @override
  Widget build(BuildContext context) => Container(
        child: Center(
          child: RaisedButton(
            child: Text("value $_howMany"),
            onPressed: update(() => _howMany++),
          ),
        ),
      );

}

语法更简洁,mixin为我们提供了很好的包装,因为它可以使用状态功能--它明确声明它将在状态上混合。

好了,我们看到了scala的灵魂,我们看到了Flutter/Dart的解决方案。现在让我们看看在Kotlin中可以做什么。

在Kotlin中可能发生的事情

在我们第一次尝试在Kotlin中模拟mixins时,我们将使用接口。好消息是,Kotlin中的接口可以有默认的方法实现,但与Scala或Dart相比,有两个严重的限制。

  1. 接口没有静态类型限制。所以接口不能知道它将被连接的类型家族。
  2. 没有私有状态,这就限制了这种方法只能在方法中添加实现的行为。
//this is the primary interface which is able to provide activity
//no guarantees that it will actually return itself
interface SelfActivityAware{
    val self:Activity
}

还有两个 "假混合"。第一个提供创建文本的功能,用于记录输入意图的基本信息。

interface IntentLog : SelfActivityAware{

    fun smallIntentInfo() : String{
        val actionName = self.intent.action ?: "UNKNOWN ACTION"
        val extras = self.intent.extras ?: "EMPTY BUNDLE"
        return "Intent{actionName=$actionName , extras : ($extras)}"
    }
}

第二种--完全独立于第一种--只是提供了围绕从活动中获取的一些属性构建的基本日志功能。

interface HasLoggingTag : SelfActivityAware{

    fun debug(message:String){
        val tag = self.localClassName
        Log.d(tag,message)
    }
}

最后,我们可以将两个接口组装在一个Activity中,首先使用第一个接口的功能来收集意图信息,然后从第二个接口继承的功能来记录这些信息。

class Process1Activity : Activity(), IntentLog, HasLoggingTag {
    override val self: Activity
        get() = this

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val intentInfo = smallIntentInfo()
        debug(intentInfo)
 }

}

我同意这种机制非常有限,只能用于组合纯行为,但还有一些东西......

用委托添加状态

我们可以在这个"mixin siumulation"上走得更远,但我们正在进入一个严重的创造性过度工程的领域 :D。Kotlin有一个有趣的功能,你可以实现一个接口并立即提供委托,为这个接口提供功能。

例如,我想让mixin具有缓冲日志的功能,我真的需要一个状态来缓冲消息。作为开始,让我们创建一个标准的接口-实现对。

interface HasBufferedLogger : HasLoggingTag{
    fun addLog(message:String)
}

class StaticBufferedLogger(private val bufferSize:Int=10) : HasBufferedLogger {

    private val messages = mutableListOf<String>()

    
    override fun addLog(message: String) {
        messages.add(message)
        if(messages.size >= bufferSize){
            val logContent=messages.joinToString()
            debug(logContent)
            messages.clear()

        }
    }

...   
}

由于委托机制的存在,我们可以很容易地将两者与我们的活动结合起来

class Process1Activity () : Activity(), IntentLog, HasBufferedLogger by delegate {

这通常是可行的,但我们还需要提供self的实现,这是每个 "自我意识混合器 "的基础。我们的委托不是一个活动,所以它不能直接作为自我。我们需要通过一个活动来扩展它,但这使情况变得复杂,因为它在活动和委托之间产生了一个依赖循环。

我能够解决这个问题的唯一方法是在创建活动后将活动与委托绑定。

所以没有复杂的时间来保证绑定会真正发生。

class StaticBufferedLogger(private val bufferSize:Int=10) : HasBufferedLogger {

    private val messages = mutableListOf<String>()

    private lateinit var activity:Activity

    fun bind(a:Activity){activity=a}

    override fun addLog(message: String) {
      ....
    }

    override val self: Activity
        get() = activity
}

和一个小例子,说明这在活动中如何工作

class Process1Activity private constructor (   //1
private val delegate:StaticBufferedLogger) : Activity(), IntentLog, 

HasBufferedLogger by delegate {  //2

    constructor() : this(StaticBufferedLogger()) {  //3
        delegate.bind(this)
    

}
...

好了,这里发生了什么

  1. 我们需要有一个私有的构造函数,在那里我们可以控制我们的委托的创建,以确保绑定的完成。
  2. 这很好--Kotlin允许你使用特定的对象作为委托--我们的对象来自私有构造函数。
  3. 这将是一个公共参数较少的构造函数,它负责处理委托的绑定问题。

正如我所说的,过度工程的界限在这里是模糊的,但是当我们在BaseActivity中有太多的功能时,它应该提供帮助。当然,你也可以在BaseActivity方法中 "混合混合"。两者都是满足你需求的工具。

我在Intellij中检查了反编译的Java,生成的字节码不应该有任何令人讨厌的意外。

public final class Process1Activity extends Activity implements IntentLog, HasBufferedLogger {
//just a mutable field
   private final StaticBufferedLogger delegate;

//private constructor for us and public for users so there is only way to initialize this class where binding always occurs
private Process1Activity(StaticBufferedLogger delegate) {
   this.delegate = delegate;
}

public Process1Activity() {
   this(new StaticBufferedLogger(0, 1, (DefaultConstructorMarker)null));
   this.delegate.bind((Activity)this);
}

//And delegation
@NotNull
public Activity getSelf() {
   return this.delegate.getSelf();
}

public void addLog(@NotNull String message) {
   Intrinsics.checkParameterIsNotNull(message, "message");
   this.delegate.addLog(message);
}

public void debug(@NotNull String message) {
   Intrinsics.checkParameterIsNotNull(message, "message");
   this.delegate.debug(message);
}

总结

在这篇文章中,我们从Scala和Flutter/Dart中获得灵感,创建了Kotlin解决方案,在我看来,这可能对特定情况有帮助。所以首先要记住,你不需要用所有可能的通用功能来创建MegaUberBaseActivity,因为Kotlin有一些不错的功能,可以用组合来代替继承。

此外,还要不时地看看其他技术,因为你的问题的解决方案可能已经存在了;)


www.deepl.com 翻译