ラベル oop の投稿を表示しています。 すべての投稿を表示
ラベル oop の投稿を表示しています。 すべての投稿を表示

2016年10月11日火曜日

Object-Functional Analysis and Designふりかえり

クラウド時代のアプリケーション開発について、「クラウド・アプリケーション・モデリング」、「クラウド・アプリケーション開発のモデル体系」と考察してきました。

クラウド・アプリケーション開発では、実装時のプログラミングで「関数」が重要な構成要素となってきています。そうなると、この「関数」を上流のモデリングでどのように扱っていくのかということが重要な論点になります。

このような観点から、上流のモデリングから実装時のOFP(Object-Functional Programming)まで、オブジェクトと関数を融合させ一気通貫にまとめた開発方法論をModegramming StyleではObject-Functional Analysis and Design(OFAD)と呼んでいます。

Modegramming Styleでは2012年ぐらいからOFADについて考察を進めてきました。クラウド・アプリケーションのモデリングの検討を進めていく上で、OFADが一つの軸となると思います。このOFADについて、2012年に要求開発アライアンスでのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』向けにまとめたものがあります。

今回はこの2012年版OFADのふりかえりを行い、検討を進めていくうえでの論点整理を行いたいと思います。

Object-Functional Analysis and Design 2012

クラウド・アプリケーションの開発方法論を整備していくためには「関数」を理解した上で、関数とオブジェクトの関係を整理しないといけないという動機もあり、OFPの有力言語であるScalaを2008年から使い始めました。

ある程度「関数」とOFPについて勘所がつかめてきたところで、2012年に『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というセッションのタイミングで一度まとめるタイミングがありました。

このセッションの内容は以下にまとまっています。

また関連して以下のような考察を行っています。

ふりかえり

今の目でOFAD 2012をチェックしてみましたが、それほど違和感はなく、以下の基本的な考え方については変更はありませんでした。

  • オブジェクトと関数の使い分け
  • オブジェクトと関数の連携
  • デザインパターン(代数的構造、圏論)
  • Domain-Driven Design (DDDD)

ただ、オブジェクトと関数の連携方法は2012年当時よりも手持ちの選択肢が増えたと思うので、その辺りは反映していきたいところです。

このタイミングで再検討したいのが以下の項目です。Reactive Streamsを始め、2012年以降、要素技術が大きく進化しているのでこれらの技術を取り込んだ上で新しい枠組みで考えてみたいと思います。

  • DSL
  • データフロー

以下のアーキテクチャ的な話題については2012年以降、特に大きな動きはなかったと思います。これらについてはOFADの再検討の中で対応を考えていきたいと思います。

  • EDA
  • DCI
  • CQRS

OFAD 2012以降の技術動向

OFAD 2012以降に起きた技術的な大きなムーブメントとしては以下の2つがあります。後者は我々の提案ですが、最近注目されているServerless Architectureに通じるところがあると思います。

  • Reactive Streams
  • Application Cloud Platform
Reactive Streams

より広い枠組みとしてはFunctional Reactive Programming(FRP)という切り口もありますが、Reactive Streamsの方が現状にあっていると思うのでReactive Streamsの用語を使います。

Modegramming Styleではscalaz-streamを中心にReactive Streamsについても考察を行ってきました。

また幾つかのセッションでお話させていただいたのでスライドとしてもまとめました。

純粋な数学的な計算はよいとして、システムの振る舞いをFunctional Programming(FP)でどのように記述するのかという点がFP実用化の重要な論点だと思いますが、モナドベースのReactive Streamsが一つの解としてブレークスルーの起点となりそうです。

そのような意味でOFADでの関数を考える上でReactive Streamsは重要な論点になります。

分析モデルの段階で、Reactive Streamsに対応するモデル要素を見つけることができればモデルから実装まで一気通貫でつなげるルートを確保することができます。

Application Cloud Platform

OFAD 2012の後、クラウド時代のアプリケーション開発ではクラウド・プラットフォームが重要な構成要素になると考え、その製品化を行う活動をしてきました。その成果として「Prefer Cloud Platform」をリリースすることができました。

Prefer Cloud PlatformのようなクラウドプラットフォームをModegramming StyleではApplication Cloud Platform(ACP)と呼んでいます。Prefer Cloud Platform自体もScalaによるOFPによって実装されていますが、この開発の中でOFPに関するノウハウ、OFADに対するヒントを蓄積することができました。

またACPによってアプリケーション開発の大きな部分を省略することができることが期待できます。こうなると、モデリングの目的はビジネスとアプリケーションの連携方法の分析とシステムの拡張方法の分析設計に絞られます。このような文脈の中での開発方法論ということもクラウド時代の開発方法論であるOFADに求められる点といえます。

