LoginSignup
12
7

More than 3 years have passed since last update.

Scalaでデザインパターンを書いてみるメモ

Last updated at Posted at 2018-06-07

勉強がてらScalaでGoFのデザインパターンを自分で考えながら書いてみた時のメモです。
Scala初心者なので、見る人が見たらあんまり参考にならないかもです。

GoFの23パターンのうち、よく使いそうなパターンのみ抜粋して書いていってます。。

オブジェクトの生成に関わるデザインパターン

  • AbstractFactoryパターン
  • Builderパターン
  • Singletonパターン

AbstractFactory

関連する一連のインスタンスをまとめて生成するためのデザインパターン。

抽象クラスは以下のような感じ。

/**
  * 抽象ファクトリ。
  */
trait AbstractFactory {
  def getConnection: Connection

  def getConfiguration: Configuration

}

/**
  * Abstract Productに相当
  */
trait Connection {
  // 任意の処理
}

/**
  * Abstract Productに相当
  */
trait Configuration {
  // 任意の処理
}

具象系のクラスを以下のように定義。

/**
  * MySQLのコネクション、設定を生成する具象ファクトリ
  * Concrete Factoryに相当
  */
class MySQLFactory extends AbstractFactory {
  override def getConnection: Connection = MySQLConnection

  override def getConfiguration: Configuration = MySQLConfiguration
}

/**
  * Concrete Productに相当
  */
object MySQLConnection extends Connection {
  // MySQLのコネクション処理
}

/**
  * Concrete Productに相当
  */
object MySQLConfiguration extends Configuration {
  // MySQLの設定情報読み込み処理
}
/**
  * PostgreSQLのコネクション、設定を生成する具象ファクトリ
  * Concrete Factoryに相当
  */
class PostgreSQLFactory extends AbstractFactory {
  override def getConnection: Connection = PostgreSQLConnection

  override def getConfiguration: Configuration = PostgreSQLConfiguration
}

/**
  * Concrete Productに相当
  */
object PostgreSQLConnection extends Connection {
  // PostgreSQLのコネクション処理
}

/**
  * Concrete Productに相当
  */
object PostgreSQLConfiguration extends Configuration {
  // PostgreSQLの設定情報読み込み処理
}

ファクトリを利用する側の実装。こんな感じかな。。

sealed trait DBMS

case object MySQL extends DBMS

case object PostgreSQL extends DBMS

// 実際に利用する側のメイン処理
object Main {
  def main(args: Array[String]): Unit = {
    val env = PostgreSQL

    val dbmsFactory = createDBMSFactory(env)

    val connection = dbmsFactory.getConfiguration

    val configuration = dbmsFactory.getConfiguration

  }

  def createDBMSFactory(env: DBMS) =
    env match {
      case MySQL =>
        new MySQLFactory
      case PostgreSQL =>
        new PostgreSQLFactory
    }
}

Builderパターン

インスタンス生成手順が複雑な場合やインスタンス生成構成が複雑な場合に、生成を簡略化するパターン。

// Builderパターンで生成するPerson
case class Person(name: String, age: Int, hobby: String)


// 組み立てが複雑で生成コストの高いPersonオブジェクトを生成するBuilderクラス
class PersonBuilder(person: Person = Person("Unknown", 0, "No hobby")) {

  // nameをセットする
  // nameのチェック処理などをしないといけないが、簡単にしている
  def setName(name: String): PersonBuilder =
    new PersonBuilder(person.copy(name = name))

  // ageをセットする
  // ageのチェック処理などをしないといけないが、簡単にしている
  def setAge(age: Int): PersonBuilder =
    new PersonBuilder(person.copy(age = age))

  // hobbyをセットする
  // hobbyのチェック処理などをしないといけないが、簡単にしている
  def setHobby(hobby: String): PersonBuilder =
    new PersonBuilder(person.copy(hobby = hobby))

  // オブジェクトを生成する
  def build(): Person = this.person
}


// ディレクタがPersonの生成手順を知っている
object Director extends App {

