実装(2)
2024年1⽉19⽇
浅海智晴
クラウドアプリケーションのための
オブジェクト指向分析設計講座
第31回
作業分野
SimpleModeling2021
• オブジェクト指向分析設計での共通範囲
• UML/UP
• 本講座で使⽤するUMLプロファイル
• プロファイル:SimpleModeling2021 (SM2021)
• オブジェクト指向分析設計の基本からの拡張部を明確化
• アジャイル開発
• Communication
• Embrace Change
• Travel Light
• Scaling
• Component-Based Development
• クラウド・アプリケーション
• モデル駆動開発
SM2021
Travel Light
Embrace Change
Cloud
Model-Driven
Scaling
CBD
Testability
Serviceability
• ⾮機能要件
• Testability
• Serviceability
第1部 基本編の構成(1)
• 概論 [第1回]
• 開発プロセス [第2回]
• 基本モデル [第3回]
• 静的モデル(1) [第4回]
• 静的モデル(2) [第5回]
• 動的モデル [第6回]
• 協調モデル [第7回]
• 関数モデル [第8回]
• 物理モデル [第9回]
• 作業分野 [第10回]
• ビジネス・モデリング [第11回]
• 要求 [第12回]
• 要求/ユースケース [第13回]
• 要求/シナリオ [第14回]
• 分析 [第15回]
• 分析/コンポーネント分析 [第16回]
• 分析/イベント駆動 [第17回]
• 作業分野
• 設計 [第18回]
• 設計/アーキテクチャ設計 [第19回]
• 設計/コンポーネント設計(1) [第20回]
• 設計/コンポーネント設計(2) [第21回]
• 設計/コンポーネント設計(3) [第22回]
• 設計/ドメイン設計(1) [第23回]
• 設計/ドメイン設計(2) [第24回]
• 設計/ドメイン設計(3) [第25回]
• 設計/ドメイン設計(4) [第26回]
• 設計/ドメイン設計(5) [第27回]
• 設計/原理 [第28回]
• 設計/ UX/UI設計 [第29回]
• 実装(1) [第30回]
• 実装(2) [第31回]
• 実装(3) [第32回]
• テスト [第33回]
第1部 基本編の構成(2)
• アプリケーション・アーキテクチャ [第34回]
• ドメイン・モデル [第35回]
• アプリケーション・モデル [第36回]
• プレゼンテーション・モデル [第37回]
• ケーススタディ[第38回]
• 要求モデル [第39回]
• 分析モデル [第40回]
• 設計モデル [第41回]
• 実装 [第42回]
• テスト [第43回]
本講座のアプローチ
• オブジェクト指向分析設計の基本を確認
• UML + UP(Unified Process)
• CBD (Component-Based Development)
• 最新技術でアップデート
• クラウド・コンピューティング
• イベント駆動、分散・並列
• ビッグデータ、AI、IoT
• コンテナ
• 関数型
• OFP(Object-Functional Programming), Reactive Streams
• ルール, AI
• DevOps
• アジャイル開発
• DX (Digital Transformation)
第25回 アプリケーション・アーキテクチャ
第2回 開発プロセス
第9回 物理モデル
第11回 ビジネス・モデリング
第2部 クラウド・アプリケーション編
第21回 設計/ドメイン設計
第20回 設計/コンポーネント設計
第2部 クラウド・アプリケーション編
原理 (Principle)
• Agile Software Development [ASD]
• SRP (The Single Responsibility Principle)
• OCP (The Open-Close Principle)
• LSP (The Liskov Substitution Principle)
• …
• GRASP (General Responsibility Assignment Software Patterns or principles)
• Low Coupling
• High Cohesion
• …
• Writing Effective Use Cases [WEUC]
• Scope
• …
パターン (Pattern)
• Design Patterns [DP]
• Observer, Strategy, …
• Domain Driven Design [DDD]
• Ubiquitous Language, Intention-
Revealing Interfaces, …
• Analysis Patterns [AP]
• Party, Quantity, …
• Pattern-Oriented Software
Architecture [POSA]
• Layers, Pipes and Filters, …
• Patterns of Enterprise
Application Architecture [PEAA]
• Unit of Work, Data Transfer Object,
…
• Enterprise Integration Patterns
[EIP]
• Message Bus, Aggregator, …
• Patterns for Effective Use
Cases [PEUC]
• CompleteSingleGoal,
VerbPhraseName, …
• AntiPatterns [AnP]
• Stovepipe System, Analysis
Paralysis, …
内容
• 設計の成果物
• 設計
• 基本構造
• メソッド実装
• 実装のポイント
• TDD (Test-Driven Development)
設計の成果物
設計の成果物
• 設計
• 基本構造
• メソッド実装
設計
アーキテクチャ・モデル(設計)
再掲 第21回 設計/コンポーネント 設計(2)
コンポーネント設計
再掲 第21回 設計/コンポーネント 設計(2)
再掲 第22回 設計/コンポーネント設計(3)
再掲 第22回 設計/コンポーネント設計(3)
基本構造
再掲 第22回 設計/コンポーネント設計(3)
基本構造
• ドメイン・コンポーネントを構成するオブジェクトとして、以
下のオブジェクトを導⼊
• サービス (IReservationService, ReservationService)
• コンフィグ (ReservationConfig)
• ルール (IReservationRule, ReservationRule)
• ロジック (IReservationLogic, ReservationLogic)
• リポジトリ(IReservationRepository,
ReservationRepository)
• ファクトリ (IReservationFactory, ReservationFactory)
• レセプタ (IReservationReceptor, ReservationReceptor)
再掲 第22回 設計/コンポーネント設計(3)
再掲 第22回 設計/コンポーネント設計(3)
再掲 第22回 設計/コンポーネント設計(3)
ドメイン・サービス
• ドメイン・サービス
• ドメイン・モデル内でのサービス
• Services [DDD]
• Service Layer [PEAA]
• 本モデルではコンポーネント(ReservationDomain)の本体とな
るオブジェクトとして実現
• DI(Dependency Injection)によって構成要素のオブジェクトを注⼊
再掲 第22回 設計/コンポーネント設計(3)
…
class ReservationService @Inject()(
val config: ReservationConfig,
val rule: IReservationRule,
val logic: IReservationLogic,
val factory: IReservationFactory,
val repository: IReservationRepository,
val receptor: IReservationReceptor,
) extends IReservationService, IReservationManagement, IReservationAdmin,
IReservationMetrix, EventListener {
…
ReservationService.scala
再掲 第22回 設計/コンポーネント設計(3)
class ReservationServiceModule(config: Config) extends AbstractModule {
override def configure(): Unit = {
val c = ReservationConfig.create(config)
bind(classOf[IReservationRule]).toInstance(ReservationRule.create(c))
bind(classOf[IReservationLogic]).toInstance(ReservationLogic.create(c))
bind(classOf[IReservationFactory]).toInstance(ReservationFactory.create(c))
bind(classOf[IReservationRepository]).toInstance(ReservationRepository.create(c))
bind(classOf[IReservationReceptor]).toInstance(ReservationReceptor.create(c))
}
}
ReservationServiceModule.scala
再掲 第22回 設計/コンポーネント設計(3)
コンフィグ
• ドメイン・コンポーネントの変化点、拡張点、構成を外部パラ
メタで指定可能にする
• ドメイン・コンポーネントに限らないコンポーネントに必要な機能
• 変化点
• 各種デフォルト値など
• 拡張点
• ドライバの設定など
再掲 第22回 設計/コンポーネント設計(3)
import com.typesafe.config.Config
import io.circe.parser._
import io.circe.generic.auto._
case class ReservationConfig(
cacheSize: Int = 10000
) {
import ReservationConfig.*
}
object ReservationConfig {
val PREFIX = "org.simplemodeling.domain.reservation"
val default = ReservationConfig()
def create(config: Config): ReservationConfig = {
val c = config.getConfig(PREFIX)
val json = c.root().render()
parse(json)
}
def parse(json: String): ReservationConfig = {
decode[ReservationConfig](json)
}
def createForTest(): ReservationConfig = default
}
ReservationConfig.scala
改良 第22回 設計/コンポーネント設計(3)
Circeを⽤いてJSONのパースを⾃動化
ドメイン・ルール
• 各種のルールを定義
• 宣⾔的に記述できる責務
• ルールとしてモデル化するのが適当なもの
• 例:料⾦表
• 関数
• Side-Effect-Free Functions [DDD]
• コンポーネントはルールを実⾏するインタープリタとして動作
する
• ルールが本格化してくると、このような⾒⽴てもできる
• 例:ビジネス・プロセス・ルール
再掲 第22回 設計/コンポーネント設計(3)
package org.simplemodeling.domain.reservation
import scala.util.Try
import org.simplemodeling.componentframework.datatype.Money
trait IReservationRule {
def calcReservationFee(reservation: Reservation): Try[Money]
}
IReservationRule.scala
追加 第22回 設計/コンポーネント設計(3)
package org.simplemodeling.domain.reservation
class ReservationRule() extends IReservationRule {
def calcReservationFee(reservation: Reservation): Try[Money] = ???
}
object ReservationRule {
def create(config: ReservationConfig): ReservationRule = new ReservationRule()
}
ReservationRule.scala
ドメイン・ロジック
• ドメイン内のロジックを記述
• ドメイン・ルールを活⽤しながら、参照処理に加えて更新処理
を⾏う
• ドメイン・サービスで実現してもよいが、分離した⽅がより拡
張性、柔軟性が⾼まる
SM2021
再掲 第22回 設計/コンポーネント設計(3)
package org.simplemodeling.domain.reservation
import scala.util.Try
import org.simplemodeling.componentframework.ExecutionContext
trait IReservationLogic {
def reserve(cmd: ReserveCommand)(using ctx: ExecutionContext): Try[ReserveResult]
def unreserve(cmd: UnreserveCommand)(using ctx: ExecutionContext): Try[ReserveResult]
def getReservation(id: ReservationId)(using ctx: ExecutionContext):
Try[Option[Reservation]]
def queryReservation(q: ReservationQuery)(using ctx: ExecutionContext):
Try[Vector[Reservation]]
}
IReservationLogic
再掲 第22回 設計/コンポーネント設計(3)
ドメイン・ファクトリ
• ドメイン・コンポーネントで使⽤するドメイン・オブジェクト
の作成
• Factories [DDD]
• ドメイン・オブジェクトのコンストラクタを直接呼び出す⽅式
⽐べて以下の優位性がある
• 将来の拡張性を担保
• デフォルト値などをドメイン・コンポーネントの変化点とし設定可能
になる
再掲 第22回 設計/コンポーネント設計(3)
package org.simplemodeling.domain.reservation
import scala.util.Try
trait IReservationFactory {
def createReservation(p: Map[String, Any]): Try[Reservation]
}
IReservationFactory.scala
再掲 第22回 設計/コンポーネント設計(3)
リポジトリ
• アプリケーションのメタデータを管理するデータストアという
イメージもあるが…
• Repository [PEAA]
• データストアとドメイン・オブジェクトをマッピング
• Repositories [DDD]
• Entities [DDD]
• Value Objects [DDD]
• Aggregates [DDD]
再掲 第22回 設計/コンポーネント設計(3)
trait IReservationRepository {
import IReservationRepository._
def get(id: ReservationId): Try[Option[Reservation]]
def list(query: Query, directive: ListDirective): Try[ListResult[Reservation]]
def create(entity: datastore.create.Reservation): Try[Unit]
def create(record: Map[String, Any]): Try[Unit]
def update(directive: datastore.update.Reservation): Try[Unit]
def update(id: ReservationId, record: Map[String, Any]): Try[Unit]
def delete(id: ReservationId): Try[Unit]
}
IReservationRepository.scala (1/2)
再掲 第22回 設計/コンポーネント設計(3)
object IReservationRepository {
case class Reservation(
id: ReservationId,
resource: Resource,
user: User,
interval: Interval[LocalDateTime]
)
case class Resource(
id: ResourceId,
name: String
)
case class Query(
resourceId: Option[ResourceId],
userId: Option[UserId],
interval: Option[Interval[LocalDateTime]]
)
IReservationRepository.scala (2/2)
object datastore {
case class Reservation(
resourceId: ResourceId,
userId: UserId,
interval: Interval[LocalDateTime]
)
object create {
case class Reservation(
resourceId: ResourceId,
userId: UserId,
interval: Interval[LocalDateTime]
)
}
object update {
case class Reservation(
resourceId: Option[ResourceId],
userId: Option[UserId],
interval: Option[Interval[LocalDateTime]]
)
}
}
}
再掲 第22回 設計/コンポーネント設計(3)
ドメイン・レセプタ
• イベントの受信と処理担当オブジェクトへのルーティングを⾏
う
• ⽣イベント、ビジネス・イベントとドメイン・イベントのマッ
ピングなどを⾏う
• 実際の処理はドメイン・ロジックに移譲する
• ドメイン・ルールを使⽤してチューニング可能にしておくとよ
い
SM2021
再掲 第22回 設計/コンポーネント設計(3)
package org.simplemodeling.domain.reservation
import org.simplemodeling.componentframework.event.Event
trait IReservationReceptor {
def receive(evt: Event): Unit
}
IReservationReceptor.scala
再掲 第22回 設計/コンポーネント設計(3)
package org.simplemodeling.domain.reservation
import org.simplemodeling.componentframework.event.Event
class ReservationReceptor(config: ReservationConfig) extends IReservationReceptor {
def receive(evt: Event): Unit = Option(evt) collect {
case m: ReservedEvent => reserved(m)
case m: UnreservedEvent => unreserved(m)
}
def reserved(evt: ReservedEvent): Unit = ???
def unreserved(evt: UnreservedEvent): Unit = ???
}
object ReservationReceptor {
def create(config: ReservationConfig): ReservationReceptor = new
ReservationReceptor(config)
}
ReservationReceptor.scala
再掲 第22回 設計/コンポーネント設計(3)
メソッド実装
ユニバース (Universe)
• 操作(Operation)に渡す環境情報
• 操作に必要なエンティティの必要な部分の断⽚を切り出したもの
• 関連パターン
• Aggregate [DDD]
• Whole-Part [POSA]
• Aggregation/Composition [UML]
• ワーキング・セット (Working Set)
• OS(仮想記憶)の概念
• プロセスの動作に必要な情報を選択してメモリに載せる
• ユースケース/タスクを実⾏するために、メモリ上に載っていなけれ
ばならないエンティティ由来のオブジェクトの総体
SM2021
再掲 第26回 設計/ドメイン設計(4)
case class ReservationSpace(
reservations: Map[RoomId, ReservationSpace.ReservationSequence] = Map.empty
) {
def isAvailable(roomid: RoomId, interval: Interval[LocalDateTime]): Boolean = ???
}
object ReservationSpace {
val USER_NAME_LENGTH = 32
val ROOM_NAME_LENGTH = 64
case class UserName(name: String) {
assert (name.length < USER_NAME_LENGTH)
}
case class RoomName(name: String) {
assert (name.length < ROOM_NAME_LENGTH)
}
case class Reservation(
id: ReservationId,
interval: Interval[LocalDateTime],
user: User,
room: Room
)
ReservationSpace.scala
ユニバースの実装としてReservationSpaceを定義
再掲 第27回 設計/ドメイン設計(5)
case class User(
id: UserId,
name: UserName
)
case class Room(
id: RoomId,
name: RoomName
)
case class ReservationSequence(
reservations: List[Reservation]
)
def build(
reservations: List[entity.Reservation],
users: List[entity.User],
rooms: List[entity.Room]
): Try[ReservationSpace] = ???
}
ReservationSpace.scala
処理に必要なデータ構造をバリューとして定義する
再掲 第27回 設計/ドメイン設計(5)
UnitOfWork
• UnitOfWork (PEAA)
• アプリケーション側でトランザクション的なリソース管理を⾏
う
• DBトランザクション
• ファイル作成・更新・削除
• イベント送信
• メッセージ送信
• 「状態遷移指⽰」をUnitOfWorkを⽤いて⾏う
再掲 第27回 設計/ドメイン設計(5)
class UnitOfWork(
context: ExecutionContext
) {
import UnitOfWork.*
def create[T](store: EntityStore, data: Record)(using instance: EntityInstance[T]): Try[CreateResult[T]]
= ???
def get[T](store: EntityStore)(using instance: EntityInstance[T]): Try[GetResult[T]] = ???
def list[T](store: EntityStore, directive: ListDirective)(using instance: EntityInstance[T]):
Try[ListResult[T]] = ???
def update[T](store: EntityStore, id: EntityId, data: Record)(using instance: EntityInstance[T]):
Try[UpdateResult[T]] = ???
def delete[T](store: EntityStore, data: Record)(using instance: EntityInstance[T]): Try[DeleteResult[T]]
= ???
def createFile(file: File, data: String): Try[Unit] = ???
def sendMessage(msg: Message): Try[Unit] = ???
def commit(): Try[CommitResult] = ???
def abort(): Try[AbortResult] = ???
}
UnitOfWork.scala
UnitOfWorkの機能を提供するオブジェクト。
Execution Contextの情報を活⽤しながら動作する。
再掲 第27回 設計/ドメイン設計(5)
object UnitOfWork {
type CommitResult = Unit
type AbortResult = Unit
sealed trait UnitOfWorkOp[T]
case class CreateEntity[ENTITY](store: EntityStore, data: Record, instance:
EntityInstance[ENTITY]) extends UnitOfWorkOp[Try[CreateResult[ENTITY]]]
type UnitOfWorkFM[T] = Free[UnitOfWorkOp, T]
type UnitOfWorkEitherFM[T] = EitherT[UnitOfWorkFM, Throwable, T]
type Config = ExecutionContext
type Log = List[String]
type State = Map[String, Any]
type UnitOfWorkRWSEitherFM[T] = RWST[UnitOfWorkEitherFM, Config, Log, State, T]
def create[A](store: EntityStore, data: Record)(using instance:
EntityInstance[A]): UnitOfWorkFM[Try[CreateResult[A]]] =
Free.liftF(CreateEntity(store, data, instance))
UnitOfWork.scala
UnitOfWorkのコンパニオン・オブジェクト。
FreeモナドベースのUnitOfWorkのDSL機能を提供する。
再掲 第27回 設計/ドメイン設計(5)
OOP (Object-Oriented Programming)
• ⼀般的なオブジェクト指向プログラミングの例
• メソッドの実装は⼿続き型
再掲 第27回 設計/ドメイン設計(5)
class ReservationLogic() extends IReservationLogic {
def reserve(cmd: ReserveCommand)(using ctx: ExecutionContext): Try[ReserveResult]
= Try {
val uow = ctx.unitOfWork
val roomid = RoomId(cmd.resourceId)
val space = build_resrvation_space(roomid, cmd.interval)
if (space.isAvailable(roomid, cmd.interval))
make_reservation(uow, roomid, cmd.interval, cmd.userId)
else
throw new IllegalStateException("???")
}
protected def build_resrvation_space(
roomid: RoomId,
interval: Interval[LocalDateTime]
): ReservationSpace = {
val reservations = fetch_reservations(roomid, interval)
val users = fetch_users(reservations)
val rooms = fetch_rooms(reservations)
ReservationSpace.build(reservations, users, rooms).get
}
ResrevationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
protected def make_reservation(
uow: UnitOfWork,
roomid: RoomId,
interval: Interval[LocalDateTime],
userid: UserId
): ReserveResult = {
val data = Map(
"room_id" -> roomid.id,
"interval" -> interval,
"user_id" -> userid.id
)
uow.create(ReservationStore, data) match {
case Success(s) => ReserveResult()
case Failure(e) => throw e
}
}
ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
ReaderWriterStateT&EitherT&Free
• EitherT&Free
• プログラム内プログラミング⾔語をエラー状態付きで実現
• 本例ではUnitOfWorkをエラー状態付きでDSL化
• ReaderWriterStateモナド
• Readerモナド、Writerモナド、Stateモナドを合成したモナド
• ⽂脈情報伝播+情報追記+状態遷移
• ReaderWriterStateTモナド・トランスフォーマ
• ReaderWriterStateモナドと任意のモナドを合成
• ReaderWriterStateT&EitherT&Free
• プログラム内プログラミング⾔語をエラー状態付きで実現
• ⽂脈情報伝播+情報追記+状態遷移
再掲 第27回 設計/ドメイン設計(5)
ReaderWriterStateT&EitherT&Freeモナドは例外状
態、DSL、⽂脈情報伝達、情報追記、状態遷移の5つ
の機能を持つマイクロフレームワークとなっている
再掲 第27回 設計/ドメイン設計(5)
def reserve(cmd: ReserveCommand)(using ctx: ExecutionContext): Try[ReserveResult]
= {
val interpreter: UnitOfWorkOp ~> Id = ctx.unitOfWorkInterpreter
reserveFM(cmd).value.foldMap(interpreter).toTry
}
def reserveFM(cmd: ReserveCommand)(using ctx: ExecutionContext):
UnitOfWorkEitherFM[ReserveResult] = {
reserveRWS(cmd).run(ctx, Map.empty).map(_._3)
}
def reserveRWS(cmd: ReserveCommand): UnitOfWorkRWSEitherFM[ReserveResult] = {
val roomid = RoomId(cmd.resourceId)
for {
space <- build_reservation_space(RoomId(cmd.resourceId), cmd.interval)
_ <- check_reservation(space, roomid, cmd.interval)
r <- make_reservation(roomid, cmd.interval, cmd.userId)
} yield r
}
ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
protected def build_reservation_space(
roomid: RoomId,
interval: Interval[LocalDateTime]
): UnitOfWorkRWSEitherFM[ReservationSpace] =
RWST { (ctx, state) =>
for {
reservations <- fetch_reservations(roomid, interval)
users <- fetch_users(reservations)
rooms <- fetch_rooms(reservations)
r <- EitherT(Free.pure(ReservationSpace.build(reservations, users,
rooms).toEither))
} yield (Nil, state, r)
}
ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
protected def check_reservation(space: ReservationSpace, roomid: RoomId, interval:
Interval[LocalDateTime]): UnitOfWorkRWSEitherFM[Unit] =
RWST { (ctx, state) =>
EitherT {
Free.pure {
if (space.isAvailable(roomid, interval))
Right((Nil, state, ()))
else
Left(new IllegalArgumentException("Already reserved"))
}
}
}
ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
protected def make_reservation(
roomid: RoomId,
interval: Interval[LocalDateTime],
userid: UserId
): UnitOfWorkRWSEitherFM[ReserveResult] = {
val data = Map(
"room_id" -> roomid.id,
"interval" -> interval,
"user_id" -> userid.id
)
RWST { (config, state) =>
EitherT {
UnitOfWork.create(ReservationStore, data).map(_.map(_ => (Nil, state,
ReserveResult())).toEither)
}
}
}
ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
コンポーネント・フレームワークと
Freeモナド
• FreeモナドによるDSLによってコンポーネント・フレーム
ワークの機能を隠蔽して使いやすくする
• 関数型を採⽤するメリット
• 使⽤するモナドの組み合わせによってインタフェースのシグネ
チャが異なってくる
• 設計に影響
• ReaderWriterStateT&EitherT&Freeの組み合わせが⼀つ
の形
• ReaderWriterStateTは学習コストと利便性のトレードオフで採⽤
を考える
• Freeモナドの代わりにIOモナドを使⽤するケースも考えられる
再掲 第27回 設計/ドメイン設計(5)
実装のポイント
実装のポイント
• CBD (Component-Based Development)
• 型
• 制約
• リファクタリング
• DI (Dependency Injection)
• 変化点
• 拡張点
CBD (Component-Based Development)
• コンポーネントを開発の単位として、インタフェースで定義さ
れてメソッドを実現していく
コンポーネント (Component)
• 再利⽤可能な部品
• 明確な外部インタフェースを定め実装を隠蔽
• 新規開発を抑えることで開発期間短縮、品質向上を図る
• 再利⽤性の⾼い部品化は⾃然にできることではないので、コストを掛けて積極的に狙い
に⾏く
• 論理モデルと物理モデルの両⽅の性質を兼ね備える
• 配備の単位
• 物理的な側⾯ ⇒ モジュール
• オブジェクト指向分析設計による開発の様々な側⾯を集約してパッケージ化
• モデリングとプログラミングの結節点
• モデリングはコンポーネントまで。コンポーネントの実現はプログラミングで。
• 再利⽤可能な部品にするための仕掛けが必要
• コンポーネント・フレームワーク
• 実⾏コンテキスト
Travel Light
Travel Light
再掲 第5回 静的モデル(2)
オブジェクト指向分析設計の様々の側⾯
をコンポーネントに集約
• 機能の単位
• 機能を凝集(coherent)して実現
• 開発の単位
• コンポーネント単位で開発の管理を
⾏う
• 開発⼯程、担当者など
• モデリングの単位
• コンポーネントの実現はプログラミ
ングで
• 運⽤の単位
• 配備の単位
• バージョン管理
• 仕様書・マニュアルの単位
• 外部仕様、マニュアルの作成
• テストの単位
• テスト仕様の作成単位
• テストの進捗管理の単位
• 障害調査の単位
• 障害発⽣箇所の特定
• 障害調査マニュアルの作成
• 販売・課⾦の単位
• 再利⽤可能な商品
Scaling
再掲 第5回 静的モデル(2)
型
• できるだけ精密に型を定義してコンパイラにエラー検出させる
• 関数型⾔語の利⽤で、より精度の⾼い型の活⽤ができる
制約 (Constraint)
• テキストで表された⾃然⾔語または特定のフォーマル⾔語による意味的な
条件または制限
• OCL (Object Constraint Language)
• オブジェクト制約記述⾔語
• ⼀種の関数型⾔語
• 契約による設計 (Design by Contract)
• 不変条件 (invariant) : オブジェクトで常に真となる条件
• 事前条件 (pre-condition) : オペレーションの実⾏の前提となる条件
• 事後条件 (post-condition) : オペレーションの実⾏完了の前提となる条件
• 制約を重視する理由
• 散逸しがちな要件を確実に記録して実装につなげる
• 要件と実装の距離が短い分野なので効果抜群
• できるだけ上流のモデルで実装に直結する精度の仕様を確⽴しておきたい
再掲 第5回 静的モデル(2)
制約の使⽤例
再掲 第5回 静的モデル(2)
制約の使い所
• データ型/値域
• データ型の不変表明として定義
• 画⾯⼊⼒、データ移⼊のパラメタ/データ検証
• 画⾯⼊⼒の⼊⼒補助
• クラス/不変表明
• バグの検出に有効
• オペレーション/事前条件
• 画⾯⼊⼒、データ移⼊のパラメタ/データ検証
• 画⾯⼊⼒の⼊⼒補助
• オペレーション/事後条件
• バグの検出に有効
• データ破壊を未然に防ぐ効果
• データ移⼊・移出
• 異常データのチェック・クレンジング
再掲 第5回 静的モデル(2)
コンポーネント仕様では、できるだけ制約
を⽤いて仕様を精密に定義したい
コンポーネント実現では、必要に応じて
適材適所で使うとよい
Travel Light
リファクタリング
• リファクタリングを恐れない
• 技術債務の累積を防ぐ
• リファクタリングを成⽴させるためのエコシステムを整備
• 静的型付け⾔語 (e.g. Scala, Java)
• 関数型⾔語がより強⼒
• CBDによる開発管理
• テスト容易性の向上
• DI (Dependency Injection)
• CI/CDパイプライン
• TDD (Test-Driven Development)
• 型、制約によるエラー検出
• Linterによるコードのチェック
DI (Dependency Injection)
• IoC (Inversion of Control)
• 依存性の注⼊
• コンポーネント(オブジェクト)が必要とするコンポーネント(オブジェクト)
への参照作成を⾃ら⾏わず、外部から設定する
• 狙い
• コンポーネント(オブジェクト)間を疎結合[Low Coupling]にする
• 部品としての再利⽤性を⾼める
• テスト容易性を⾼める
• 注⼊のタイミング
• コンポーネント(オブジェクト)作成・組み⽴て時
• オペレーション(メソッド)呼び出し時
再掲 第20回 設計/コンポーネント設計(1)
DI(Dependency Injection)の種類
• DIコンテナ
• DIコンテナが利⽤サービスをワイヤリング
• Cakeパターン
• Scalaのモジュール機能を使って利⽤サービスをワイヤリング
• Readerモナド
• Readerモナドで利⽤サービスを受け渡す
• Freeモナド
• Freeモナドのインタープリタで利⽤サビースを受け渡す
• 実⾏コンテキスト
• 実⾏コンテキスト経由で利⽤サービスを受け渡す
• コンストラクタ
• 利⽤サービスをコンストラクタで受け渡す
再掲 第20回 設計/コンポーネント設計(1)
本講座ではDIコンテナとしてGuiceを⽤いる
import com.google.inject.AbstractModule
class ReservationService @Inject()(
val config: ReservationConfig,
val rule: IReservationRule,
val logic: IReservationLogic,
val factory: IReservationFactory,
val repository: IReservationRepository,
val receptor: IReservationReceptor
) extends IReservationService, IReservationManagement, IReservationAdmin,
IReservationMetrix, EventListener {
…
ReservationService.scala
import com.google.inject.AbstractModule
class ReservationServiceModule(config: Config) extends AbstractModule {
override def configure(): Unit = {
val c = ReservationConfig.create(config)
bind(classOf[IReservationRule]).toInstance(ReservationRule.create(c))
bind(classOf[IReservationLogic]).toInstance(ReservationLogic.create(c))
bind(classOf[IReservationFactory]).toInstance(ReservationFactory.create(c))
bind(classOf[IReservationRepository]).toInstance(ReservationRepository.create(c))
bind(classOf[IReservationReceptor]).toInstance(ReservationReceptor.create(c))
}
}
ReservationServiceModule.scala
組み⽴て
• アプリケーションをDI (Dependency Injection)を⽤いて組み⽴
てる
• コンポーネントを部品として組み込む
class App @Inject()(reservation: IReservationService) {
def run(): Unit = ???
}
object App {
def main(args: Array[String]): Unit = {
val config = ConfigFactory.load()
val injector = Guice.createInjector(new AppModule(config))
val app = injector.getInstance(classOf[App])
app.run()
}
}
App.scala
package app
import javax.inject.Singleton
import com.google.inject.AbstractModule
import reservation.*
class AppModule(config: Config) extends AbstractModule {
override def configure(): Unit = {
bind(classOf[IReservationService]).
to(classOf[ReservationService]).
in(classOf[Singleton])
install(new ReservationServiceModule(config))
}
}
AppModule.scala
変化点 (variation point)
• 各種デフォルト値や処理ロジックの選択などを起動時の外部パ
ラメタで与える
• 部品化による再利⽤には重要な機能
• 変化点の実現⽅法
• コンストラクタでパラメタとして受け取る
• DIで注⼊
• 外部リソース(環境変数、ファイル、DB)の情報から設定
再掲 第21回 設計/コンポーネント設計(2)
変化点/本講座の選択
• 変化点の記述にはHOCON形式を⽤いる
• コンポーネント単位で変化点をcase classにまとめる
• ReservationConfig
• コンポーネントへはDIの処理でHOCON情報をコンポーネント
に渡す
• コンポーネントが固有の場所から変化点情報を取得しない
import com.typesafe.config.Config
import io.circe.parser._
import io.circe.generic.auto._
case class ReservationConfig(
cacheSize: Int = 10000
) {
import ReservationConfig.*
}
object ReservationConfig {
val PREFIX = "org.simplemodeling.domain.reservation"
val default = ReservationConfig()
def create(config: Config): ReservationConfig = {
val c = config.getConfig(PREFIX)
val json = c.root().render()
parse(json)
}
def parse(json: String): ReservationConfig = {
decode[ReservationConfig](json)
}
def createForTest(): ReservationConfig = default
}
ReservationConfig.scala
改良 第22回 設計/コンポーネント設計(3)
Circeを⽤いてJSONのパースを⾃動化
class App @Inject()(reservation: IReservationService) {
def run(): Unit = ???
}
object App {
def main(args: Array[String]): Unit = {
val config = ConfigFactory.load()
val injector = Guice.createInjector(new AppModule(config))
val app = injector.getInstance(classOf[App])
app.run()
}
}
App.scala
class ReservationServiceModule(config: Config) extends AbstractModule {
override def configure(): Unit = {
val c = ReservationConfig.create(config)
bind(classOf[IReservationRule]).toInstance(ReservationRule.create(c))
bind(classOf[IReservationLogic]).toInstance(ReservationLogic.create(c))
bind(classOf[IReservationFactory]).toInstance(ReservationFactory.create(c))
bind(classOf[IReservationRepository]).toInstance(ReservationRepository.create(c))
bind(classOf[IReservationReceptor]).toInstance(ReservationReceptor.create(c))
}
}
ReservationServiceModule.scala
再掲 第22回 設計/コンポーネント設計(3)
拡張点 (extension point)
• 要求インタフェースを使⽤して、後付で機能拡張を可能にする
• 部品化による再利⽤には重要な機能
• 拡張点の実現⽅法
• コンストラクタでパラメタとして受け取る
• DIで注⼊
• 外部リソース(環境変数、ファイル、DB)の情報から設定
• 拡張点にOSGiを採⽤することでライブラリ・バージョン衝突
問題(ライブラリ・ヘル)を解決できる
再掲 第21回 設計/コンポーネント設計(2)
拡張点/本講座の選択
• コンポーネントを中⼼クラスのコンストラクタに設定
• DIではコンストラクタにインジェクトする
• テストプログラムでは、コンポーネント⽣成時にテストプログ
ラムがコンストラクタに渡す
TDD
Test-Driven Development
TDD (Test-Driven Development)
• テストをプログラミングの前に書く開発⼿法
• CI/CDパイプラインに⾃動テストとして組み込むことで常に⾃
動テストによって品質が保持される
• テストを動く仕様書とする
• Specification as a Code
• プログラミングの前にテストケースを整備することで、テスト
ケースの作成⼯数を確保できる
再掲 第30回 設計/実装(1)
詳細は「第32回テスト」
仕様書の作成場所 再掲 第30回 設計/実装(1)
class ReservationServiceSpec extends AnyFreeSpec
with Matchers
with GivenWhenThen
with ReservationMatchers {
"ReservationService" - {
"reserveメソッド" - {
"予約の追加" - {
"現在時刻より後ろの有効な予約時間を予約" in {
Given("テスト⽤に作成したReservationServiceを使⽤")
val service = ReservationService.createForTest()
When("運⽤時間内の時間")
val dt = "2024-01-15T10:00:00+JST"
Then("テスト対象時刻で実⾏コンテキストを⽤意")
given ExecutionContext = ExecutionContext.createWithCurrentDateTime(dt)
And("現在時刻より後ろの有効な予約時間で予約")
ReservationServiceSpec.sbt (1/2)
val rid = ResourceId("id1234")
val uid = UserId("user789")
val start = LocalDateTime.of(2024, 1, 15, 11, 00)
val end = LocalDateTime.of(2024, 1, 15, 12, 00)
val interval = Interval.openUpper(start, end)
val cmd = ReserveCommand(rid, uid, interval)
service.reserve(cmd) should successReserve(cmd)
}
}
}
}
}
ReservationServiceSpec.sbt (2/2)
trait ReservationMatchers {
import ReservationMatchers.*
def successReserve(cmd: ReserveCommand): SuccessReserveMatcher =
SuccessReserveMatcher(cmd)
}
object ReservationMatchers {
case class SuccessReserveMatcher(cmd: ReserveCommand)
extends Matcher[Try[ReserveResult]] {
def apply(p: Try[ReserveResult]): MatchResult = {
val result = ???
MatchResult(result, "failure", "success")
}
}
}
ReservationMatcher.scala
sbt:reservation> test
[info] compiling 2 Scala sources to
/Users/asami/src/Project2023/ofad/domain/target/scala-3.1.1/test-classes ...
[info] done compiling
[info] ReservationServiceSpec:
[info] ReservationService
[info] reserveメソッド
[info] 予約の追加
[info] - 現在時刻より後ろの有効な予約時間を予約
[info] + Given テスト⽤に作成したReservationServiceを使⽤
[info] + When 運⽤時間内の時間
[info] + Then テスト対象時刻で実⾏コンテキストを⽤意
[info] + And 現在時刻より後ろの有効な予約時間で予約
[info] Run completed in 270 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 1 s, completed 2024/01/16 16:00:27
テストを実⾏
class FractionSpec extends AnyFreeSpec
with Matchers
with GivenWhenThen
with ScalaCheckDrivenPropertyChecks {
case class Fraction(n: Int, d: Int) {
require (d != 0)
require (n != Integer.MIN_VALUE)
require (d != Integer.MIN_VALUE)
val numerator = n * (if (d < 0) -1 else 1)
val denominator = d.abs
}
FractionSpec.scala (1/2)
"分数(例題として)" - {
"分⼦と分⺟の値域の全範囲でnumeratorメソッドとdenominatorメソッドを確認" in {
forAll { (分⼦: Int, 分⺟: Int) =>
whenever (分⺟ != 0 && 分⺟ != Integer.MIN_VALUE && 分⼦ !=
Integer.MIN_VALUE) {
val 分数 = Fraction(分⼦, 分⺟)
if (分⼦ < 0 && 分⺟ < 0 || 分⼦ > 0 && 分⺟ > 0)
分数.numerator should be > 0
else if (分⼦ != 0)
分数.numerator should be < 0
else
分数.numerator should equal (0)
分数.denominator should be > 0
}
}
}
}
}
FractionSpec.scala (2/2)
sbt:reservation> test
[info] compiling 2 Scala sources to
/Users/asami/src/Project2023/ofad/domain/target/scala-3.1.1/test-classes ...
[info] done compiling
[info] [info] FractionSpec:
[info] 分数(例題として)
[info] - 分⼦と分⺟の値域の全範囲でnumeratorメソッドとdenominatorメソッドを確認
[info] Run completed in 270 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 1 s, completed 2024/01/16 16:00:27
テストを実⾏
まとめ
• 設計の成果物を確認
• 実装のポイント
• CBD (Component-Based Development)
• 型
• 制約
• リファクタリング
• DI (Dependency Injection)
• 変化点
• 拡張点
• TDD (Test-Driven Development)
• テストプログラムを動く仕様書にする
参考⽂献
• The Unified Modeling Language Reference
Manual, 2nd (Rumbaugh他, 2004)
• The Unified Modeling Language User Guide,
2nd (Booch他, 2004)
• The Unified Software Development Process
(Jacobson他, 1999)
• The Object Constraint Language, 2nd (Warmer
他, 2003)
• UML 2 and the Unified Process: Practical
Object-Oriented Analysis and Design (Arlow
他, 2005)
• OMG Unified Modeling Language Version 2.5
(OMG, 2015)
• 上流⼯程UMLモデリング (浅海, 2008)
• Modern Software Engineering : Doing What
Works to Build Better Software Faster (Farley,
2021)

実装(2) 【クラウドアプリケーションのためのオブジェクト指向分析設計講座 第31回】

  • 1.
  • 2.
    SimpleModeling2021 • オブジェクト指向分析設計での共通範囲 • UML/UP •本講座で使⽤するUMLプロファイル • プロファイル:SimpleModeling2021 (SM2021) • オブジェクト指向分析設計の基本からの拡張部を明確化 • アジャイル開発 • Communication • Embrace Change • Travel Light • Scaling • Component-Based Development • クラウド・アプリケーション • モデル駆動開発 SM2021 Travel Light Embrace Change Cloud Model-Driven Scaling CBD Testability Serviceability • ⾮機能要件 • Testability • Serviceability
  • 3.
    第1部 基本編の構成(1) • 概論[第1回] • 開発プロセス [第2回] • 基本モデル [第3回] • 静的モデル(1) [第4回] • 静的モデル(2) [第5回] • 動的モデル [第6回] • 協調モデル [第7回] • 関数モデル [第8回] • 物理モデル [第9回] • 作業分野 [第10回] • ビジネス・モデリング [第11回] • 要求 [第12回] • 要求/ユースケース [第13回] • 要求/シナリオ [第14回] • 分析 [第15回] • 分析/コンポーネント分析 [第16回] • 分析/イベント駆動 [第17回] • 作業分野 • 設計 [第18回] • 設計/アーキテクチャ設計 [第19回] • 設計/コンポーネント設計(1) [第20回] • 設計/コンポーネント設計(2) [第21回] • 設計/コンポーネント設計(3) [第22回] • 設計/ドメイン設計(1) [第23回] • 設計/ドメイン設計(2) [第24回] • 設計/ドメイン設計(3) [第25回] • 設計/ドメイン設計(4) [第26回] • 設計/ドメイン設計(5) [第27回] • 設計/原理 [第28回] • 設計/ UX/UI設計 [第29回] • 実装(1) [第30回] • 実装(2) [第31回] • 実装(3) [第32回] • テスト [第33回]
  • 4.
    第1部 基本編の構成(2) • アプリケーション・アーキテクチャ[第34回] • ドメイン・モデル [第35回] • アプリケーション・モデル [第36回] • プレゼンテーション・モデル [第37回] • ケーススタディ[第38回] • 要求モデル [第39回] • 分析モデル [第40回] • 設計モデル [第41回] • 実装 [第42回] • テスト [第43回]
  • 5.
    本講座のアプローチ • オブジェクト指向分析設計の基本を確認 • UML+ UP(Unified Process) • CBD (Component-Based Development) • 最新技術でアップデート • クラウド・コンピューティング • イベント駆動、分散・並列 • ビッグデータ、AI、IoT • コンテナ • 関数型 • OFP(Object-Functional Programming), Reactive Streams • ルール, AI • DevOps • アジャイル開発 • DX (Digital Transformation) 第25回 アプリケーション・アーキテクチャ 第2回 開発プロセス 第9回 物理モデル 第11回 ビジネス・モデリング 第2部 クラウド・アプリケーション編 第21回 設計/ドメイン設計 第20回 設計/コンポーネント設計 第2部 クラウド・アプリケーション編
  • 6.
    原理 (Principle) • AgileSoftware Development [ASD] • SRP (The Single Responsibility Principle) • OCP (The Open-Close Principle) • LSP (The Liskov Substitution Principle) • … • GRASP (General Responsibility Assignment Software Patterns or principles) • Low Coupling • High Cohesion • … • Writing Effective Use Cases [WEUC] • Scope • …
  • 7.
    パターン (Pattern) • DesignPatterns [DP] • Observer, Strategy, … • Domain Driven Design [DDD] • Ubiquitous Language, Intention- Revealing Interfaces, … • Analysis Patterns [AP] • Party, Quantity, … • Pattern-Oriented Software Architecture [POSA] • Layers, Pipes and Filters, … • Patterns of Enterprise Application Architecture [PEAA] • Unit of Work, Data Transfer Object, … • Enterprise Integration Patterns [EIP] • Message Bus, Aggregator, … • Patterns for Effective Use Cases [PEUC] • CompleteSingleGoal, VerbPhraseName, … • AntiPatterns [AnP] • Stovepipe System, Analysis Paralysis, …
  • 8.
    内容 • 設計の成果物 • 設計 •基本構造 • メソッド実装 • 実装のポイント • TDD (Test-Driven Development)
  • 9.
  • 10.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
    基本構造 • ドメイン・コンポーネントを構成するオブジェクトとして、以 下のオブジェクトを導⼊ • サービス(IReservationService, ReservationService) • コンフィグ (ReservationConfig) • ルール (IReservationRule, ReservationRule) • ロジック (IReservationLogic, ReservationLogic) • リポジトリ(IReservationRepository, ReservationRepository) • ファクトリ (IReservationFactory, ReservationFactory) • レセプタ (IReservationReceptor, ReservationReceptor) 再掲 第22回 設計/コンポーネント設計(3)
  • 19.
  • 20.
  • 21.
    ドメイン・サービス • ドメイン・サービス • ドメイン・モデル内でのサービス •Services [DDD] • Service Layer [PEAA] • 本モデルではコンポーネント(ReservationDomain)の本体とな るオブジェクトとして実現 • DI(Dependency Injection)によって構成要素のオブジェクトを注⼊ 再掲 第22回 設計/コンポーネント設計(3)
  • 22.
    … class ReservationService @Inject()( valconfig: ReservationConfig, val rule: IReservationRule, val logic: IReservationLogic, val factory: IReservationFactory, val repository: IReservationRepository, val receptor: IReservationReceptor, ) extends IReservationService, IReservationManagement, IReservationAdmin, IReservationMetrix, EventListener { … ReservationService.scala 再掲 第22回 設計/コンポーネント設計(3)
  • 23.
    class ReservationServiceModule(config: Config)extends AbstractModule { override def configure(): Unit = { val c = ReservationConfig.create(config) bind(classOf[IReservationRule]).toInstance(ReservationRule.create(c)) bind(classOf[IReservationLogic]).toInstance(ReservationLogic.create(c)) bind(classOf[IReservationFactory]).toInstance(ReservationFactory.create(c)) bind(classOf[IReservationRepository]).toInstance(ReservationRepository.create(c)) bind(classOf[IReservationReceptor]).toInstance(ReservationReceptor.create(c)) } } ReservationServiceModule.scala 再掲 第22回 設計/コンポーネント設計(3)
  • 24.
  • 25.
    import com.typesafe.config.Config import io.circe.parser._ importio.circe.generic.auto._ case class ReservationConfig( cacheSize: Int = 10000 ) { import ReservationConfig.* } object ReservationConfig { val PREFIX = "org.simplemodeling.domain.reservation" val default = ReservationConfig() def create(config: Config): ReservationConfig = { val c = config.getConfig(PREFIX) val json = c.root().render() parse(json) } def parse(json: String): ReservationConfig = { decode[ReservationConfig](json) } def createForTest(): ReservationConfig = default } ReservationConfig.scala 改良 第22回 設計/コンポーネント設計(3) Circeを⽤いてJSONのパースを⾃動化
  • 26.
    ドメイン・ルール • 各種のルールを定義 • 宣⾔的に記述できる責務 •ルールとしてモデル化するのが適当なもの • 例:料⾦表 • 関数 • Side-Effect-Free Functions [DDD] • コンポーネントはルールを実⾏するインタープリタとして動作 する • ルールが本格化してくると、このような⾒⽴てもできる • 例:ビジネス・プロセス・ルール 再掲 第22回 設計/コンポーネント設計(3)
  • 27.
    package org.simplemodeling.domain.reservation import scala.util.Try importorg.simplemodeling.componentframework.datatype.Money trait IReservationRule { def calcReservationFee(reservation: Reservation): Try[Money] } IReservationRule.scala 追加 第22回 設計/コンポーネント設計(3) package org.simplemodeling.domain.reservation class ReservationRule() extends IReservationRule { def calcReservationFee(reservation: Reservation): Try[Money] = ??? } object ReservationRule { def create(config: ReservationConfig): ReservationRule = new ReservationRule() } ReservationRule.scala
  • 28.
    ドメイン・ロジック • ドメイン内のロジックを記述 • ドメイン・ルールを活⽤しながら、参照処理に加えて更新処理 を⾏う •ドメイン・サービスで実現してもよいが、分離した⽅がより拡 張性、柔軟性が⾼まる SM2021 再掲 第22回 設計/コンポーネント設計(3)
  • 29.
    package org.simplemodeling.domain.reservation import scala.util.Try importorg.simplemodeling.componentframework.ExecutionContext trait IReservationLogic { def reserve(cmd: ReserveCommand)(using ctx: ExecutionContext): Try[ReserveResult] def unreserve(cmd: UnreserveCommand)(using ctx: ExecutionContext): Try[ReserveResult] def getReservation(id: ReservationId)(using ctx: ExecutionContext): Try[Option[Reservation]] def queryReservation(q: ReservationQuery)(using ctx: ExecutionContext): Try[Vector[Reservation]] } IReservationLogic 再掲 第22回 設計/コンポーネント設計(3)
  • 30.
    ドメイン・ファクトリ • ドメイン・コンポーネントで使⽤するドメイン・オブジェクト の作成 • Factories[DDD] • ドメイン・オブジェクトのコンストラクタを直接呼び出す⽅式 ⽐べて以下の優位性がある • 将来の拡張性を担保 • デフォルト値などをドメイン・コンポーネントの変化点とし設定可能 になる 再掲 第22回 設計/コンポーネント設計(3)
  • 31.
    package org.simplemodeling.domain.reservation import scala.util.Try traitIReservationFactory { def createReservation(p: Map[String, Any]): Try[Reservation] } IReservationFactory.scala 再掲 第22回 設計/コンポーネント設計(3)
  • 32.
    リポジトリ • アプリケーションのメタデータを管理するデータストアという イメージもあるが… • Repository[PEAA] • データストアとドメイン・オブジェクトをマッピング • Repositories [DDD] • Entities [DDD] • Value Objects [DDD] • Aggregates [DDD] 再掲 第22回 設計/コンポーネント設計(3)
  • 33.
    trait IReservationRepository { importIReservationRepository._ def get(id: ReservationId): Try[Option[Reservation]] def list(query: Query, directive: ListDirective): Try[ListResult[Reservation]] def create(entity: datastore.create.Reservation): Try[Unit] def create(record: Map[String, Any]): Try[Unit] def update(directive: datastore.update.Reservation): Try[Unit] def update(id: ReservationId, record: Map[String, Any]): Try[Unit] def delete(id: ReservationId): Try[Unit] } IReservationRepository.scala (1/2) 再掲 第22回 設計/コンポーネント設計(3)
  • 34.
    object IReservationRepository { caseclass Reservation( id: ReservationId, resource: Resource, user: User, interval: Interval[LocalDateTime] ) case class Resource( id: ResourceId, name: String ) case class Query( resourceId: Option[ResourceId], userId: Option[UserId], interval: Option[Interval[LocalDateTime]] ) IReservationRepository.scala (2/2) object datastore { case class Reservation( resourceId: ResourceId, userId: UserId, interval: Interval[LocalDateTime] ) object create { case class Reservation( resourceId: ResourceId, userId: UserId, interval: Interval[LocalDateTime] ) } object update { case class Reservation( resourceId: Option[ResourceId], userId: Option[UserId], interval: Option[Interval[LocalDateTime]] ) } } } 再掲 第22回 設計/コンポーネント設計(3)
  • 35.
    ドメイン・レセプタ • イベントの受信と処理担当オブジェクトへのルーティングを⾏ う • ⽣イベント、ビジネス・イベントとドメイン・イベントのマッ ピングなどを⾏う •実際の処理はドメイン・ロジックに移譲する • ドメイン・ルールを使⽤してチューニング可能にしておくとよ い SM2021 再掲 第22回 設計/コンポーネント設計(3)
  • 36.
    package org.simplemodeling.domain.reservation import org.simplemodeling.componentframework.event.Event traitIReservationReceptor { def receive(evt: Event): Unit } IReservationReceptor.scala 再掲 第22回 設計/コンポーネント設計(3)
  • 37.
    package org.simplemodeling.domain.reservation import org.simplemodeling.componentframework.event.Event classReservationReceptor(config: ReservationConfig) extends IReservationReceptor { def receive(evt: Event): Unit = Option(evt) collect { case m: ReservedEvent => reserved(m) case m: UnreservedEvent => unreserved(m) } def reserved(evt: ReservedEvent): Unit = ??? def unreserved(evt: UnreservedEvent): Unit = ??? } object ReservationReceptor { def create(config: ReservationConfig): ReservationReceptor = new ReservationReceptor(config) } ReservationReceptor.scala 再掲 第22回 設計/コンポーネント設計(3)
  • 38.
  • 39.
    ユニバース (Universe) • 操作(Operation)に渡す環境情報 •操作に必要なエンティティの必要な部分の断⽚を切り出したもの • 関連パターン • Aggregate [DDD] • Whole-Part [POSA] • Aggregation/Composition [UML] • ワーキング・セット (Working Set) • OS(仮想記憶)の概念 • プロセスの動作に必要な情報を選択してメモリに載せる • ユースケース/タスクを実⾏するために、メモリ上に載っていなけれ ばならないエンティティ由来のオブジェクトの総体 SM2021 再掲 第26回 設計/ドメイン設計(4)
  • 40.
    case class ReservationSpace( reservations:Map[RoomId, ReservationSpace.ReservationSequence] = Map.empty ) { def isAvailable(roomid: RoomId, interval: Interval[LocalDateTime]): Boolean = ??? } object ReservationSpace { val USER_NAME_LENGTH = 32 val ROOM_NAME_LENGTH = 64 case class UserName(name: String) { assert (name.length < USER_NAME_LENGTH) } case class RoomName(name: String) { assert (name.length < ROOM_NAME_LENGTH) } case class Reservation( id: ReservationId, interval: Interval[LocalDateTime], user: User, room: Room ) ReservationSpace.scala ユニバースの実装としてReservationSpaceを定義 再掲 第27回 設計/ドメイン設計(5)
  • 41.
    case class User( id:UserId, name: UserName ) case class Room( id: RoomId, name: RoomName ) case class ReservationSequence( reservations: List[Reservation] ) def build( reservations: List[entity.Reservation], users: List[entity.User], rooms: List[entity.Room] ): Try[ReservationSpace] = ??? } ReservationSpace.scala 処理に必要なデータ構造をバリューとして定義する 再掲 第27回 設計/ドメイン設計(5)
  • 42.
    UnitOfWork • UnitOfWork (PEAA) •アプリケーション側でトランザクション的なリソース管理を⾏ う • DBトランザクション • ファイル作成・更新・削除 • イベント送信 • メッセージ送信 • 「状態遷移指⽰」をUnitOfWorkを⽤いて⾏う 再掲 第27回 設計/ドメイン設計(5)
  • 43.
    class UnitOfWork( context: ExecutionContext ){ import UnitOfWork.* def create[T](store: EntityStore, data: Record)(using instance: EntityInstance[T]): Try[CreateResult[T]] = ??? def get[T](store: EntityStore)(using instance: EntityInstance[T]): Try[GetResult[T]] = ??? def list[T](store: EntityStore, directive: ListDirective)(using instance: EntityInstance[T]): Try[ListResult[T]] = ??? def update[T](store: EntityStore, id: EntityId, data: Record)(using instance: EntityInstance[T]): Try[UpdateResult[T]] = ??? def delete[T](store: EntityStore, data: Record)(using instance: EntityInstance[T]): Try[DeleteResult[T]] = ??? def createFile(file: File, data: String): Try[Unit] = ??? def sendMessage(msg: Message): Try[Unit] = ??? def commit(): Try[CommitResult] = ??? def abort(): Try[AbortResult] = ??? } UnitOfWork.scala UnitOfWorkの機能を提供するオブジェクト。 Execution Contextの情報を活⽤しながら動作する。 再掲 第27回 設計/ドメイン設計(5)
  • 44.
    object UnitOfWork { typeCommitResult = Unit type AbortResult = Unit sealed trait UnitOfWorkOp[T] case class CreateEntity[ENTITY](store: EntityStore, data: Record, instance: EntityInstance[ENTITY]) extends UnitOfWorkOp[Try[CreateResult[ENTITY]]] type UnitOfWorkFM[T] = Free[UnitOfWorkOp, T] type UnitOfWorkEitherFM[T] = EitherT[UnitOfWorkFM, Throwable, T] type Config = ExecutionContext type Log = List[String] type State = Map[String, Any] type UnitOfWorkRWSEitherFM[T] = RWST[UnitOfWorkEitherFM, Config, Log, State, T] def create[A](store: EntityStore, data: Record)(using instance: EntityInstance[A]): UnitOfWorkFM[Try[CreateResult[A]]] = Free.liftF(CreateEntity(store, data, instance)) UnitOfWork.scala UnitOfWorkのコンパニオン・オブジェクト。 FreeモナドベースのUnitOfWorkのDSL機能を提供する。 再掲 第27回 設計/ドメイン設計(5)
  • 45.
    OOP (Object-Oriented Programming) •⼀般的なオブジェクト指向プログラミングの例 • メソッドの実装は⼿続き型 再掲 第27回 設計/ドメイン設計(5)
  • 46.
    class ReservationLogic() extendsIReservationLogic { def reserve(cmd: ReserveCommand)(using ctx: ExecutionContext): Try[ReserveResult] = Try { val uow = ctx.unitOfWork val roomid = RoomId(cmd.resourceId) val space = build_resrvation_space(roomid, cmd.interval) if (space.isAvailable(roomid, cmd.interval)) make_reservation(uow, roomid, cmd.interval, cmd.userId) else throw new IllegalStateException("???") } protected def build_resrvation_space( roomid: RoomId, interval: Interval[LocalDateTime] ): ReservationSpace = { val reservations = fetch_reservations(roomid, interval) val users = fetch_users(reservations) val rooms = fetch_rooms(reservations) ReservationSpace.build(reservations, users, rooms).get } ResrevationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
  • 47.
    protected def make_reservation( uow:UnitOfWork, roomid: RoomId, interval: Interval[LocalDateTime], userid: UserId ): ReserveResult = { val data = Map( "room_id" -> roomid.id, "interval" -> interval, "user_id" -> userid.id ) uow.create(ReservationStore, data) match { case Success(s) => ReserveResult() case Failure(e) => throw e } } ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
  • 48.
    ReaderWriterStateT&EitherT&Free • EitherT&Free • プログラム内プログラミング⾔語をエラー状態付きで実現 •本例ではUnitOfWorkをエラー状態付きでDSL化 • ReaderWriterStateモナド • Readerモナド、Writerモナド、Stateモナドを合成したモナド • ⽂脈情報伝播+情報追記+状態遷移 • ReaderWriterStateTモナド・トランスフォーマ • ReaderWriterStateモナドと任意のモナドを合成 • ReaderWriterStateT&EitherT&Free • プログラム内プログラミング⾔語をエラー状態付きで実現 • ⽂脈情報伝播+情報追記+状態遷移 再掲 第27回 設計/ドメイン設計(5)
  • 49.
  • 50.
    def reserve(cmd: ReserveCommand)(usingctx: ExecutionContext): Try[ReserveResult] = { val interpreter: UnitOfWorkOp ~> Id = ctx.unitOfWorkInterpreter reserveFM(cmd).value.foldMap(interpreter).toTry } def reserveFM(cmd: ReserveCommand)(using ctx: ExecutionContext): UnitOfWorkEitherFM[ReserveResult] = { reserveRWS(cmd).run(ctx, Map.empty).map(_._3) } def reserveRWS(cmd: ReserveCommand): UnitOfWorkRWSEitherFM[ReserveResult] = { val roomid = RoomId(cmd.resourceId) for { space <- build_reservation_space(RoomId(cmd.resourceId), cmd.interval) _ <- check_reservation(space, roomid, cmd.interval) r <- make_reservation(roomid, cmd.interval, cmd.userId) } yield r } ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
  • 51.
    protected def build_reservation_space( roomid:RoomId, interval: Interval[LocalDateTime] ): UnitOfWorkRWSEitherFM[ReservationSpace] = RWST { (ctx, state) => for { reservations <- fetch_reservations(roomid, interval) users <- fetch_users(reservations) rooms <- fetch_rooms(reservations) r <- EitherT(Free.pure(ReservationSpace.build(reservations, users, rooms).toEither)) } yield (Nil, state, r) } ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
  • 52.
    protected def check_reservation(space:ReservationSpace, roomid: RoomId, interval: Interval[LocalDateTime]): UnitOfWorkRWSEitherFM[Unit] = RWST { (ctx, state) => EitherT { Free.pure { if (space.isAvailable(roomid, interval)) Right((Nil, state, ())) else Left(new IllegalArgumentException("Already reserved")) } } } ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
  • 53.
    protected def make_reservation( roomid:RoomId, interval: Interval[LocalDateTime], userid: UserId ): UnitOfWorkRWSEitherFM[ReserveResult] = { val data = Map( "room_id" -> roomid.id, "interval" -> interval, "user_id" -> userid.id ) RWST { (config, state) => EitherT { UnitOfWork.create(ReservationStore, data).map(_.map(_ => (Nil, state, ReserveResult())).toEither) } } } ReservationLogic.scala 再掲 第27回 設計/ドメイン設計(5)
  • 54.
    コンポーネント・フレームワークと Freeモナド • FreeモナドによるDSLによってコンポーネント・フレーム ワークの機能を隠蔽して使いやすくする • 関数型を採⽤するメリット •使⽤するモナドの組み合わせによってインタフェースのシグネ チャが異なってくる • 設計に影響 • ReaderWriterStateT&EitherT&Freeの組み合わせが⼀つ の形 • ReaderWriterStateTは学習コストと利便性のトレードオフで採⽤ を考える • Freeモナドの代わりにIOモナドを使⽤するケースも考えられる 再掲 第27回 設計/ドメイン設計(5)
  • 55.
  • 56.
    実装のポイント • CBD (Component-BasedDevelopment) • 型 • 制約 • リファクタリング • DI (Dependency Injection) • 変化点 • 拡張点
  • 57.
    CBD (Component-Based Development) •コンポーネントを開発の単位として、インタフェースで定義さ れてメソッドを実現していく
  • 58.
    コンポーネント (Component) • 再利⽤可能な部品 •明確な外部インタフェースを定め実装を隠蔽 • 新規開発を抑えることで開発期間短縮、品質向上を図る • 再利⽤性の⾼い部品化は⾃然にできることではないので、コストを掛けて積極的に狙い に⾏く • 論理モデルと物理モデルの両⽅の性質を兼ね備える • 配備の単位 • 物理的な側⾯ ⇒ モジュール • オブジェクト指向分析設計による開発の様々な側⾯を集約してパッケージ化 • モデリングとプログラミングの結節点 • モデリングはコンポーネントまで。コンポーネントの実現はプログラミングで。 • 再利⽤可能な部品にするための仕掛けが必要 • コンポーネント・フレームワーク • 実⾏コンテキスト Travel Light Travel Light 再掲 第5回 静的モデル(2)
  • 59.
    オブジェクト指向分析設計の様々の側⾯ をコンポーネントに集約 • 機能の単位 • 機能を凝集(coherent)して実現 •開発の単位 • コンポーネント単位で開発の管理を ⾏う • 開発⼯程、担当者など • モデリングの単位 • コンポーネントの実現はプログラミ ングで • 運⽤の単位 • 配備の単位 • バージョン管理 • 仕様書・マニュアルの単位 • 外部仕様、マニュアルの作成 • テストの単位 • テスト仕様の作成単位 • テストの進捗管理の単位 • 障害調査の単位 • 障害発⽣箇所の特定 • 障害調査マニュアルの作成 • 販売・課⾦の単位 • 再利⽤可能な商品 Scaling 再掲 第5回 静的モデル(2)
  • 60.
  • 61.
    制約 (Constraint) • テキストで表された⾃然⾔語または特定のフォーマル⾔語による意味的な 条件または制限 •OCL (Object Constraint Language) • オブジェクト制約記述⾔語 • ⼀種の関数型⾔語 • 契約による設計 (Design by Contract) • 不変条件 (invariant) : オブジェクトで常に真となる条件 • 事前条件 (pre-condition) : オペレーションの実⾏の前提となる条件 • 事後条件 (post-condition) : オペレーションの実⾏完了の前提となる条件 • 制約を重視する理由 • 散逸しがちな要件を確実に記録して実装につなげる • 要件と実装の距離が短い分野なので効果抜群 • できるだけ上流のモデルで実装に直結する精度の仕様を確⽴しておきたい 再掲 第5回 静的モデル(2)
  • 62.
  • 63.
    制約の使い所 • データ型/値域 • データ型の不変表明として定義 •画⾯⼊⼒、データ移⼊のパラメタ/データ検証 • 画⾯⼊⼒の⼊⼒補助 • クラス/不変表明 • バグの検出に有効 • オペレーション/事前条件 • 画⾯⼊⼒、データ移⼊のパラメタ/データ検証 • 画⾯⼊⼒の⼊⼒補助 • オペレーション/事後条件 • バグの検出に有効 • データ破壊を未然に防ぐ効果 • データ移⼊・移出 • 異常データのチェック・クレンジング 再掲 第5回 静的モデル(2) コンポーネント仕様では、できるだけ制約 を⽤いて仕様を精密に定義したい コンポーネント実現では、必要に応じて 適材適所で使うとよい Travel Light
  • 64.
    リファクタリング • リファクタリングを恐れない • 技術債務の累積を防ぐ •リファクタリングを成⽴させるためのエコシステムを整備 • 静的型付け⾔語 (e.g. Scala, Java) • 関数型⾔語がより強⼒ • CBDによる開発管理 • テスト容易性の向上 • DI (Dependency Injection) • CI/CDパイプライン • TDD (Test-Driven Development) • 型、制約によるエラー検出 • Linterによるコードのチェック
  • 65.
    DI (Dependency Injection) •IoC (Inversion of Control) • 依存性の注⼊ • コンポーネント(オブジェクト)が必要とするコンポーネント(オブジェクト) への参照作成を⾃ら⾏わず、外部から設定する • 狙い • コンポーネント(オブジェクト)間を疎結合[Low Coupling]にする • 部品としての再利⽤性を⾼める • テスト容易性を⾼める • 注⼊のタイミング • コンポーネント(オブジェクト)作成・組み⽴て時 • オペレーション(メソッド)呼び出し時 再掲 第20回 設計/コンポーネント設計(1)
  • 66.
    DI(Dependency Injection)の種類 • DIコンテナ •DIコンテナが利⽤サービスをワイヤリング • Cakeパターン • Scalaのモジュール機能を使って利⽤サービスをワイヤリング • Readerモナド • Readerモナドで利⽤サービスを受け渡す • Freeモナド • Freeモナドのインタープリタで利⽤サビースを受け渡す • 実⾏コンテキスト • 実⾏コンテキスト経由で利⽤サービスを受け渡す • コンストラクタ • 利⽤サービスをコンストラクタで受け渡す 再掲 第20回 設計/コンポーネント設計(1) 本講座ではDIコンテナとしてGuiceを⽤いる
  • 67.
    import com.google.inject.AbstractModule class ReservationService@Inject()( val config: ReservationConfig, val rule: IReservationRule, val logic: IReservationLogic, val factory: IReservationFactory, val repository: IReservationRepository, val receptor: IReservationReceptor ) extends IReservationService, IReservationManagement, IReservationAdmin, IReservationMetrix, EventListener { … ReservationService.scala
  • 68.
    import com.google.inject.AbstractModule class ReservationServiceModule(config:Config) extends AbstractModule { override def configure(): Unit = { val c = ReservationConfig.create(config) bind(classOf[IReservationRule]).toInstance(ReservationRule.create(c)) bind(classOf[IReservationLogic]).toInstance(ReservationLogic.create(c)) bind(classOf[IReservationFactory]).toInstance(ReservationFactory.create(c)) bind(classOf[IReservationRepository]).toInstance(ReservationRepository.create(c)) bind(classOf[IReservationReceptor]).toInstance(ReservationReceptor.create(c)) } } ReservationServiceModule.scala
  • 69.
    組み⽴て • アプリケーションをDI (DependencyInjection)を⽤いて組み⽴ てる • コンポーネントを部品として組み込む
  • 70.
    class App @Inject()(reservation:IReservationService) { def run(): Unit = ??? } object App { def main(args: Array[String]): Unit = { val config = ConfigFactory.load() val injector = Guice.createInjector(new AppModule(config)) val app = injector.getInstance(classOf[App]) app.run() } } App.scala
  • 71.
    package app import javax.inject.Singleton importcom.google.inject.AbstractModule import reservation.* class AppModule(config: Config) extends AbstractModule { override def configure(): Unit = { bind(classOf[IReservationService]). to(classOf[ReservationService]). in(classOf[Singleton]) install(new ReservationServiceModule(config)) } } AppModule.scala
  • 72.
    変化点 (variation point) •各種デフォルト値や処理ロジックの選択などを起動時の外部パ ラメタで与える • 部品化による再利⽤には重要な機能 • 変化点の実現⽅法 • コンストラクタでパラメタとして受け取る • DIで注⼊ • 外部リソース(環境変数、ファイル、DB)の情報から設定 再掲 第21回 設計/コンポーネント設計(2)
  • 73.
    変化点/本講座の選択 • 変化点の記述にはHOCON形式を⽤いる • コンポーネント単位で変化点をcaseclassにまとめる • ReservationConfig • コンポーネントへはDIの処理でHOCON情報をコンポーネント に渡す • コンポーネントが固有の場所から変化点情報を取得しない
  • 74.
    import com.typesafe.config.Config import io.circe.parser._ importio.circe.generic.auto._ case class ReservationConfig( cacheSize: Int = 10000 ) { import ReservationConfig.* } object ReservationConfig { val PREFIX = "org.simplemodeling.domain.reservation" val default = ReservationConfig() def create(config: Config): ReservationConfig = { val c = config.getConfig(PREFIX) val json = c.root().render() parse(json) } def parse(json: String): ReservationConfig = { decode[ReservationConfig](json) } def createForTest(): ReservationConfig = default } ReservationConfig.scala 改良 第22回 設計/コンポーネント設計(3) Circeを⽤いてJSONのパースを⾃動化
  • 75.
    class App @Inject()(reservation:IReservationService) { def run(): Unit = ??? } object App { def main(args: Array[String]): Unit = { val config = ConfigFactory.load() val injector = Guice.createInjector(new AppModule(config)) val app = injector.getInstance(classOf[App]) app.run() } } App.scala
  • 76.
    class ReservationServiceModule(config: Config)extends AbstractModule { override def configure(): Unit = { val c = ReservationConfig.create(config) bind(classOf[IReservationRule]).toInstance(ReservationRule.create(c)) bind(classOf[IReservationLogic]).toInstance(ReservationLogic.create(c)) bind(classOf[IReservationFactory]).toInstance(ReservationFactory.create(c)) bind(classOf[IReservationRepository]).toInstance(ReservationRepository.create(c)) bind(classOf[IReservationReceptor]).toInstance(ReservationReceptor.create(c)) } } ReservationServiceModule.scala 再掲 第22回 設計/コンポーネント設計(3)
  • 77.
    拡張点 (extension point) •要求インタフェースを使⽤して、後付で機能拡張を可能にする • 部品化による再利⽤には重要な機能 • 拡張点の実現⽅法 • コンストラクタでパラメタとして受け取る • DIで注⼊ • 外部リソース(環境変数、ファイル、DB)の情報から設定 • 拡張点にOSGiを採⽤することでライブラリ・バージョン衝突 問題(ライブラリ・ヘル)を解決できる 再掲 第21回 設計/コンポーネント設計(2)
  • 78.
    拡張点/本講座の選択 • コンポーネントを中⼼クラスのコンストラクタに設定 • DIではコンストラクタにインジェクトする •テストプログラムでは、コンポーネント⽣成時にテストプログ ラムがコンストラクタに渡す
  • 79.
  • 80.
    TDD (Test-Driven Development) •テストをプログラミングの前に書く開発⼿法 • CI/CDパイプラインに⾃動テストとして組み込むことで常に⾃ 動テストによって品質が保持される • テストを動く仕様書とする • Specification as a Code • プログラミングの前にテストケースを整備することで、テスト ケースの作成⼯数を確保できる 再掲 第30回 設計/実装(1) 詳細は「第32回テスト」
  • 81.
  • 82.
    class ReservationServiceSpec extendsAnyFreeSpec with Matchers with GivenWhenThen with ReservationMatchers { "ReservationService" - { "reserveメソッド" - { "予約の追加" - { "現在時刻より後ろの有効な予約時間を予約" in { Given("テスト⽤に作成したReservationServiceを使⽤") val service = ReservationService.createForTest() When("運⽤時間内の時間") val dt = "2024-01-15T10:00:00+JST" Then("テスト対象時刻で実⾏コンテキストを⽤意") given ExecutionContext = ExecutionContext.createWithCurrentDateTime(dt) And("現在時刻より後ろの有効な予約時間で予約") ReservationServiceSpec.sbt (1/2)
  • 83.
    val rid =ResourceId("id1234") val uid = UserId("user789") val start = LocalDateTime.of(2024, 1, 15, 11, 00) val end = LocalDateTime.of(2024, 1, 15, 12, 00) val interval = Interval.openUpper(start, end) val cmd = ReserveCommand(rid, uid, interval) service.reserve(cmd) should successReserve(cmd) } } } } } ReservationServiceSpec.sbt (2/2)
  • 84.
    trait ReservationMatchers { importReservationMatchers.* def successReserve(cmd: ReserveCommand): SuccessReserveMatcher = SuccessReserveMatcher(cmd) } object ReservationMatchers { case class SuccessReserveMatcher(cmd: ReserveCommand) extends Matcher[Try[ReserveResult]] { def apply(p: Try[ReserveResult]): MatchResult = { val result = ??? MatchResult(result, "failure", "success") } } } ReservationMatcher.scala
  • 85.
    sbt:reservation> test [info] compiling2 Scala sources to /Users/asami/src/Project2023/ofad/domain/target/scala-3.1.1/test-classes ... [info] done compiling [info] ReservationServiceSpec: [info] ReservationService [info] reserveメソッド [info] 予約の追加 [info] - 現在時刻より後ろの有効な予約時間を予約 [info] + Given テスト⽤に作成したReservationServiceを使⽤ [info] + When 運⽤時間内の時間 [info] + Then テスト対象時刻で実⾏コンテキストを⽤意 [info] + And 現在時刻より後ろの有効な予約時間で予約 [info] Run completed in 270 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 1 s, completed 2024/01/16 16:00:27 テストを実⾏
  • 86.
    class FractionSpec extendsAnyFreeSpec with Matchers with GivenWhenThen with ScalaCheckDrivenPropertyChecks { case class Fraction(n: Int, d: Int) { require (d != 0) require (n != Integer.MIN_VALUE) require (d != Integer.MIN_VALUE) val numerator = n * (if (d < 0) -1 else 1) val denominator = d.abs } FractionSpec.scala (1/2)
  • 87.
    "分数(例題として)" - { "分⼦と分⺟の値域の全範囲でnumeratorメソッドとdenominatorメソッドを確認"in { forAll { (分⼦: Int, 分⺟: Int) => whenever (分⺟ != 0 && 分⺟ != Integer.MIN_VALUE && 分⼦ != Integer.MIN_VALUE) { val 分数 = Fraction(分⼦, 分⺟) if (分⼦ < 0 && 分⺟ < 0 || 分⼦ > 0 && 分⺟ > 0) 分数.numerator should be > 0 else if (分⼦ != 0) 分数.numerator should be < 0 else 分数.numerator should equal (0) 分数.denominator should be > 0 } } } } } FractionSpec.scala (2/2)
  • 88.
    sbt:reservation> test [info] compiling2 Scala sources to /Users/asami/src/Project2023/ofad/domain/target/scala-3.1.1/test-classes ... [info] done compiling [info] [info] FractionSpec: [info] 分数(例題として) [info] - 分⼦と分⺟の値域の全範囲でnumeratorメソッドとdenominatorメソッドを確認 [info] Run completed in 270 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 1 s, completed 2024/01/16 16:00:27 テストを実⾏
  • 89.
    まとめ • 設計の成果物を確認 • 実装のポイント •CBD (Component-Based Development) • 型 • 制約 • リファクタリング • DI (Dependency Injection) • 変化点 • 拡張点 • TDD (Test-Driven Development) • テストプログラムを動く仕様書にする
  • 90.
    参考⽂献 • The UnifiedModeling Language Reference Manual, 2nd (Rumbaugh他, 2004) • The Unified Modeling Language User Guide, 2nd (Booch他, 2004) • The Unified Software Development Process (Jacobson他, 1999) • The Object Constraint Language, 2nd (Warmer 他, 2003) • UML 2 and the Unified Process: Practical Object-Oriented Analysis and Design (Arlow 他, 2005) • OMG Unified Modeling Language Version 2.5 (OMG, 2015) • 上流⼯程UMLモデリング (浅海, 2008) • Modern Software Engineering : Doing What Works to Build Better Software Faster (Farley, 2021)