まとめ

クラウド・アプリケーション・モデリング」を起点に進めている考察は『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』によって始動したOFAD 2012を最新技術動向やApplication Cloud Platform(ACP)の活用を前提に、2016年版OFADとして再構築を行うという目的のものです。

この検討を進めるためのベースとしてOFAD 2012を簡単にふりかえり、論点整理を行いました。

このフィードバックを活かして、次回はOFADのモデル体系について考えてみたいと思います。

お知らせ

10月24日に開催される「QCon Tokyo 2016」で以下のテーマでお話させていただくことになりました。

  • オブジェクト‐関数型プログラミングからオブジェクト‐関数型分析設計へ~クラウド時代のモデリングを考える

現在検討しているOFADについてのチュートリアル的な内容になる予定です。

2015年3月23日月曜日

Scala的状態機械/OOP編

オブジェクト・モデリングにおける動的モデルは状態機械で記述するのが基本です。つまり状態機械はオブジェクト・モデリングの重要な構成要素の一つということです。

ScalaでObject-Functional Programming(OFP)を行う場合でも、要求・分析・設計の各アクティビティを経て作成されたオブジェクト・モデル内の状態機械をどのように実装していくのかという実装方式が論点になります。

普通のOOP流の実装はすでに議論しつくされていると思いますが、OFPにおけるFPでの実装方式については、これから整備されていくことになると思います。

注意が必要なのはクラウド・アプリケーション開発をターゲットにする場合、伝統的なFPというよりMonadic Programming(以下MP)を経てFunctional Reactive Programming(以下FRP)がゴールになるということです。

このためFRPとして利用可能な状態機械実装を探っておきたいところです。

課題

状態機械の使い所としては、エンティティの状態遷移を記述することで業務ワークフローの構築に用いたり、プロトコルドライバのフロー制御といったものが考えられます。前者はDBに状態を格納することになりエンタープライズ・アプリケーション的なライフサイクルの長い応用ですし、後者は制御系の応用でメモリ内の制御で完結する形のものです。

色々な応用があるわけですが、どの方面でも使える実現方法を念頭におきつつ、ここではCSVのパーサー処理を状態機械で実装してみることにします。FPとの接続部分に興味を集中させるためできるだけ簡単なものを選んでみました。

具体的には以下の課題になります。

  • CSVの1行を「,」区切りのレコードとしてパースし、文字列の列を取得する処理に使用する状態機械

この状態機械は文字列の列の各文字をイベントとして扱い、一連のイベント終了後にパース処理の結果となる「文字列の列」を状態の情報として保持する形になります。

case classで状態機械

状態機械は「状態×イベント→状態」のメカニズムが基本の構成要素です。このメカニズムをcase classで実装します。

いうまでもなくcase classはScalaプログラミングの超重要な構成要素で、OOP的側面とFP的側面を兼ね備えたObject-Functional Programmingの肝となる機構です。

OOP的にはDDDでいうところのvalue objectの実装に適した文法になっています。

FP的には代数的データ型(algebraic data type)として利用することが想定されています。

つまりvalue object兼代数的データ型を実現するための機構がcase classです。

ただしcase classの文法上はvalue objectや代数的データ型に適さない普通のオブジェクトを実装することも可能です。このため、value object兼代数的データ型を実現するための紳士協定を組み込んでおかなければなりません。

この紳士協定は以下の2つです。

  • 不変オブジェクト(immutable object)にする
  • 基底のトレイトや抽象クラスはsealedにする

Value objectと代数的データ型は共に不変オブジェクトである必要があります。

また代数的データ型の要件としてコンパイル時に全インヘリタンス関係が確定している必要があるので、sealedにすることでこれを担保します。

case classはオブジェクト指向の普通のオブジェクトでもあるので、不変オブジェクトの性質さえ守ればオブジェクトのフルスペックを使用することができます。

package sample

sealed trait ParseState {
  def event(c: Char): ParseState
  def endEvent(): EndState
}

case object InitState extends ParseState {
  def event(c: Char) = c match {
    case ',' => InputState(Vector(""), "")
    case '\n' => EndState(Nil)
    case _ => InputState(Nil, c.toString)
  }
  def endEvent() = EndState(Nil)
}

case class InputState(
  fields: Seq[String],
  candidate: String
) extends ParseState {
  def event(c: Char) = c match {
    case ',' => InputState(fields :+ candidate, "")
    case '\n' => EndState(fields :+ candidate)
    case _ => InputState(fields, candidate :+ c)
  }
  def endEvent() = EndState(fields :+ candidate)
}

case class EndState(
  row: Seq[String]
) extends ParseState {
  def event(c: Char) = this
  def endEvent() = this
}