  // オブジェクトを生成するDirector
  val person = new PersonBuilder().setName("48hands").setAge(99).setHobby("盆栽").build()

}

上のような単純な例であれば、ケースクラスのcopyメソッドを使ってもう少し簡単に書けました。

もともと提供されているnewで生成できないクラスがあった場合に使えそうです。

// newで生成できないPersonクラス
class Person private(name: String, age: Int, hobby: String) {
  // defined methods
}
object Person {
  def getInstance(name: String, age: Int, hobby: String): Person = new Person(name, age, hobby)
}

Builderを定義します。

// Personのビルダークラス
case class PersonBuilder(name: String = "Unknown", age: Int = 777, hobby: String = "Unknown") {
  def build() = Person.getInstance(name, age, hobby)
}

以下のように使います。

object SampleApp extends App {
  val person: Person = PersonBuilder()
    .copy(name = "48hands")
    .copy(age = 22, hobby = "盆栽")
    .build()
}

Singletonパターン

あるクラスについて、インスタンスが唯一の存在であることを保証するためのデザインパターン。

システム内で変更された設定内容も保持する必要がある場合、設定情報を保持するインスタンスが複数あると、インスタンス間で整合性が取れなくなる。常に唯一のインスタンスであれば整合性が確保できる。

// 設定情報を管理するシングルトンオブジェクト
object Configure {
  def getHostName: String = ???

  def getUrl: String = ???
}

プログラムの構造に関するパターン

  • Adapterパターン
  • Compositeパターン

Adapterパターン

インタフェースに互換性のないクラス同士を組み合わせるパターン
既存のプログラムを再利用して新しいプログラムに適合させたりするパターン。

新しいプログラムのインタフェースが既存のプログラムのインタフェースとは異なるインタフェースである場合に、既存のプログラムを新しいインタフェースに適合させる。

以下の2つの方法があるので、両方書いてみました。

  • 継承を利用する方法
  • 委譲を利用する方法

GoFのプリンシパルは、「委譲、委譲、委譲」、「継承より集約」なので、委譲のほうが思想としてマッチするはず。

継承を利用する方法

/**
  * 既存のプログラムのクラス
  */
class OldSystem {

  def oldProcess01(): Unit = {
    println("既存01処理です。")
  }

  def oldProcess02(): Unit = {
    println("既存02処理です、")
  }

}

/**
  * 新しいプログラムが持っているメソッドを定義した
  * インタフェース
  */
trait Target {
  def newProcess01(): Unit

  def newProcess02(): Unit
}

/**
  * OldSystemを継承したアダプター
  */
class Adapter extends OldSystem with Target {

  // 既存プログラムのoldProcess01をnewProcess01に適合させる
  override def newProcess01(): Unit = {
    oldProcess01()
  }

  // 既存プログラムのoldProcess02をnewProcess02に適合させる
  override def newProcess02(): Unit = {
    oldProcess02()
  }
}

/**
  * アダプターを呼び出す処理
  */
object ExtendAdapterMain extends App {
  val target = new Adapter

  target.newProcess01()
  target.newProcess02()

}

ここで、新しいプログラムのインターフェースtrait Targetとしている箇所がabstract class Targetで宣言されていた場合を考える。

AdapterクラスがOldSystemTargetクラスを両方継承できない(単一継承しかできない)。

そういう場合は継承よりも委譲を使うことにする。

委譲を利用する方法

/**
  * 既存のプログラムのクラス
  */
class OldSystem {

  def oldProcess01(): Unit = {
    println("既存01処理です。")
  }

  def oldProcess02(): Unit = {
    println("既存02処理です、")
  }

}

/**
  * 新しいプログラムが持っているメソッドを定義した
  * インタフェース
  */
abstract class Target { // traitで宣言するのが一般的だと思う。
  def newProcess01(): Unit

  def newProcess02(): Unit
}

/**
  * アダプター
  * OldSystemに処理を委譲する
  */
class Adapter(oldSystem: OldSystem) extends Target {

