【问题标题】:Scala Factory Pattern with Generic具有泛型的 Scala 工厂模式
【发布时间】:2021-10-12 00:55:39
【问题描述】:

我想使用带有泛型的工厂方法,它可以与特定的实现一起使用。在服务类中我想要类型安全,但在控制器中操作只知道接口。

代码

我已经定义了不同类型的操作类型

trait Transaction {
  val amount: BigDecimal
}
case class CreditCardTransaction(amount: BigDecimal, ccNumber: String, expiry: String) extends Transaction
case class BankTransaction(amount: BigDecimal, bankAccount: String) extends Transaction

以及可以使用特定操作类型的服务

trait Service[T <: Transaction] {
  def transfer(transaction: T)
}

class CCService() extends Service[CreditCardTransaction] {
  override def transfer(transaction: CreditCardTransaction): Unit = println("pay with cc")
}
class TTService() extends Service[BankTransaction] {
  override def transfer(transaction: BankTransaction): Unit = println("pay with telex transfer")
}

我已经用具体实例创建了工厂

class PaymentSystemFactory(ccService: CCService, ttService: TTService) {
  def getService(paymentMethod: String) = paymentMethod match {
    case "cc" => ccService
    case "tt" => ttService
  }
}

解析器从外部服务获取特定交易

object Parser {
  def parse(service: Service[_ <: Transaction]) = service  match {
    case _: Service[CreditCardTransaction] => CreditCardTransaction(100, "Name", "01/01")
    case _: Service[BankTransaction] => BankTransaction(100, "1234")
  }
}

但由于提供的类型与 PaymentSystemFactory 方法不匹配,该代码不想编译:

object App {
  val factory = new PaymentSystemFactory(new CCService, new TTService)
  val service = factory.getService("cc") // return Service[_ >: CreditCardTransaction with BankTransaction <: Transaction]
  val transaction: Transaction = Parser.parse(service)
  service.transfer(transaction) // Failed here: Required _$1 found Transaction
}

如果可能由于工厂方法调用,我很乐意避免类型擦除,并想知道为什么该代码不起作用

【问题讨论】:

    标签: scala generics factory factory-pattern


    【解决方案1】:

    您在这里所做的(显然是偶然的?)是创建一个广义代数数据类型,简称 GADT。如果您想了解有关此功能的更多信息,这可能是一个有用的搜索词。

    至于如何做到这一点:parse方法的类型签名需要反映返回事务的类型与服务的事务类型匹配。

    另外,你不能这样做case _: Service[CreditCardTransaction],因为擦除将无法正常工作。请改用case _: CCService

    试试这个:

    object Parser {
      def parse[A <: Transaction](service: Service[A]): A = service  match {
        case _: CCService => CreditCardTransaction(100, "Name", "01/01")
        case _: TTService => BankTransaction(100, "1234")
      }
    }
    

    您还需要更改调用代码:

    object App {
      val factory = new PaymentSystemFactory(new CCService, new TTService)
      factory.getService("cc") match {
        case service: Service[a] =>
          val transaction: a = Parser.parse(service)
          service.transfer(transaction)
      }
    }
    

    请注意,match 并不用于实际区分多种情况。相反,它在这里的唯一目的是为事务类型命名,在这种情况下为a。这是 Scala 语言中最晦涩的特性之一。当您对通配符类型进行模式匹配并使用a 之类的小写名称作为类型参数时,它不会检查类型是否为a(就像检查大写名称一样),但是它创建了一个新的类型变量,您以后可以使用它。在这种情况下,它用于声明transaction 变量,并隐式调用Parser.parse 方法。

    【讨论】:

    • 哇,我不知道这个功能。我实际上在阅读代码时想知道为什么您使用小写字母作为类型。我正在使用类型成员制作解决方案,但该解决方案更好!
    【解决方案2】:

    当我在制定以下解决方案时,@Mathias 提出了另一个我觉得非常好的解决方案。但我仍然将它作为可能有趣的替代品发布。

    你可以使用类型成员来代替类型参数(即泛型):

    trait Service {
      type T <: Transaction
      def transfer(transaction: T): Unit
    }
    class CCService() extends Service {
      type T = CreditCardTransaction
      override def transfer(transaction: CreditCardTransaction): Unit = println("pay with cc")
    }
    class TTService() extends Service {
      type T = BankTransaction
      override def transfer(transaction: BankTransaction): Unit = println("pay with telex transfer")
    }
    
    // need to use asInstanceOf, I don't know how to tell scala that the type is safe
    def parse(service: Service): service.T = service  match {
      case _: CCService => CreditCardTransaction(100, "Name", "01/01").asInstanceOf[service.T]
      case _: TTService => BankTransaction(100, "1234").asInstanceOf[service.T]
    }
    
    val factory = new PaymentSystemFactory(new CCService, new TTService)
    val service = factory.getService("tt")
    val transaction = parse(service) // transaction has type service.T
    service.transfer(transaction)
    

    【讨论】:

    • 是的,类型成员是这个问题的另一种解决方案,但它带来了一组不同的权衡。我的解决方案允许您在不同的服务实例之间传递事务,只要它们的事务类型匹配。使用类型成员,类型绑定到实例——这是好是坏,取决于用例。另请注意,您的 asInstanceOf 问题实际上与 Scala 版本无关。事务需要由服务本身创建才能进行类型检查。使用这种方法(没有asInstanceOf)是不可能实现像Parser.parse 这样的。
    • 顺便说一句,您实际上并不需要 type T = … 行。相反,您可以在此处声明案例类,即。 e. case class T(amount: BigDecimal, bankAccount: String) extends Transaction。节省了一点打字……
    • 感谢 cmets。我更正了我对asInstanceOf 的评论。关于第二个,我决定继续在服务之外声明事务类,以遵循问题中的建议。我的主要观点是使用 type-member 而不是 type-argument 能够拥有类型service.T
    猜你喜欢
    • 2016-12-26
    • 1970-01-01
    • 2020-09-17
    • 1970-01-01
    • 1970-01-01
    • 2021-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多