case class FailureState(
  row: Seq[String],
  message: String
) extends ParseState {
  def event(c: Char) = this
  def endEvent() = sys.error("failure")
}

状態機械は、「状態×イベント→状態」の情報を記述したマトリックスとして表現できます。このマトリクスを実現するデータ構造と評価戦略は色々考えられますが、ここではOOP的なアプローチで実現しました。

状態

まず状態機械全体を表すトレイトとしてParseStateを定義します。sealedにすることで代数的データ構造の要件の1つを満たします。

ParseStateのサブクラスとして具体的な状態を定義します。情報を持つInitState, InputState, EndState, FailureStateはcase classとして、情報を持たないInitStateはcase objectとして定義しました。

イベント

発生するイベントはメソッドで表現しました。

event
1文字入力イベント発生
endEvent
パース終了イベント発生
状態×イベント→状態

状態とイベントの組から新しい状態を決定するアルゴリズムは「発生するイベント」で定義したメソッドの実装になります。

具体的には各case class, case objectのeventメソッド、endEventメソッドの実装を参照して下さい。

オブジェクト版状態機械

case classによる状態機械の表現はimmutableなので、そのままでは状態機械としては動きません。あくまでも「状態×イベント→状態」のメカニズムを提供するまでになります。

状態機械の状態遷移は状態を遷移させるので本質的にはmutableな処理です。

OOP的には、オブジェクトのインスタンス変数でmutableな状態を保持する実現方法になります。

OOP版の状態機械をParserObjectとして実装しました。

package sample

class ParserObject {
  var state: ParseState = InitState

  def charEvent(c: Char) {
    state = state.event(c)
  }

  def parseEnd(): Seq[String] = {
    val end = state.endEvent()
    state = end
    end.row
  }
}

PaserObjectクラスでは、変更可能なインスタンス変数としてstateを定義しています。

イベントの発生をメソッドで受取り、状態とイベントの組合せで新たな状態を計算し、これをstateに設定することで状態遷移を実現しています。

新しい状態の計算ロジックは、ParseStateオブジェクトにカプセル化しています。

代数的データ構造で表現した状態機械のimmutableなオブジェクトを、インスタンス変数で管理することで、OOP的な状態機械が実現できました。

使い方

プログラムを実行するためのSpecは以下になります。

package sample

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks

@RunWith(classOf[JUnitRunner])
class ParserObjectSpec extends WordSpec with Matchers with GivenWhenThen with GeneratorDrivenPropertyChecks {
  "ParserObject" should {
    "parse" in {
      val parser = new ParserObject()
      val text = "abc,def,xyz"
      for (c <- text) {
        parser.charEvent(c) // 一文字づつパーサーに送信
      }
      val r = parser.parseEnd() // 解析終了のイベントを送信
      println(s"ParserObject: $r")
      r should be (Vector("abc", "def", "xyz"))
    }
  }
}
実行

実行結果は以下になります。

$ sbt test-only sample.ParserObjectSpec
ParserObject: List(abc, def, xyz)
[info] ParserObjectSpec:
[info] ParserObject
[info] - should parse
[info] ScalaTest
[info] 36mRun completed in 180 milliseconds.0m
[info] 36mTotal number of tests run: 10m
[info] 36mSuites: completed 1, aborted 00m
[info] 36mTests: succeeded 1, failed 0, canceled 0, ignored 0, pending 00m
[info] 32mAll tests passed.0m
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 0 s, completed 2015/03/21 17:47:46

アクター版状態機械

FRP的な応用を考える場合、並行・並列処理で枠組みの中で状態機械を実現していく必要があります。

この実現方式として有力なのがアクターです。

ScalaではAkkaというアクター基盤を使用することができます。

このアクターは(理論的には色々あるでしょうが)状態を持ったactive objectを実現する方式になっています。

active objectであるアクター間はメッセージボックスでキューイングされたメッセージ通信で協調動作するので、アプリケーションレベルで特別な排他制御を行わなくても並行・並列処理を簡潔に記述することができます。

アクター版のCSVパーサーであるParserActorは以下になります。

package sample

import akka.actor._

case class ParseCharEvent(c: Char)
case object ParseEnd
case class ParseResult(result: Seq[String])

class ParserActor extends Actor {
  var state: ParseState = InitState

  def receive = {
    case ParseCharEvent(c) =>
      state = state.event(c)
    case ParseEnd =>
      val end = state.endEvent()
      state = end
      sender ! ParseResult(end.row)
  }
}

基本的に行っているのは(passive objectである)ParserObjectと同じです。オブジェクトのインスタンス変数でmutableな状態を保持する実現方法になります。