  // 既存プログラムのoldProcess01をnewProcess01に適合させる
  override def newProcess01(): Unit = {
    oldSystem.oldProcess01()
  }

  // 既存プログラムのoldProcess02をnewProcess02に適合させる
  override def newProcess02(): Unit = {
    oldSystem.oldProcess02()
  }
}

/**
  * アダプターを呼び出す処理(委譲ver)
  */
object DelegationAdapterMain extends App {
  val oldSystem = new OldSystem

  val target = new Adapter(oldSystem)
  target.newProcess01()
  target.newProcess02()

}

補足ですが、上記では、

class Adapter(oldSystem: OldSystem) extends Target {
  ...

としてOldSystemのオブジェクトを渡していますが、以下のように
クラス内部でインスタンス変数として生成する方法もあると思います。

class Adapter extends Target {
  private val oldSystem = new OldSystem
  ...

この場合、Adapterクラスのテストをしようとしたときに、テストの内容がOldSystemに依存してしまうと思ったので、コンストラクタでOldSystemのオブジェクトを渡すようにしました。

Compositeパターン

再帰的な構造を取り扱いを効率的にするデザインパターン。

よくあるファイルシステムを例にして、ディレクトリとファイルを同一視してディレクトリであれば、再帰的な構造を持つようにするような感じです。

以下の通り簡単に実装してみたのですが、もっと良い実装がある気がします。
Scalaレベルが上ってきたら再度挑戦しようと思います。

sealed trait Entry

case class File(name: String) extends Entry

case class Directory(name: String, entries: Entry*) extends Entry {

  def addEntry(entry: Entry) =
    Directory(name, entries :+ entry: _*)

}


object CompositeSampleMain extends App {

  val emptyDirectory = Directory("EmptyDirectory")
  println(emptyDirectory)

  val notEmptydirectory = Directory("NotEmptyDirectory", File("file1"), File("file2")).addEntry(File("file3"))
  println(notEmptydirectory)

  // emptyDirectoryとnotEmptyDirectoryを集約
  val rootDirectory = Directory("RootDirectory", emptyDirectory, notEmptydirectory, File("file4"))
  println(rootDirectory)

}

オブジェクトの振る舞い

  • Commmandパターン
  • Strategyパターン
  • Template Methodパターン
  • Iteratorパターン
  • Observerパターン

Commandパターン

命令をインスタンスとして扱い、処理の組み合わせを簡単にするためのパターン。

処理内容が似た命令をパターンに応じて使い分けたり、組み合わせたりして実行する。

こんな感じだと思います。。

trait Command {
  def execute(): Unit
}

class CommandA extends Command {
  override def execute(): Unit = println("Execute command A")
}

class CommandB extends Command {
  override def execute(): Unit = println("Execute command B")
}

class Executor(commands: List[Command] = List.empty) {
  // コマンドを追加する
  def addCommand(command: Command) = new Executor(commands :+ command)

  // コマンド群を実行する
  def executeCommands(): Unit = commands.foreach(_.execute())
}

// コマンドを組み立てて実行するクラス
object CommandSampleMain extends App {

  val executor = new Executor()

  // 命令群を実行する
  executor
    .addCommand(new CommandA) // CommandAを追加
    .addCommand(new CommandB) // CommandBを追加
    .executeCommands()

}

@takezoux2さんが書かれているように、関数で書くともっとシンプルです。

object CommandSampleMain2 extends App {

  // 関数オブジェクトでコマンドを表現する
  val commandA = (s: String) => println(s + "A")
  val commandB = (s: String) => println(s + "B")

  // コマンドをセット
  val commands = List(commandA, commandB)

  // 命令群を実行
  commands.foreach(f => f("Execute command "))

}

今度は、Stackっぽいものを使った版です。

trait CommandStack[T <: {def execute(): Unit}] {
  sealed abstract class CommandStack {
    def isEmpty: Boolean
    def addCommand(elm: T): CommandStack
    def execute(): Unit
  }

