VB.NETでも割と楽に状態を扱いたい
はじめに
.NETチームのブログを眺めていたところ、面白そうなライブラリを見つけました。
オブジェクトの状態遷移を管理するためのライブラリで、状態遷移を引き起こすトリガーとトリガーによって変わる状態を扱ってくれます。 また、状態遷移をイベントとしてデリゲートの実行なども行ってくれます。
購買申請
生産性アプリケーションの状態遷移といったら真っ先に思い浮かぶのが購買申請です。 某拝承系企業では購買申請でリアルスタンプラリーが開催されていると聞きますが、本当なのでしょうか?
混沌を極めた購買申請フローは狂気を感じるところがありますが、ここではとりあえず以下のような簡単な状態遷移を考えてみます。
"否認" +-------> 否認済み | | 承認待ち --------> 承認済み --------> 発注済み --------> 検収済み "承認" "発注" "検収"
また、各状態に遷移したらそれぞれに対応した処理を実行します。
- 承認済み・否認済み
- 申請者に通知
- 発注済み
- 業者にファックス
- 検収済み
- 買掛金元帳に追加
それでは、実際のコードを見てみましょう。
Enum 状態 承認待ち 承認済み 否認 発注済み 検収済み End Enum Enum 条件 承認 否認 発注 検収 End Enum
まずはオブジェクトがとりうる状態と、状態遷移のトリガーとなる条件を定義します。
Private _品目 As String Private _価格 As Decimal Private _状態 = 状態.承認待ち Private _状態機械 As StateMachine(Of 状態, 条件) Private _否認行為 As StateMachine(Of 状態, 条件).TriggerWithParameters(Of String) Public Sub New(品目 As String, 価格 As Decimal) _品目 = 品目 _価格 = 価格 _状態機械 = New StateMachine(Of 状態, 条件)(Function() _状態, Sub(状態) _状態 = 状態) _否認行為 = _状態機械.SetTriggerParameters(Of String)(条件.否認) _状態機械.Configure(状態.承認待ち). Permit(条件.承認, 状態.承認済み). Permit(条件.否認, 状態.否認) _状態機械.Configure(状態.承認済み). OnEntry(CType(Sub() 承認時(), Action)). Permit(条件.発注, 状態.発注済み) _状態機械.Configure(状態.否認). OnEntryFrom(_否認行為, Sub(理由) 否認時(理由)) _状態機械.Configure(状態.発注済み). OnEntry(CType(Sub() 発注時(), Action)). Permit(条件.検収, 状態.検収済み) _状態機械.Configure(状態.検収済み). OnEntry(CType(Sub() 検収時(), Action)) End Sub
まずは、
_状態機械 = New StateMachine(Of 状態, 条件)(Function() _状態, Sub(状態) _状態 = 状態)
でステートマシンの状態とオブジェクトの状態を関連付けます。 まぁ、状態を取得するラムダと状態をセットするラムダを突っ込むだけです。
そうしたら定義した状態と因子を用いて、ある状態から遷移可能な状態とその状態遷移を引き起こす条件をポチポチ定義していきます。 日本語でおk状態なので、一部分を切り取って見てみましょう。
"否認" +-------> 否認済み | | 承認待ち --------> 承認済み "承認"
_状態機械.Configure(状態.承認待ち). Permit(条件.承認, 状態.承認済み). Permit(条件.否認, 状態.否認)
承認待ち
という状態は承認
によって承認済み
という状態に、否認
によって否認済み
という状態に遷移します。
それをPermit
メソッドを用いてステートマシンに登録します。
また、状態が遷移したタイミング(もしくは今回は使っていませんが状態を離れるタイミング)で実行するデリゲートを登録するためにOnEntry
(もしくはOnExit
)を使うことができます。
Class 購買申請 Enum 状態 承認待ち 承認済み 否認 発注済み 検収済み End Enum Enum 条件 承認 否認 発注 検収 End Enum Private _品目 As String Private _価格 As Decimal Private _状態 = 状態.承認待ち Private _状態機械 As StateMachine(Of 状態, 条件) Private _否認行為 As StateMachine(Of 状態, 条件).TriggerWithParameters(Of String) Public Sub New(品目 As String, 価格 As Decimal) _品目 = 品目 _価格 = 価格 _状態機械 = New StateMachine(Of 状態, 条件)(Function() _状態, Sub(状態) _状態 = 状態) _否認行為 = _状態機械.SetTriggerParameters(Of String)(条件.否認) _状態機械.Configure(状態.承認待ち). Permit(条件.承認, 状態.承認済み). Permit(条件.否認, 状態.否認) _状態機械.Configure(状態.承認済み). OnEntry(CType(Sub() 承認時(), Action)). Permit(条件.発注, 状態.発注済み) _状態機械.Configure(状態.否認). OnEntryFrom(_否認行為, Sub(理由) 否認時(理由)) _状態機械.Configure(状態.発注済み). OnEntry(CType(Sub() 発注時(), Action)). Permit(条件.検収, 状態.検収済み) _状態機械.Configure(状態.検収済み). OnEntry(CType(Sub() 検収時(), Action)) End Sub Public Sub 承認() _状態機械.Fire(条件.承認) End Sub Private Sub 承認時() Console.WriteLine("申請者に通知") Console.WriteLine($"{_品目}の申請が承認されました") Console.WriteLine() End Sub Public Sub 否認(理由 As String) _状態機械.Fire(_否認行為, 理由) End Sub Private Sub 否認時(理由 As String) Console.WriteLine("申請者に通知") Console.WriteLine($"{_品目}の申請が以下の理由により否認されました") Console.WriteLine(理由) Console.WriteLine() End Sub Public Sub 発注() _状態機械.Fire(条件.発注) End Sub Private Sub 発注時() Console.WriteLine($"業者に{_品目}の注文をファックス") Console.WriteLine() End Sub Public Sub 検収() _状態機械.Fire(条件.検収) End Sub Private Sub 検収時() Console.WriteLine($"買掛金元帳に{_品目}({_価格}円)を追加") Console.WriteLine() End Sub End Class
使う方はこんな感じです。 不正な状態遷移を行おうとすると例外が送出されるので、アプリケーションがバグっていても安心ですね。
Module Module1 Sub Main() Dim 筆記用具購買申請 = New 購買申請("ボールペン", 100D) 筆記用具購買申請.承認() ' 申請者に通知 ' ボールペンの申請が承認されました 筆記用具購買申請.発注() ' 業者にボールペンの注文をファックス 筆記用具購買申請.検収() ' 買掛金元帳にボールペン(100円)を追加 Dim コンピュータ購買申請 = New 購買申請("HPE Integrity Superdome X", 100000000D) コンピュータ購買申請.否認("エクセルを使うのにこのスペックは必要?") ' 申請者に通知 ' HPE Integrity Superdome Xの申請が以下の理由により否認されました ' エクセルを使うのにこのスペックは必要? Try コンピュータ購買申請.発注() Catch ex As InvalidOperationException Console.WriteLine("否認された購買申請を発注することが何を意味するのか、貴様分かっているのだろうな?") End Try End Sub End Module
おわりに
割と煩雑になりがちな状態遷移をいい感じ宣言的に扱えるのがいいですね。
特定の目的を果たすための小さ目のライブラリ、大好きです。
C#でもUbuntu+Docker+Jenkins+GitBucket+MonoでCIしたい(CI編)
はじめに
前回はJenkinsとGitBucketの環境を構築しました。 今回は実際にJenkinsでビルドとテストを実行させてみましょう。
プロジェクトの作成
まずはCIをブンブン回すプロジェクトを作成します。
テストの実装及びJenkinsでのテストの実行のために
NUnit
NUnit.Runners
をNuGet経由でインストールし、リポジトリ直下のnuget
ディレクトにNuGetの実行ファイルを突っ込んでおきます。
というわけで今回使ったのがこちら。
なお、弊社はなんとなくGitHubのリポジトリが不用意に増えるのが嫌なのでGitHubに上げてるのは汎用リポジトリに突っ込んじゃってますが、今回の内容ではHelloMonoCi
が独立したリポジトリだと思って下さい。
GitBuckerリポジトリの作成
とりあえずリポジトリを作成して、先程作成したVisual Studioプロジェクトを突っ込んでおきましょう。
Jenkinsビルドジョブの作成
ジョブの作成からフリースタイル・プロジェクトのビルド
を選択します。
設定画面ではとりあえずこんな感じに設定します。
名前は自由で構いません。
Monoはスレーブノードにしか(今回の構成では)インストールされていないので、スレーブノードで実行されるように実行ノードの制限はかけておきましょう。
この辺は特にこれと言った注意点はありません。
あとでweb hookを引っ掛けるので、Build when a change is pushed to GitBucket
のチェックを入れておきましょう。
この辺は好みでどうぞ。
ビルドの最初のステップでNuGet経由でライブラリを復元します。
mono nuget/nuget.exe restore HelloMonoCi/HelloMonoCi.sln
リリースビルドを実行します。
xbuild /p:Configuration=Release HelloMonoCi/HelloMonoCi.sln
後続のテストリザルドパブリッシャー殿がNUnit3形式だと『無☆理』と言って死ぬので--result:TestResult.xml;format=nunit2
を指定します。
mono HelloMonoCi/packages/NUnit.ConsoleRunner.3.4.1/tools/nunit3-console.exe \ HelloMonoCi/HelloMonoCi.Test/HelloMonoCi.Test.csproj \ "--config=Release" "--result:TestResult.xml;format=nunit2"
テストリザルドファイルを指定します。
サーバのローカルアドレスが192.168.0.10
だとかGitBucketのユーザがurikk
だとかどうでもいい情報を全世界に垂れ流しつつ、とりあえずJenkinsはこれでokです。
GitBucker Web Hook
GitBucketのリポジトリに戻ってweb hookの設定をします。
Payload URLにはhttp://192.168.0.10:8090/gitbucket-webhook/
を指定して、あとはデフォルトで大丈夫です。
ここまで来ればもう終わり。あとはGitBuckerにプッシュするだけで優秀な執事がビルドとテストを回してくれます。
おわりに
まぁ、ふつーにWindowsでビルドサーバーを立てるかAppVeyorを使ったほうが圧倒的に楽ですね。 公開前に読み返して『これ何かの意味があるのかなぁ?』と頭を抱えています。
あと、全く関係がないのですが弊社は.NETの単体テストライブラリはMSTestしか使ったことがなく、またMSTestで事足りてたので他のライブラリを使うという発想自体がなく今までNUnitを触ったことすらなかったのですが、NUnitって良いですね。
特にTestCase
によるパラメータ化テストはMSTestには無い*1のでそのうち追加されたらいいな〜とおもいました。
おわり
*1:外部ライブラリを導入すれば同じようなことはできるっぽい
C#でもUbuntu+Docker+Jenkins+GitBucket+MonoでCIしたい(環境構築編)
はじめに
今回はLinuxサーバ環境で半ば無理やりCI環境を構築して.NETプラットフォームのアプリケーションをCIするという誰が得をするのだろうという内容の話です。
具体的に言うと、GitBucketでソースコードをホストして、GitBucketにプッシュされたときにJenkinsでビルド+テスト実行を行うというごくありふれた内容です。 こいつらをDocker上に構築します。
+-------------------+ +-------------------+ | GitBucket | -- (2)web hook --> | Jenkins |----- -->| 192.168.0.10:8080 | | 192.168.0.10:8090 | | | +-------------------+ ---------- +-------------------+<-- | | | | | | (1)push | (4)pull | | (3)order | | (6)return result | | | | | | | +-------------------+ | +-------------------+ | | ---| Windows Client | --------->| Jenkins Slave |--- | +-------------------+ | 192.168.0.10:9000 | | +-------------------+<---- (5)build & test
余談ですが、最初はいつも通り『VB.NETでも〜』という内容にしようと思ったのですが、xbuild
がvbproj
をどうしてもビルドしたくないと言うので急遽C#殿にお越しいただきました。
環境
とりあえず以下の環境を使用しました。
- Ubuntu 16.04
- Docker 1.12.1
- docker-compose 1.8.1
- Jenkins 2.19.1
- GitBucket 4.5.0
- Mono 4.6.1
- Visual Studio 2015 Community update 3
- .NET Framework 4.5.2
- ReSharper 2016.02
- NUnit 3.5.0
- Nuit.ConsoleRunner 3.4.1
セットアップ
Docker・docker-compose
公式マニュアルを以下略。
Jenkins・GitBucket・Jenkinsビルドスレーブ環境構築
まずはGitBuckt環境を構築しましょう。
こちらは特に変わったことはしてません。openjdk-8-jre-alpine
をベースにGitBucketのバイナリをダウンロードして実行しているだけです。
FROM java:openjdk-8-jre-alpine ADD https://github.com/gitbucket/gitbucket/releases/download/4.5/gitbucket.war /opt/gitbucket/gitbucket.war VOLUME /srv/gitbucket EXPOSE 8080 CMD ["java", "-jar", "/opt/gitbucket/gitbucket.war", "--gitbucket.home=/srv/gitbucket"]
Jenkinsは公式イメージを使用しています。
ただ、公式イメージにはMonoの環境が入っておらず、途中でユーザを切り替えてるせいで公式イメージをベースにMono環境を追加するという手法も使えなかったので、別途Monoをインストールしたコンテナをスレーブとして使用しています。(クソポイントその1)
ubuntu:16.04
をベースにビルド及びテスト実行のためのMono、Jenkinsスレーブデーモンを動かすためのJava、Jenkinsのスレーブとして動作させるためにDockerでは禁忌とされているSSHサーバをインストールしています。(クソポイントその2)
何処かで見たことがあると思ったあなた。素晴らしいです。先頭部分はbuildpack-deps:jessie-scm
からパクって拝借してきました。
あとはNuGetにパッケージの復元を許可するためにEnableNuGetPackageRestore
をTRUE
に設定しています。
FROM ubuntu:16.04 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ wget \ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y --no-install-recommends \ bzr \ git \ mercurial \ openssh-client \ subversion \ \ procps \ && rm -rf /var/lib/apt/lists/* RUN set -x \ && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \ && echo "deb http://download.mono-project.com/repo/debian wheezy main" | tee /etc/apt/sources.list.d/mono-xamarin.list \ # && echo "deb http://download.mono-project.com/repo/debian wheezy-libjpeg62-compat main" | tee -a /etc/apt/sources.list.d/mono-xamarin.list \ && echo "deb http://download.mono-project.com/repo/debian wheezy-apache24-compat main" | tee -a /etc/apt/sources.list.d/mono-xamarin.list \ && apt-get update \ && apt-get -y install mono-complete openjdk-8-jdk \ && rm -rf /var/lib/apt/lists/* RUN set -x \ && apt-get update \ && apt-get install -y openssh-server \ && rm -rf /var/lib/apt/lists/* RUN mkdir /var/run/sshd RUN echo 'root:screencast' | chpasswd RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config # SSH login fix. Otherwise user is kicked off after login RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd ENV NOTVISIBLE "in users profile" RUN echo "export VISIBLE=now" >> /etc/profile ENV EnableNuGetPackageRestore TRUE EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"]
あとはこれらのコンテナを上げたり下げたりビルドする為のdocker-compose.yml
をでっち上げれば構築は完了です。
version: '2' services: gitbucket-websrv: build: './gitbucket' ports: - '8080:8080' volumes: - '/srv/mono-ci/gitbucket:/srv/gitbucket' jenkins: image: 'jenkins:2.19.1' ports: - '8090:8080' - '50000:50000' volumes: - '/srv/mono-ci/jenkins:/var/jenkins_home' jenkins-slave-mono: build: './jenkins-slave-mono' ports: - '9000:22'
ちなみにこれらの環境を構築するだけなら10秒*1で終わります。
git clone https://github.com/jyuch/mono-ci.git cd mono-ci docker-compose up
Jenkins・GitBucket初期設定
GitBucket
GitBucketにroot:root
でログインし、自身の作業用アカウントとJenkins用のアカウントを作成しましょう。
Jenkinsアカウントはコントリビューターになれれば良いので管理者でなくても大丈夫です。
Jenkins
アカウント登録後のプラグイン選択画面ではとりあえずおすすめをインストールしておきましよう。
その後、管理画面より以下のプラグインをインストールします。
- GitBucket Plugin
- NUnit plugin
その後はJenkinsの管理
> ノードの管理
よりスレーブノードを登録します。
今回の構成ですとSSHポートは9000なのでそこだけは注意が必要です。
最後にCSRF対策
のチェックボックスを外します。セキュリティを切るのはいささか気が引けますがこの作業をしないとGitBucketからのweb hookが通らないのでまぁ仕方ないのかなと自分の中で折り合いを付けながらチェックボックスを外すか別のいい感じの方法を探して下さい。いい感じの方法がありましたら教えてください。
ここまでで初期セットアップは完了です。お疲れ様でした。
*1:イメージのプル・ビルド時間を除く