ParserObjectとの違いは、メソッド呼び出しではなくアクター間でのメッセージ通信によって処理が実行されるため、以下のようなメッセージ通信のためのコードを用意しないといけない点です。

  • アクター間で送受信されるメッセージの定義
  • メッセージを受け取り処理を行うイベントハンドラ

ParserObjectではメソッドとして簡単に定義できる処理ですが、これをメッセージ処理ように仕立て直さないといけないわけです。

アクターはとても簡単に利用できるのですが、少しボイラープレイトのコードを書く必要があります。

使い方
package sample

import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import akka.actor._
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._

@RunWith(classOf[JUnitRunner])
class ParserActorSpec extends WordSpec with Matchers with GivenWhenThen with GeneratorDrivenPropertyChecks {
  "ParseActor" should {
    "parse" in {
      implicit val timeout = Timeout(5.seconds)
      val system = ActorSystem("state")
      implicit val context = system.dispatcher
      val actor = system.actorOf(Props[ParserActor])

      val text = "abc,def,xyz"
      for (c <- text) {
        actor ! ParseCharEvent(c)
      }
      for (r <- actor ? ParseEnd) yield {
        r match {
          case ParseResult(r) =>
            println(s"ParserActorSpec: $r")
            r should be (Vector("abc", "def", "xyz"))
        }
      }
      system.shutdown()
    }
  }
}

アプリケーションロジックは以下の部分です。

val text = "abc,def,xyz"
      for (c <- text) {
        actor ! ParseCharEvent(c)
      }
      for (r <- actor ? ParseEnd) yield {
        r match {
          case ParseResult(r) =>
            println(s"ParserActorSpec: $r")
            r should be (Vector("abc", "def", "xyz"))
        }
      }

アクターを使うために以下のような準備が必要になります。

// タイムアウト値の暗黙知を定義
      implicit val timeout = Timeout(5.seconds)
      // アクターの実行基盤を作成
      val system = ActorSystem("state")
      // スレッドの実行文脈を定義
      implicit val context = system.dispatcher
      // アクターの生成
      val actor = system.actorOf(Props[ParserActor])
      ....
      // アクターの実行基盤をシャットダウン
      system.shutdown()

アクターはJava流のスレッドによる並行・並列プログラミングと比較するとはるかに楽でバグも出にくいアプローチなのですが、それでも少し込み入ったボイラープレイトが必要になります。

実行

実行結果は以下になります。

$ sbt test-only sample.ParserActorSpec
[info] Compiling 1 Scala source to /Users/asami/src/workspace2015/0317.blog.statemachine/target/scala-2.11/test-classes...
ParserActorSpec: List(abc, def, xyz)
[info] ParserActorSpec:
[info] ParseActor
[info] - should parse
[info] ScalaTest
[info] 36mRun completed in 408 milliseconds.0m
[info] 36mTotal number of tests run: 10m
[info] 36mSuites: completed 1, aborted 00m
[info] 36mTests: succeeded 1, failed 0, canceled 0, ignored 0, pending 00m
[info] 32mAll tests passed.0m
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 2 s, completed 2015/03/21 17:48:09

まとめ

今回は状態機械のベースとなるcase class群(ParseState)を作成した後、通常のOOPのアプローチとして普通のオブジェクト版(passive object)のパーサーとアクター版(active object)のパーサーを作成しました。

ParseStateはOOPのvalue objectであると同時にFPの代数的データ型でもあります。このParseStateを普通のオブジェクト、アクターの両方で普通に利用して状態機械を作成できることが確認できました。

次回は今回のParseStateをベースにMonadic Programming, Functional Reactive Programmingにおける状態機械の実現方法について考えます。

諸元

  • Scala 2.11.4
  • Scalaz 7.1.0
  • Scalaz-stream 0.6a
  • Scalatest 2.2.4

2012年3月28日水曜日

EDAとオブジェクトと関数

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。

今回はセッションで説明したモデルの改良案として、EDA(Event-Driven Architecture)の導入を行ってみます。

元のスライド

オブジェクトと関数型の繋ぎのところを具体的に考えるために2つスライドを作成しました。

「イベント→データフローの流れ」は、OOPレベルの細かい処理の流れを示しています。

「ユースケースと関数」は、OOADの観点からユースケース・モデルからOOPを経て、FPに至る流れを示しています。

OOADを軸としたアプローチが検討対象にしているので、いずれも(協調→)状態機械→アクションというオブジェクト・モデルとして素直な連携を軸に考えています。

実際に図にして考えてみると、(協調→)状態機械→アクションという流れのモデリングはあまり便利そうに見えません。協調→状態機械のモデリングは難しいですし、仮にモデルをきちんと作ったとしても、モデルと実装の乖離が大きいため、実装が進むにつれモデルが朽ちていきそうです。