  case class NonEmptyCommandStack(head: T, tail: CommandStack) extends CommandStack {
    override def isEmpty: Boolean = false
    override def addCommand(elm: T): CommandStack = NonEmptyCommandStack(elm, this)
    override def execute(): Unit = {
      head.execute()
      if (tail != EmptyCommandStack) tail.execute()
    }
  }

  case object EmptyCommandStack extends CommandStack {
    override def isEmpty: Boolean = true
    override def addCommand(elm: T): CommandStack = NonEmptyCommandStack(elm, this)
    override def execute(): Unit = throw new IllegalStateException
  }
}
trait Command {
  def execute(): Unit
}

class SQLCommand extends Command {
  override def execute(): Unit = println("sql execute!")
}

class BashCommand extends Command {
  override def execute(): Unit = println("bash execute!")
}
object CommandExecutor extends CommandStack[Command] {
  def main(args: Array[String]): Unit = {
    val sqlCommand = new SQLCommand
    val bashCommand = new BashCommand

    val commandStack = EmptyCommandStack
      .addCommand(sqlCommand)
      .addCommand(bashCommand)

    commandStack.execute()
  }
}

Strategyパターン

戦略を簡単に切り替える仕組みを提供するためのデザインパターンです。
Strategyは簡単に言うと処理のアルゴリズム。

本屋さんの販売戦略を実装してみました。
本を通常のディスカウントで販売する戦略(DiscountStrategy)とスペシャルディスカウントで販売する戦略(SpecialDiscountStrategy)を定義してimplicitで戦略を渡すようにしてみました。

case class Book(title: String, price: Double)

trait Strategy {
  def discount(book: Book): Book
}

class DiscountStrategy extends Strategy {
  override def discount(book: Book): Book =
    Book(book.title, book.price * 0.9)
}

class SpecialDiscountStrategy extends Strategy {
  override def discount(book: Book): Book =
    Book(book.title, book.price * 0.7)
}

// implicit parameterでStrategyを渡す
// 本屋さんクラス
class BookShop(implicit strategy: Strategy) {

  def sell(book: Book) = {
    val price = strategy.discount(book).price
    println(s"${book.title}を${price}万円で販売します。")
  }

}

object StrategySampleMain extends App {

  // 本屋さんの販売戦略を定義する。ここでは特別価格な戦略を設定
  implicit val strategy = new SpecialDiscountStrategy

  // 本屋さんに暗黙的にstrategyが渡される
  val bookShop: BookShop = new BookShop()

  val book = new Book("イチャイチャパラダイス", 20)

  bookShop.sell(book)

}

型クラスを使った Strategyパターン

以下は商品種別ごとのディスカウント戦略を型クラスを使って実装したパターンです。

case class AnimeVideo(title: String, price: Double)

case class AdultVideo(title: String, price: Double, age: Int)

// ディスカウント戦略の型クラス
trait DiscountStrategy[A] {
  def discount(item: A): A
}

// 商品種別ごとにディスカウントの戦略を決めるためのオブジェクト
object DiscountStrategy {
  // アニメビデオのディスカウント戦略
  // 型インスタンスはAnimeVideo
  implicit object AnimeVideoDiscountStrategy extends DiscountStrategy[AnimeVideo] {
    override def discount(item: AnimeVideo): AnimeVideo = AnimeVideo(item.title, item.price * 0.7)
  }

  // アダルトビデオのディスカウント戦略
  // 型インスタンスはAdultVideo
  implicit object AdultVideoDiscountStrategy extends DiscountStrategy[AdultVideo] {
    override def discount(item: AdultVideo): AdultVideo =
      AdultVideo(item.title, item.price * 0.5, item.age)
  }
}

上で定義したDiscountStrategyを使う側は以下のように実装しました。
discountVideoメソッドの第二引数リストにimplicitをつかって暗黙的に戦略を渡しています。

object SellApp {
  import DiscountStrategy._

  def discountVideo[A](item: A)(implicit discountStrategy: DiscountStrategy[A]) = {
    discountStrategy.discount(item)
  }