理論的には正しくても、便利でないものは使われないので、もう少し実装に近いモデルで再検討してみます。

EDA

クラウド・アプリケーションのアプリケーション・アーキテクチャでは、EDA(Event Driven Arachitecture)やEIP(Enterprise Integration Patterns)といったSOA、エンタープライズ系のアーキテクチャの技術が有効ではないかと考えています。

そこで、OOAD的な(協調→)状態機械→アクションではなくて、EDA上での実現を考えてみました。

イベント→データフローの流れ

スライド「イベント→データフローの流れ」の改良案です。

OOAD的には、イベント発生が状態機械に状態遷移を発生させ、状態機械のアクションによってプログラムの処理がキックされるというモデルになりますが、「状態機械→アクション」の部分をEDAに取り替えてみたのが以下の図です。


図に登場するEDAの構成要素は以下の通りです。

イベント
システム内で発生する事象
イベントプロデューサ
イベントを生成するエージェント
イベントチャネル
イベントプロデューサとイベントコンシュマーを結び付けるチャネル
イベント処理エージェント
イベントのルーティングを行うエージェント
イベントコンシュマー
イベントを受信するエージェント

イベントプロデューサがイベントを発生させると、イベントチャネルからイベント処理エージェントを経由してイベントコンシュマーにイベントが通知されます。

イベントコンシュマーは、元のOOADの図におけるアクションに対応します。ここで、イベントに対するアプリケーション・ロジックを記述します。

イベントコンシュマーの実装では、データフローと関数型でアプリケーション・ロジックの中核部分を記述し(データフローの実装技術)、イベントコンシュマー本体でエンティティの更新を行うという役割分担(オブジェクトと関数の連携(2))を採ることになります。

EDAの構成を採ることで、イベント発生とアプリケーション・ロジックを疎結合にし、アプリケーション・ロジックの追加や拡張を容易にします。複数のユースケース・コンテキストがドメイン・モデル上に重なってくるようなアプリケーションでは、ユースケース・コンテキスト(あるいはユースケース)毎にイベント・コンシュマーを用意することで、システムのモジュール化を促進します。

また、イベントチャネルを介在させることで、MOMやESBを使用した非同期処理での実装も可能になります。

実装

論理モデルとして、この構成でモデリングしておいても、設計/実装の段階ではイベントプロデューサからイベントコンシュマーを直接同期型で呼んでしまうという実現方法を選択することも可能です。

実装時のイメージとしては、右のようなものを想定しています。基本的な処理はイベントチャネルを介さず、同期型でイベントコンシュマーにイベントを通知し、処理結果を持ち帰ります。一方、それ以外の処理はイベントチャネル経由で非同期にイベントコンシュマーにイベントを通知して処理を行います。

ユースケースと関数

スライド「ユースケースと関数」の改良案です。

OOAD的な視点では、ユースケースから関数までの流れが重要になります。

ユースケースは自然言語で記述した物語ですが、システム開発につなげるためには、もう少しシステムよりのモデルを用意してユースケースから使用するような形に持ってくる必要があります。ここでは、タスクとサービスというモデル要素を用意しました。

タスクは、ユースケース・フローにおけるシステムの使用方法を定義したものです。システムが提供するサービスの使い方(パラメタなど)を定めます。

サービスはイベントプロデューサを呼出して(サービス自身がイベントプロデューサも可)、EDAのメカニズムの上で処理を進めます。ここから先は前節で説明した流れで処理が進んでいきます。(イベント処理エージェントは宣言的な定義で記述する事が多いと思われるので図ではOOPから外していますが、OOPやFPあるいはDSLによる実現が可能です。)

DSLによるモデル記述と実装への展開

EDA上でオブジェクト・モデルと関数型を連携させていく方法についてやや実装よりに細かく考えてきました。

このあたりは、g3フレームワークSimpleModelerで試行錯誤しながら色々と検討してきた点なのですが、ここでいっているような形では状態機械にはこだわらず、まずEDAベースでモデリングとフレームワークを構築するのがよさそうというのが最近の考えです。そのようなこともあり、今回ちょっと腰を据えて考えてみました。

今回考えたモデルをベースに、SimpleModelerとg3フレームワークでどのようにDSL化(モデルDSLとフレームワークDSL)していくのかというのが次の課題です。

諸元

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

このスライド作成の様子は以下の記事になります。

回顧は以下の記事になります。

2012年3月27日火曜日

オブジェクトの世界と関数の世界

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。
「オブジェクトの世界と関数の世界」として用意した以下のスライドを説明します。


このスライドは元々、以下の2つの情報を表現するために用意していたのですが、いろいろ書き込んでいくうちに、OFADでのモデル変換の流れの側面が大きくなってしまいました。
  • オブジェクトの世界と関数の世界は併存するのが必然
  • アプリケーション・モデルの実装側はできるだけ関数型にしていく
このため、同じような情報を持つスライドをセッションでは2つ省略しましたが、逆に、このスライドでは、「OFADでのモデル変換の流れ」の説明になってしまい、肝心なことの説明ができなくなってしまったので、この記事で補足します。

オブジェクトの世界と関数の世界は併存するのが必然

オブジェクト指向技術は、元々はシミュレーション技術に根っこがあり、その最も重要な軸は、(経験則的な)人間の認知モデル、メンタルモデルをそのままモデルとして使用している点です。
このため、要求モデルを記述する際に、普通の人が把握できる範囲のモデルにおさまることが期待できます。さらにオブジェクト指向言語を使うことで、要求仕様から実装まで同じセマンティクス、インピーダンス・ミスマッチなしで一気通貫に記述できるというのがOOADの最大のアピールポイントです。(OOの重要なメリットである情報隠蔽による安全性やポリモーフィズムによる拡張性は、その次ぐらいに重要な項目といえます。)
たとえば、DDDのUbiquitous Languageで、用語集からドメイン・モデル、さらにOOPでの実装まで一気通貫に共通化できるのはこのような理論的な背景があるからですね。
一方、振舞いの記述、アルゴリズムの記述は、情報科学や数学のバックグラウンドを持つ関数型に一日の長があります。(関数型はモデルの種類の名称としては一般的ではないので、以下では数理モデルという用語を使うことにします。)
とはいえ、数理モデルは、以下の問題があるので汎用的な意味で要求モデルを記述するモデルに採用するのは困難です。
  • 問題を数理モデルで記述できるとは限らない
  • 数理モデルは情報科学や数学のスキルがないと作成できない
  • 数理モデルは情報科学や数学の素養がないと内容を理解できない
要求モデルとして数理モデルで記述できる範囲のモデル、顧客が数理モデルの素養がある、という条件がうまくハマった場合には、一気通貫に要求モデルから実装コードの生成まで持っていけるので、極めて強力です。
このため、大枠はオブジェクト・モデルで考え、サブシステムまたはモジュール単位で数理モデルで要求モデルを併用するようなアプローチが有効でしょう。
いずれにしても、数理モデル一本ということは現実的には不可能なので、オブジェクト・モデルは必須です。全体の大枠は清濁併せ呑むオブジェクト・モデル、ぴったりとハマったところは数理モデル、ハマらなかったところはオブジェクト・モデルで記述して併用・併存するのが現実解です。

アプリケーション・モデルの実装側はできるだけ関数型にしていく

要求モデルでは、大枠はオブジェクト・モデルが必須で、一部条件を満たしたケースで関数型/数理モデルを併用することになります。エンタープライズシステムの場合、現実的には当面関数型/数理モデルを使うことはほとんどないでしょう。つまり、事実上要求モデルはオブジェクト・モデルということになります。
しかし、オブジェクト・モデルを実装する過程の中で、関数型を使えるポイントが色々とあります。一つは関数型言語を使った関数型的なプログラミングですし、より抽象的なモデルとしてはデーターフローが有力です。
図中のオブジェクトの世界では、現実世界→ドメイン・モデルのラインとやりたい事の物語→ユースケース・モデル→協調→状態遷移→アクションのラインがありますが、前者のドメイン・モデル・ラインはオブジェクト・モデリングがよいでしょう。
一方、後者のユースケース・モデル・ラインは、オブジェクト指向的によい実装方法がないので、可能なかぎり関数型にしていくのが望ましいところです。たとえばデーターフローで記述できるところを見つけて、これをデータフローモデル→関数型またはDSLで記述していくというアプローチですね。この部分をいかに関数型の方向に倒していくのか、というのが今後のプログラミング技術やモデリング技術の方向性を見ていく上で重要な視点ではないかと思います。
この場合、関数型で記述したアルゴリズムが扱う事実の情報、いわゆるファクトモデルとして、オブジェクトのドメイン・モデルを使うのがよい組み合わせです。

省略したスライド

内容が重複するので省略したのは以下の2つのスライドです。

上側は「 オブジェクトと関数の連携(3)」として用意していた図です。オブジェクトと関数の接点として、当面はデータフローが現実解かな、という内容です。


下側は「Domain-Driven Design (DDD)」で説明したとおり、OOADの中でのDDDの位置付けを説明する内容です。

OOPは不要?