  def main(args: Array[String]): Unit = {
    val video1 = AnimeVideo("NARUTO", 1000)
    val video2 = AdultVideo("いちゃいちゃパラダイス", 1000, 20)

    println("--- before discount ---")
    println(s"${video1.title} : ${video1.price}")
    println(s"${video2.title} : ${video2.price}")

    val discountedVideo1 = discountVideo(video1)
    val discountedVideo2 = discountVideo(video2)

    println("--- after discount ---")
    println(s"${discountedVideo1.title} : ${discountedVideo1.price}")
    println(s"${discountedVideo2.title} : ${discountedVideo2.price}")
  }
}

実行結果は以下です。

--- before discount ---
NARUTO : 1000.0
いちゃいちゃパラダイス : 1000.0
--- after discount ---
NARUTO : 700.0
いちゃいちゃパラダイス : 500.0

Template Methodパターン

複数のコードでやりたいこと(アルゴリズム)がほとんど同じである一部だけ変えたいようなときに有効なデザインパターンです。

Scalaだとtraitのミックスインによって複数の機能を小分けに追加できたりします。

まずは、以下のようなテンプレを作っておいて、

trait LayoutA {
  def printHeader(): Unit
}

trait LayoutC {
  def printFooter(): Unit
}

trait LayoutB {
  def printBody(): Unit
}

// レポートの雛形トレイト
trait Report {
  // LayoutA, LayoutB, LayoutCを
  // 継承したトレイトをミックスインさせるように強制させる
  self: LayoutA with LayoutB with LayoutC =>

  def printWhole(): Unit = {
    printHeader()
    printBody()
    printFooter()
  }
}

ミックスインの機能を使ってTextReportHtmlReportを定義しました。

trait TextLayoutA extends LayoutA {
  override def printHeader(): Unit = println("Text Header")
}

trait TextLayoutB extends LayoutB {
  override def printBody(): Unit = println("Text Body")
}

trait TextLayoutC extends LayoutC {
  override def printFooter(): Unit = println("Text Footer")
}

// テキストレポートのクラス
class TextReport extends Report with TextLayoutA with TextLayoutB with TextLayoutC
trait HtmlLayoutA extends LayoutA {
  override def printHeader(): Unit = println("HTML Header")
}

trait HtmlLayoutB extends LayoutB {
  override def printBody(): Unit = println("HTML Body")
}

trait HtmlLayoutC extends LayoutC {
  override def printFooter(): Unit = println("HTML Footer")
}

// HTMLレポートのクラス
class HtmlReport extends Report with HtmlLayoutA with HtmlLayoutB with HtmlLayoutC

ついでにテンプレートメソッドを利用する側は以下のような感じです。

object TemplateMethodSampleMain extends App {
  val textReport: Report = new TextReport
  val htmlReport: Report = new HtmlReport

  textReport.printWhole()
  htmlReport.printWhole()
}

状態管理に関するパターン

Observer

trait Subscriber {
  def doSomething(): Unit
}

class BatchExecuteSubscriber extends Subscriber {
  override def doSomething(): Unit = println("Batch execute")
}

class EmailSendSubscriber extends Subscriber {
  override def doSomething(): Unit = println("Email send")
}

trait Publisher {
  val subscribers: List[Subscriber]

  def addSubscriber(subscriber: Subscriber): Publisher

  def publish(): Unit
}

class EventPublisher(override val subscribers: List[Subscriber] = List.empty) extends Publisher {
  override def addSubscriber(subscriber: Subscriber): Publisher =
    new EventPublisher(subscriber :: subscribers)

  override def publish(): Unit =
    subscribers.foreach(subscriber => subscriber.doSomething())
}

def main(args: Array[String]): Unit = 
    new EventPublisher()
      .addSubscriber(new BatchExecuteSubscriber)
      .addSubscriber(new EmailSendSubscriber)
      .publish()

DI(Dependency Injection)

// TODO つかれたので、つづきはまたあとで追記する

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7