昨日たまたま某所で教えてもらったのですが、以下のような流れもあるみたいです。(注意:一年前の記事です)
情報科学やアルゴリズムの勉強にOOPは不要でFPのみでよい、というのは一理あります。
現状ではエンタープライズ向けにはOOPが支配的な言語パラダイムである(COBOLがあるので"新規開発では"という注釈つきかもしれません)、システム、サブシステム粒度のモデルの記述方式に何を用いるのか、といった点は置いておくとしても、情報システム構築の要求モデルにオブジェクト・モデルが必須である以上、長期的に見てもエンジニアのスキルとしては引き続きOOPは必要と思います。
とはいえ、OOPが圧倒的、絶対的という訳ではなく、条件付きで有効というように相対化していく流れを象徴する出来事であるのは確かです。

諸元

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)
このスライド作成の様子は以下の記事になります。
回顧は以下の記事になります。

2012年3月7日水曜日

オブジェクトと関数の連携(2)

要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。

今回は背景説明第6弾として、「オブジェクトと関数の連携(2)」として用意した以下の図を説明します。

オブジェクトと関数の連携(1)では、オブジェクトの世界と関数の世界を完全に分けてしまうのがよい、と説明しました。今回は、その具体的な連携方法について考えます。

Scalaでは関数もオブジェクトの一種なので、そういう意味ではすべてオブジェクトの世界で動いています。ここでは、純粋関数型言語の枠組みのみを使用しているものを関数型の世界、それ以外のものをオブジェクトの世界と呼ぶことにします。

図では、オブジェクトの世界を左側に関数の世界を右側に配置しました。

更新処理の流れ

オブジェクトの世界では、オブジェクトがグラフ構造で格納されています。このグラフ構造の一部を、関数を使って更新する流れについて見ていきましょう。

純粋関数型では副作用がありません。つまり、オブジェクトの内容や構造を更新することはできません。関数ができることは、計算をすることのみです。

とはいえ、純粋関数型言語でもインタラクションゲームデータベースアクセスを書くことができます。

副作用はないにもかかわらず、プログラムの外の世界の副作用は記述できるわけですね。この矛盾を解決するためのちょっとしたトリックがあります。このトリックがオブジェクトと関数を連携させる場合でも鍵となります。

基本データ構造

一般的な処理で関数が扱う構造は大きく以下の4種類です。

  • 値(構造は持たない)
  • リスト

この中で最も複雑な構造が木構造です。木構造までは再帰によって関数で自然に記述できます。これより複雑な構造はグラフ構造になりますが、これは特別な扱いを必要とするので、一般論を考える時は除外して考えるとよいと思います。(グラフ構造を扱うときは、木構造に参照を追加したり、表(行列)の形にしたり(ノードの集合として扱う)という操作の仕方になります。)

関数で自然に扱える最も複雑な構造が木構造で、表、リスト、(構造を持たない)値は木構造を単純化した構造と考えることができます。以上の点から、以下では木構造をベースに考えていきます。木構造でできることは表、リスト、(構造を持たない)値でも同様に適用できます。

データ構造の抽出

可変オブジェクトのグラフ構造から、処理に必要な情報を木構造として抽出します。この情報を、代数的データ型と永続データ構造を用いて表現します。どちらも不変オブジェクトで、変更を行うことはできません。

図ではグラフ構造としての情報は、木構造に参照を追加する形で表現しています。

データ構造のコンバージョン

関数での計算の基本は木構造のコンバージョンです。データ構造を更新することはできないので、複製しながら複製過程でその一部を変えることで、木構造のコンバージョンを行います。

更新指示書

関数では計算ができるだけなので、直接オブジェクト側のデータ構造の更新はできません。

単純なケースでは、関数で計算したデータ構造を直接オブジェクト側のデータ構造に上書きしてしまうことで、更新処理を完結させることができます。

問題は、そのようなことはできない複雑なデータ構造の場合ですね。

その場合、関数は木構造から「更新指示書」を計算します。ここが「トリック」です。

関数はあくまでも更新指示書の計算を行うのみで、更新処理そのものにはタッチしません。副作用という下世話な処理は、関数型言語の外側の誰かが更新指示書を見ながらやってくれる、という役割分担になっています。この役割分担によって、純粋関数型の純粋性が保たれているわけです。

計算結果の反映

オブジェクト側では、関数から返ってくる更新指示書に従って、可変オブジェクトのグラフ構造を更新します。

副作用はオブジェクト側で引き受けることで、関数の世界の純粋性を守りながら、オブジェクトと関数の連携を行うことができます。

ノート

更新指示書を使った更新処理はかなりややこしいです。

オブジェクト指向から関数型に入ってくるときに戸惑うのはこの点で、この(擬似)更新処理を円滑に行うためのテクニックが数多く存在します。モナドもその一つですね。先ほど紹介したゲームは名前がMonadiusですが、これはモナドを使っているのが名前の由来とのことです。

こういった、ややこしいメカニズムで関数型の純粋性を保つと何がよいのかというと、数学の理論の範囲内で処理を完結させる事ができるということです。これは、理論的にはプログラムが証明可能ということです。

もちろん現時点では、普通の関数型言語ではプログラムを証明することはできないのですが、関数型の枠組みでプログラミングすることによって間接的にバグの混入を大幅に防ぐことができます。

たとえば、並行処理を行う小さなモナドをたくさん用意して、このモナドを合成して一つの大きなサービスを記述するとします。各モナドは代数的構造デザインパターンで取り上げた代数的構造を持つ代数データ型を操作するとします。

このようなモナドの合成では、代数的に証明されたメカニズムでモナドを合成することができ、さらに代数データ型の枠組みの中で処理の最適化を行うことも可能になるはずです。

「小さなモナド」側で十分に検証してバグを取っておけば、これらのモナドを合成して作成したサービスは、問題なく動作することが期待できます。(モナドの合成メカニズムそのものは数学的に証明されたメカニズムが使用されるはずなので。)

並行プログラミングでは、このようなプログラムの作り方が非常に有益です。並行プログラミングでは、単純なケースを除いてはデバッガによるデバッグはほぼ不可能と考えてよいので、プログラミングの時点でバグが混入しづらいメカニズムの存在が大きな鍵になります。

このあたりの考えを進めていくと証明駆動や形式手法といった方面に進んでいくことになりますが、その土台は関数型プログラミングなので、まずは関数型プログラミングに習熟することが大事です。

SQL

「更新指示書」によるメカニズムはSQLを用いたデータベースプログラミングと同じことをしていると気づくと、処理の内容が腑に落ちると思います。(SQLでは木構造ではなく表構造になります。)

「更新指示書」としてSQL文を作る関数を書けば、オブジェクトの世界を飛ばして、状態はすべてデータベースに格納管理するというアプローチも可能です。こうすることで、データベースをバックエンドに使う一般的なWebアプリケーションを純粋関数型言語で記述できますね。

トランザクション

「更新指示書」によるメカニズムはトランザクション処理とも相性がよいです。

トランザクション処理では、処理がアボートした時には、データが処理前の状態になっていなければなりません。このため、データにロックをかけて随時情報を更新していくというアプローチでは、アボートした時にデータを戻す処理を行わなければなりません。

このような手間が必要なので、「更新指示書」方式のアプローチでも処理の複雑さは変わりません。関数型では言語の特性上「更新指示書」アプローチになるので、この上でトランザクション処理を考えていけばよいわけです。また、(ロックをかけない)楽観的アプローチは、「更新指示書」アプローチと相性がよいので、そういう意味でも「更新指示書」アプローチは有効です。

つまるところ、関数型言語はトランザクション処理と相性がよいということですね。

2012年2月27日月曜日

Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標

3月19日に要求開発アライアンスの3月定例会で『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というタイトルでお話させていただくことになりました。

今回の定例会のテーマは『要求開発×クラウド』で丸山先生の『エンタープライズ・クラウドの現在』との二本立てとなっています。

内容は以下のようなものになる予定です。個人的にはクラウド、FPの文脈を取り込んだOFADの大枠みたいなものが朧気ながらみえてきた気がするので、各種の論点についてOOADの専門家の皆さんのご意見をお聞きできればという感じで考えています。

  • OOADの問題点
  • 最新FP with Scala
    • trait, monad, type class
  • クラウドプラットフォーム
  • モデリング&アーキテクチャ
    • DDD, DCI, CQRS, EDA
  • その他の技術動向
    • DSL, コード中心, Agile/Lean, SA/SD
  • OFP = OOP+FP
  • OFAD
  • OFAD with Scala

セッション時間は50分ですが、OFADの要素技術、関連技術が多岐に渡るため個別の技術についてはOFADの文脈における枠組みを示すにとどまることになります。

今までは、こういった場合、スライドを作りすぎて駆け足になってしまったり、作ったスライドを捨てたりすることが多く、なかなか情報を的確に伝えきれないというのが反省点になっていました。駆け足で説明をはしょったり、削った項目についてはその場ではブログでまとめておくことを考えたりするのですが、セッションが終わってしまうと集中力が切れるためか、結局そのまま放置ということになってしまっています。

そこで、今回は個別の技術については、準備ができたものについては事前にブログ上に説明を上げておくことにしました。セッション(スライドはセッション後に公開予定です)と合わせてより包括的に情報を伝達することができればと思います。