受注管理機能を支える技術 〜 VueCompositionAPIとGraphQLとAtomicDesignとScopedStyle〜

こんにちは、 @mugi_uno です。

少し前に背骨の手術を受けたら身長が伸びました。

🎉 受注管理機能をリリースしました

2020/12/10に、Misocaに新しく「受注管理機能」をリリースしました。

www.misoca.jp

f:id:mugi1:20201217165947p:plain

いままでは、請求書・見積書・納品書といった単位でのステータス管理が主でしたが、新たに追加された受注管理機能を使うことで、案件単位でステータスを管理しつつ、各文書への変換も簡単に行えるようになりました。

そして、同時にこの受注管理機能は、開発面においても様々な新しい技術面でのトライもありました。

  • Vue.js & Vue Composition API
  • GraphQL
  • Apollo Client & Vue Apollo
  • Atomic Design
  • Scoped Style

今回は、これらについてどういった対応をしたのかと、リリース後にふりかえってみてそれぞれよかった点、大変だった点についてのお話です。

前提 / 以前から利用している技術要素と受注管理機能の基本構成

受注管理に限らないMisoca全体の代表的な技術要素として、次のものを以前から利用しています。

  • バックエンド: Ruby on Rails
  • フロントエンド: TypeScript & Vue 2.x
  • API: REST
  • デザイン: SCSS (*.scss ファイルに記述)

新たに作成した受注管理機能はSPAで構成されており、次のような機能を実現しています。

  • ステータスの追加・変更・削除
  • 受注情報の新規作成・複製
  • 受注情報のステータス変更
  • 履歴の確認
  • コメントの追加
  • 検索

Vue.js (Vue Composition API)

受注管理機能の開発着手時点で、すでに Vue 3.x 系のリリースも見えている状態で、当時の選択肢としては2つ考えられました。

  • 従来の Vue 2.x 系の記法のまま実装する
  • Vue Composition API を利用する

Vue 3.x 以降も従来の記法はサポートされるので、無理に新しい記法とする必要はなかったのですが、「どうせ近い将来にすべて書き換えるなら、最初から Vue3フレンドリーな形で実装していこう」と、Vue Composition API を利用した形を主軸に実装しました。

github.com

結果としては、2020年9月時点で Vue3 が正式リリースされました。MisocaではIE対応の必要性から未導入ですが、後々必要となるマイグレーションを考えると、良い決断だったかなと思います。

👍 良かったこと

共通処理を Vue Composion API を使って簡単に切り出せるのは使い勝手が良かったです。

例として「新規作成と編集と複製って、ロジックがほぼ同じなんだけどちょっとだけ違うんだよな〜」といった際に、共通ロジックを useEditForm.ts として切り出す、といったことが簡単にできます。

また、Composition API は TypeScript との親和性が高く、機能追加・変更時も安全に手を加えていける点も良かったです。

🙈 大変だったこと

開発までの慣れ

いままでは Vue 2.x + Class Component で開発していたため、書き方が大きく変わる Composition API に開発メンバーが慣れるまでに少し時間がかかりました。ただ最初でこそ大変でしたが、慣れてくると「こっちのほうが書きやすい」という意見が多くなりました。

なお、開発と並行して @kawamataryoの主催で Vue Composition API のドキュメントを対象にした輪読会も開催されていました。そちらも、レビュアー・レビュイー双方の観点で良い効果があったと思います。

setup関数の肥大化

Composition API は setup 関数が肥大化しがちで、意識せずに処理を詰め込んでいくと、全体的に何をやってるか見通しが悪くなるのが課題でした。対策として、「大きくなりそうなら、処理は意味のある単位でコンポーネント内のローカル関数に切り出して、setup は小さく保つ」といったルールを共有していました。

<script lang="ts">

...

const useA = () => {
   // Aに関連する処理
}

const useB = () => {
   // Bに関連する処理
}

const useC = () => {
   // Cに関連する処理
}

export default defineComponent({
  setup() {
    return {
      ...useA(),
      ...useB(),
      ...useC()
    }
  }
})
</script>

関連ライブラリの影響

Vue Router や Vue Apollo などで一部機能が未対応だったり、Vue Composition API 側の変更の煽りを受けて挙動が壊れるというのを何度か踏みました。こればかりは新しい技術を突っ込んだ以上逃げられないものですので、ライブラリ側に修正パッチを送るなどして、わりと泥臭く対応してました。

GraphQL

SPAなのでデータの取得・更新はAPI経由ですが、すべてGraphQLで実装しています。

バックエンド(Rails)では、GraphQL Ruby でエンドポイントの実装とスキーマファイル出力を行い、フロントエンドでは、スキーマファイルを元に GraphQL Code Generator を利用して TypeScript の型定義ファイルを生成しています。

GraphQLを導入した背景としては、すでにMisocaでメイン利用されていた TypeScript との親和性が非常に高いことや、将来的にモバイルなど他の領域からの利用を考えた場合に柔軟に対応できる点が挙げられます。(※ただ正直なところ、エンジニアチームとして新しい技術にトライしたかったというのもあったかなと思います。)

👍 良かったこと

GraphQL による開発体験はとても良く、特に GraphQL Code Generator 経由で生成した型定義をフロントで利用できる点が非常に快適でした。

Misocaでは開発中の仕様変更も柔軟に受け入れていくスタイルですので、DB定義も含めてガンガン変更が入ります。その際に、GraphQL スキーマの変更から連動して型定義も変更されるため、ビルド時の静的検査の時点で検知でき、簡単かつ安全に対応していくことができました。

「バックエンドが変わったのにフロントエンドの変更が漏れてる」といったこともほぼ無く、品質面でも大きなメリットがあったように感じます。

ふりかえりなどでも GraphQL 超便利といった声が多数聞けました。導入して良かったです。

🙈 大変だったこと

ついうっかり N+1 を踏んでしまうことが何度かありました。「履歴データが多くなってくると一覧がタイムアウトします!!」みたいな感じです。

根本的な対処としては、Dataloader を利用してバッチ的にデータを取得する方法などが例として挙げられるかと思います。

しかし、Misocaの場合は発行されるGraphQLクエリがある程度は予測可能でしたので、単純に ActiveRecord 側で includes を足して対処しました。それでも、可能な限り無駄なくincludes を足すため、GraphQL クエリに基づいて自動的に includes に渡す引数を生成するヘルパーを自作しました。以下はヘルパーの利用イメージです。

RELATION_MAP = {
  comments: {
    user: {}
  },
  histories: {},
  items: {},
  status: {}
}.freeze

# クエリ内容とマッピング定義から includes に渡すべきパラメータを得る
includes_params = IncludesHelper.create_includes(
  lookahead, # GraphQLRubyが提供するクエリ検査用インタフェース
  RELATION_MAP
)

orders.includes(includes_params)

これにより、あるクエリ内で comments だけ取得している場合は comments のみが includes に含まれ、別クエリで itemsstatus を取得する場合には itemsstatus のみが includes に含まれるようになっています。

状態管理

Vue & TypeScript の組み合わせで迷いやすいのが状態管理の方法かなと思います。

今回のケースでは GraphQL のために Apollo Client を利用しており、基本的なデータについては Apollo Client のキャッシュ機構を利用して状態管理の代替としています。

同時に Vue Apollo も組み合わせているので、たとえば

  1. ユーザーが何か入力する
  2. 1 を Vue のリアクティブシステムで検知して GraphQL クエリを自動発行
  3. Apollo Client のキャッシュが更新
  4. 同一 GraphQL クエリに依存する箇所すべてが自動的に再描画

といった挙動もわりと簡単に実現できます。

ただ、画面上のすべての情報が Apollo Client で集約できるかというとそんなこともなく、UI操作上一時的にどこかに保持したい かつ 様々なコンポーネントから参照が必要になる値も存在します。モーダルの表示状態やドラッグ&ドロップの一時的なデータ保持などが代表例です。

Apollo Client に local stateとして管理させる方法もありましたが、シンプルさ重視で Vue Composition API の reactive を利用した簡易的なストアを作成しました。

👍 良かったこと

リフレッシュや多重リクエスト制御が自然といい感じになる

複数コンポーネントが Apollo Client のクエリ・ミューテーションと連動して自動的に再描画されていくのはなかなか便利です。

独自でクエリ発行→再描画を制御している場合、適宜処理の影響を受ける箇所でリフレッシュが必要になりますが、Vue Apollo がそのあたりを吸収してくれて、とても簡単に実装できました。

また、データが必要な箇所ではあまり深く考えずにクエリを実行しても、Apollo Client側で多重リクエストは自動的に抑制して捌いてくれます。(※設定によります)

開発時の「こうやって動いてくれたらいいのにな〜」という思いを、ほどよく汲みとって動作してくれるのは体験として良かったです。

reactive での簡易ストアは楽だった

reactive での簡易的な状態管理を作成しましたが、単純なオブジェクトを変更・参照していく形なのでとてもシンプルでした。TypeScriptとの相性も良かったです。最低限のガードとして参照側は Readonly 型としていたため、意図しないストア書き換えによる事故も発生しませんでした。

ただし、これは常にベストな選択肢では無さそうにも思います。一定以上の規模感になってくると、独自実装を増やすよりも、Vuexなどを利用して王道に沿った形にしていくほうが、長期的なメンテナンスコストは低くなるかもしれません。

🙈 大変だったこと

テストが書きづらい

Vue Test Utilsを利用したコンポーネントのテストを書く際に、Apollo Client のキャッシュに依存したテストが非常に厄介でした。

graphql-toolsを使うことでモック化自体は容易ですが、キャッシュを前提とした挙動のテストを書く場合、セットアップ段階でキャッシュを生成する必要があります。

ただ、実際にテストを実行していると、「キャッシュが無くてテストに落ちている」というのがエラーから気付くのがなかなか難しく、長時間ハマってしまうことも何度かありました。

暗黙的な挙動が増える&対処が複雑化しがち

Vue Apollo を組み合わせることで「Vueでの値の変更や別の箇所でのクエリ結果に連動して自動的に再描画してくれる」というのは、便利である反面、暗黙的な挙動となる箇所が増えていきます。また、クエリ発行のタイミング制御も難しくなり、画面上で複雑な操作・更新が発生する場合、それを実現するためのコードも同時に複雑になっていく傾向がありました。

Vue Apolloを利用せず、クエリ結果をストアに記録して明示的に制御するように書き換えるほうがシンプルになるかもしれないな..と思っている部分もありますが、もちろん恩恵を受けている箇所もあるため、今後様子を見ての判断になりそうです。

コンポーネント設計&デザイン

Atomic Design

コンポーネント設計は Atomic Design を採用しました。これは、全力で死守するものというより「判断基準を最初に設けたかった」というのが目的です。

以前からMisocaのフロントエンド開発時には、コンポーネントをどういった粒度で切るべきか / どこに配備すべきか、といった点が曖昧で迷ってしまい、フロントエンドチームが相談を受けることも度々ありました。

受注管理の機能開発時も同じ状況に陥ることが予想できたため、予め何らかの基準を設けるのが必要と考えました。

Scoped Style (Vue)

CSSは Vue.js の Scoped Style で書いていきました。

もともとMisocaのデザインは *.scss ファイル内に SCSS で書かれていますが、デザイナーの皆様の尽力もあり、いまではかなり整理されて、ファイル単位での影響範囲が明確になっていました。

tech.misoca.jp

さらに将来的な展望として Scoped CSS を利用した Vueコンポーネント化もあったため、まず手始めに今回の受注管理機能で実践してみた形です。

開発時のルールとしては、外部のCSSとの干渉や無駄な重複を避けるため、

  • Scoped Style を適用するためのクラスにはすべて _ を prefix として付与する
  • 色やフォントサイズ定義は既存の定義をimportして利用する

などがありました。

👍 良かったこと

迷いにくかった

「Atomic Design ベースでいきましょう」と合意をとったことで、当初の目論見どおり、コンポーネントの粒度・配備で迷うことは少なかったです。また、あくまでも判断基準のためで良くも悪くも緩い導入だったので、ルールに縛られすぎずに程よい付き合いができたのも良かった点でした。

CSSの変更が楽だった

Scoped Style ではコンポーネントに対応するスタイルは、当然ではありますが、そのコンポーネントの中に書いてあります。

整理されていたとはいえ、外部化されているCSSを触るときは「これってどこか他を壊しちゃうんじゃ…」という不安が心の片隅にありましたが、Scoped Style のように影響範囲がそのファイル内で閉じているのはなかなかに快適で、修正時の負担がかなり軽減されたように感じます。

🙈 大変だったこと

Scoped Style のスタイルの親子継承が厄介

影響範囲がファイル内で閉じていると書きましたが、実は Vue の Scoped Style には一部例外があり、子コンポーネントのルート要素だけは親要素のCSSを引き継いでしまうケースがあります。

スコープ付き CSS · vue-loader

たとえば、親コンポーネント側で _foo というクラスに対してスタイリングした場合に、子コンポーネントのルート要素に _foo というクラスを付与すると、親コンポーネント側で定義したスタイルが適用されてしまいます。

レイアウト用のスタイリング時に利用できる仕組みのようですが、どちらかというと意図しないCSS適用を引き起こすケースのほうが多かったです。

調べた限りではこれを無効化する仕組みは無いようで、コンポーネントのルート要素に Scoped Style 用のクラスを付与する場合には、できるだけ他で利用されにくい冗長な名前をつけて回避していました。

まとめ

今回は新しくリリースされた受注管理機能で利用した技術の話でした。

今後もずっと使い続けていけそうなノウハウも得られましたし、逆に「もっとこうすればよかったなぁ」という反省も勿論ありました。

いずれにしても試してみない限りは得られることのなかった知見かなと思うので、今後も(将来の開発者に迷惑をかけない範囲で)新しいことにチャレンジしていけるといいな〜と思います。

AndroidアプリにViewModelを導入しました

はじめまして。Misocaモバイルチームのtijinsです。

この記事は、弥生アドベントカレンダー14日目の記事です。

MVPからMVVMへ

Android版MisocaはModel-View-Presenter構造で作られていたのですが、Model-View-ViewModel構造へのリファクタリングが完了しました。

コードがスッキリしたので、リファクタリングの内容を紹介します。

MVPパターンとMVVMパターン

Model-View-Presenter(MVPパターン)

MVPパターンでは、PresenterがViewの参照(intrefaceで)を保持し、結果をViewに表示します。

Viewを直接参照せずにInterfaceにしておくことで、PresenterからViewの依存を排除しています。

classDiagram
  class PresenterResult
  class View
  class Presenter
  class Model


  <<interface>> PresenterResult
  View --|> PresenterResult
  View --> Presenter: 読み込み・新規作成等の操作
  Presenter --> PresenterResult : 結果の表示
  Presenter --> Model

Model-View-ViewModel(MVVMパターン)

MVVMパターンでは、ViewModelからViewの参照は不要です。

表示用のデータはViewModelが保持しており、ViewがViewModel上のデータを監視(Observe)して表示します。

classDiagram
  class View
  class ViewModel
  class Model

  View --> ViewModel: ViewModel上のデータを監視(変更があれば表示を更新する)
  ViewModel --> Model:操作結果により、ViewModel上のデータを更新する

ViewModel化のメリット

ViewModelにも色々あるみたいなのですが、ここでのViewModelはデータを保持できるPresenterといった感じです。

PresenterからViewの参照を排除できる

ViewModel化前は、Presenterに非同期処理完了のコールバックとして、Viewの参照を(interfaceとしてですが)渡していました。

ViewModelを利用すると、ViewModelからViewへの参照は不要になります。

AndroidアプリでのViewModelのメリット

  • FragmentでもActivityライフサイクルでのデータ保持が可能

Fragmentにデータを保持していると、画面の回転等でFragmentが破棄された際に、データの読み込みが必要でした。

ActivityライフサイクルのViewModelを利用する事で、Fragmentが破棄されても、データの引き継ぎが可能になります。

  • ライフサイクル管理の単純化

Presenterが行う非同期処理の結果をFragmentで表示する場合、先にFragmentが破棄されていると、クラッシュしてしまう問題がありました。

ViewModelのライフサイクルはFragmentから分離されている為、Fragmentの状態を気にせずに、非同期処理が可能です。

リファクタリング前のコード

最初にリファクタリング前のコードです。

Viewを直接参照しないのは、PresenterからViewの依存を排除する為です。

こうしておくと、テスト用のダミークラスに差し替えたりできます。

classDiagram
  class ItemsResult{
    onItemLoaded(items)
    onFailed(message)
  }
  
  class ItemsFragment{
    items:List<Item>
  }
  class ItemsPresenter{
    loadItems(page)
  }
  class UseCase
  class Repository

  <<interface>> ItemsResult
  ItemsFragment --|> ItemsResult
  ItemsFragment --> ItemsPresenter: itemsの読み込み操作
  ItemsPresenter --> ItemsResult : 結果(items)
  ItemsPresenter --> UseCase
  UseCase --> Repository

ItemsViewInterface.kt

interface ItemsResult
{
    fun onItemsLoaded(items:List<Item>)
    fun onFailed(message:String)
}

ItemsPresenter.kt

class ItemsPresenter(
  private val context:Context,
  private val coroutineScope: CoroutineScope,
  private val view: ItemsResult) {

    private val useCase = ItemsUseCase()

    fun loadItems(page:Int) {
        coroutineScope.launch{
            try {
                val result = useCase.loadItems(context, page)
                view.onLoadItems(result)
            } catch (ex: Exception) {
                view.onFailed(ex.message)
            }
        }
    }
}

ItemsFragment.kt

class ItemsFragment:Fragment,ItemsResult
{
    // 請求書一覧のアダプターです
    private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>?
    private var presenter: ItemsPresenter?
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        presenter = ItemsPresenter(requireContext(), viewLifecycleOwner.lifecycleScope, this)
        presenter?.loadItems(1)
    }
    
    override fun onItemsLoaded(items:List<Item>){
        adapter?.submitList(items)
    }
    
    override fun onFailed(message: String){
        // エラー表示する
    }
}

リファクタリング後のコード

ViewModelの実装

ViewModelからFragmentへの参照が無くなり、すっきりしていると思います。

classDiagram
  class ItemsViewModel{
    - itemsBuffer:List<Item>
    + items:MutableLiveData<List<Item>>
    + networkState:MutableLiveData<NetworkState>
    laodItems(page)
  }

  class Fragment
  class UseCase
  class Repository

  Fragment --> ItemsViewModel : items<LiveData>をobserveして、アイテム一覧の変更を監視
  ItemsViewModel --> UseCase
  UseCase --> Repository

ItemsViewModel.kt

MisocaのAndroid版では、APIとの通信にContextが必要な為、AndroidViewModelを継承しています。

AndroidViewModelを継承することで、ApplicationContextをgetApplication()で参照可能になります。

ただし、ApplicationContextには言語設定や画面向きの変更が反映されない為、ViewModel内でUIに関する処理を行わないよう注意が必要です。

class ItemsViewModel(context: Context) : AndroidViewModel(context.applicationContext as Application){
    // 通信中状態、エラーの通知用のobserve可能なプロパティ
    val networkState = MutableLiveData<NetworkState>()    
    // 表示するデータの実体(LiveData経由でFragmentに渡す)
    private val itemsBuffer = ArrayList<Item>()
    // observe可能なプロパティ
    val items = MutableLiveData<List<Item>>()
    
    // データの一覧を読み込みます
    fun loadItems(page: Int) {
        viewModelScope.launch {
            try {
                networkState.postValue(NetworkState.LOADING)
                
                // loadItemsはDispatchers.IOで動作するsuspend functionです。
                val result = useCase.loadItems(getApplication(), page)
                itemsBuffer.addAll(result)
                
                // observeしているViewに更新を通知する
                items.postValue(itemsBuffer)
                networkState.postValue(NetworkState.LOADED)
            } catch (ex: Exception) {
                // エラーメッセージを表示
                networkState.postValue(NetworkState.error(ex.message))
            }
        }
    }
}

NetworkStateの実装は、下記のコードを参考にしました。

https://github.com/android/architecture-components-samples/tree/master/PagingWithNetworkSample

ViewModel上のデータ変更を監視する

ViewModelをFragmentから使う場合は、fragment-ktxにあるExtension(viewModels,activityViewModels)を使用すると便利です。

build.gradle

dependencies {
    implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"
}

ItemsFragment.kt

class ItemsFragment:Fragment
{
    // fragment-ktxのactivityViewModelsでviewModelを取得します
    private val viewModel: ItemsViewModel by activityViewModels()
    // 請求書一覧のアダプターです
    private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>?

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)

     observeViewModel()
     // itemsの読み込みを開始します
     viewModel.loadItems(1)
   }

    private fun observeViewModel() {
        // itemsの更新を監視します
        viewModel.items.observe(viewLifecycleOwner) { items ->
            // List<Item>をそのまま通知しても、DiffUtilが効率的に再描画してくれるので気にしない
            adapter?.submitList(items)
        }
        
        // 通信中状態の更新を監視します
        viewModel.networkState.observe(viewLifecycleOwner) {
            when (it.status) {
                Status.RUNNING ->{
                    // プログレスバーを表示
                }
                Status.FAILED -> {
                    // エラー表示
                }
                Status.Success -> {
                    // プログレスバーを消す
                }
            }
        }
    }
}

おまけ

この画面は2つのタブがそれぞれFragmentになっているのですが、ViewModelを通じて2つのタブがデータを共有しています。

タブ間(Fragment間)でのデータの移動も、ViewModelを使用する事で、簡単に実現できています。

まとめ

Viewの操作が不要になって、スッキリしました

宣伝

Misoca 開発チームでは、モバイルアプリエンジニア(iOS, Android)を募集しています。

www.wantedly.com

君のプログラミング言語は輝いているか

こんにちは、Misoca開発チームの黒曜(@kokuyouwind)です。

そしてこの記事は弥生 Advent Calendar 2020の 11日目の記事です。

昨日の担当はkosappiさんのCodePipelineのステージ間で変数を受け渡しするでした。

プログラミング言語について

Misoca開発チームでは主にサーバーサイド側言語としてRuby、クライアントサイド言語としてTypeScriptを利用しています。

一方で弥生製品開発チームは主にC#を利用しており、最近ではTypeScriptやGo言語も一部で利用しています。

そうなってくると、気になるのは「今後新しいプロダクトを作るとしたら、プログラミング言語は何にするべきなのか?」ということです。

今回はそれぞれの言語の特徴や向いている用途について、ざっくりまとめていきたいと思います。なお極力資料を探しながら正確に書こうと思いますが、個人的な印象やイメージが入ってしまう部分もあるかと思いますのでご了承ください。

なお誕生時期や特徴の主な出典はプログラミング言語図鑑に拠ります。

プログラミング言語図鑑

プログラミング言語図鑑

  • 作者:増井敏克
  • 発売日: 2018/04/13
  • メディア: Kindle版

目次

Ruby

言語の特徴

Misoca開発チームがバックエンド開発に使っている言語です。日本発で個人開発から始まったという、割とレアな出自を持っています。また公開が1995年と、実はC#より長い歴史を持つ言語です。

Rubyの特徴として、C++に代表されるクラスベースのオブジェクト指向よりも、Smalltalkに代表されるメッセージベースのオブジェクト指向を強く意識していることが挙げられます。

多くの言語では、数やそれ以外のプリミティブな型はオブジェクトではありません。 ですが、RubyはSmalltalkの影響を受け、すべての型がメソッドやインスタンス変数を与えられるようになっています。 これがRubyが使いやすい理由の一つです。 Ruby公式ページ - Rubyとは より

この特徴と、メッセージ呼び出しやブロック渡しでの柔軟な構文によって、Rubyでは自由度の高い言語内DSLを定義することができます。

例えばRailsの routes.rb ファイルは以下のように書くことができますが、これは妥当なRubyコードになっています。*1

Rails.application.routes.draw do
  resources :brands, only: [:index, :show] do
    resources :products, only: [:index, :show]
  end

  resource :basket, only: [:show, :update, :destroy]

  resolve("Basket") { route_for(:basket) }
end

他にも関数型プログラミングの要素が取り入れられていたり、オープンクラスとオーバーライドによる柔軟なクラス拡張が行えたりなど、とにかく「プログラマが自然に書きやすい」ことを重視したまつもとひろゆき氏の思想が伺えます。

向いている用途

最も普及している用途はRuby on RailsによるWebプログラミングで、特にスタートアップなどの小規模かつ迅速な開発で強みを発揮します。

他に言語内DSLの特性を活かしてVagrantChefなどの設定ファイルでの利用が有名です。またサーバ内で実行するバッチ処理などのスクリプト用途でも手軽に書きやすく重宝します。

一方で、自由度や動的な型システムによって大規模開発では混乱を生みやすい言語だといえるでしょう。また実行速度の面では遅い部類に入るため、処理速度が重要な場合はあまり向いていないと言えます。

なお12月25日(クリスマス!)にリリースされる予定のRuby 3.0*2では静的型検査システムの導入や実行速度の改善などが行われているため、これらの懸念点が多少緩和されるのではないかと期待しています。

C#

言語の特徴

弥生製品チームが主に利用している言語です。マイクロソフトが2000年に公開した、汎用目的のオブジェクト指向言語になっています。

一般に使われ始めたのは、Visual Studio .NETに組み込まれた2002年からになりそうです。それまでのネイティブビルドがターゲットだったVisual Studioシリーズ(6.0まで)から、.NET FrameworkをターゲットとしたVisual Studio .NETに移行するタイミングで追加されただけあり、.NET Frameworkの標準的な言語という扱いを受けています。*3

C++の構文をベースにしつつも、低レベルのメモリアクセスは基本的に制限したりガベージコレクションが標準で利用されるなど、自分の足を撃ち抜くことがないようになっています。型システムは.NETの共通型システムに依りますが、おおむねclassやinterfaceを用いたクラスベースのオブジェクト指向といえるでしょう。

またC# 3.0で追加されたLINQ用のクエリ式やC# 7.0で追加された関数型プログラミング由来のパターンマッチ機能など、積極的に様々な機能が導入されています。

特にクエリ式によって、データベースに対するクエリ発行を以下のように言語内で記述できる点は特筆すべきでしょう。*4

IEnumerable<string> highScoresQuery2 =
    from score in scores
    where score > 80
    orderby score descending
    select $"The score is {score}";

ほぼSQLと同じ内容を言語内で記述できるため、構文検査や静的な型検査もコンパイル時に行えるのは強みといえます。*5

向いている用途

.NET Frameworkの主力言語であるため、Windowsデスクトップ開発においては最有力候補と言えます。またUnityでのスクリプティングにおいてもC#が採用されています。

.NET ASPなどを利用したWebプログラミングも可能で、大企業やSIerなどの大規模プロジェクトで利用されているように思います。

一方で、やや文法が古くボイラープレートコードが多くなること・開発環境が実質的にVisual Studioに制限され自由度が少ないこと・稼働環境にWindowsサーバライセンスが必要になることなど諸々の問題から、スタートアップなど小規模なWebアプリケーションでの採用は稀という印象です。

またWindowsや.NET Framework自体が重厚なため、昨今のコンテナ化・マイクロサービス化の文脈ではコンテナサイズやメモリ使用量が膨らみやすいという点で他言語に軍配が上がりやすいと言えるでしょう。

こうした状況はMicrosoftも把握しているようで、.NET Coreのマルチプラットフォーム対応や.NET 5での単一の実行可能バイナリ生成など、着々と改善を重ねています。BlazorによるWebAssemblyアプリ作成などもあり、今後の展開によっては小規模Webサービスやマイクロサービス用途でも選択肢に挙がってくるかもしれません。

Go

言語の特徴

弥生基盤チームが一部で利用している言語です。Googleが2009年に公開した汎用目的のプログラミング言語です。

Goの基本的な言語仕様は単純で、個人的にはBetter Cという印象を受けます。ただしinterfaceを利用したオブジェクト指向機能と、GoRoutineを用いた並列処理機構の2つは非常に特徴的です。

Goのオブジェクト指向ではクラス定義を行うことはできず、どんなメソッドを持つかのinterfaceのみを定義することができます。そしてある型がinterfaceのメソッドを全て定義していれば、その型はinterfaceを実装しているとみなされます。

例えば、sort.InterfaceLen() int, Less(i, j int) bool, Swap(i, j int) の3つのメソッドを要求するため、これらを定義した型であればsort.Sortに渡して整列することができます。いわゆる構造的部分型ですね。Rubyでよく言われるダックタイピングのような振る舞いを記述できますが、コンパイル時にインターフェイスを満たしているか静的に検査できる点が優れています。

GoRoutineはマイクロスレッドを立ち上げるための言語機構です。Erlangのマイクロスレッドに近いですが、各プロセスがメッセージボックスを持つアクターモデルではなく、チャネルを通じて同期的に通信を行うCSPスタイルとなっています。

また標準でコードフォーマッターや単体テストライブラリなど組み込みのエコシステムが整備されており、コンパイルすると単一実行ファイルが生成されるため、コンテナベースのCI/CDといったモダンな開発プロセスに適応しやすくなっています。

向いている用途

Googleが自社向けに設計しただけあって、コンテナを利用したマイクロサービスシステムでは最初に名前が挙がる言語でしょう。ユーザが直接触れるWebアプリよりも、マイクロサービス内部のAPIサービスで特に強みを発揮するイメージです。また静的型システムを持ち並列処理機構も言語標準で持つため、Dockerなどミドルウェアの開発でも積極的に採用されています。

一般的なWebアプリでの利用も十分可能だと思いますが、Webフレームワークはデファクトが固まっておらず、フロントエンドも通してカバーできるTypeScriptに軍配が挙がっている印象です。もっとも、標準ライブラリのみで簡単なWebアプリなら作成できてしまうため、Webフレームワークの必要性が低いという事情もありそうです。

TypeScript

言語の特徴

Misoca開発チームがフロントエンド開発で利用している言語です。また弥生製品チームでも一部で利用されています。マイクロソフトが2012年に公開した、いわゆるAltJS言語の一種です。

JavaScriptに型検査システムやスコープ制限などの機能を追加した言語で、JavaScirptの厳密なスーパーセットになっているため既存のJavaScriptソースをそのまま利用でき移行が容易という点で他のAltJS言語と一線を画しています。

また型システムも非常に特徴的で、典型的なプリミティブ型・オブジェクト型などに留まらず、リテラルの値に依存するリテラル型や型レベル関数など意欲的な仕様が取り入れられています。

例えば、以下のように方位を表す "North", "East", "South", "West"のいずれかのみを取る型 CardinalDirection を定義することで、 move にそれ以外の文字列が渡されないことを型レベルで保証できます。*6

type CardinalDirection =
    | "North"
    | "East"
    | "South"
    | "West";

function move(distance: number, direction: CardinalDirection) {
    // ...
}

move(1,"North"); // Okay
move(1,"Nurth"); // Error!

一方で、型注釈を間違うと容易に型の整合性を崩せるなど健全性は諦めている部分があり、型検査を実用的なレベルで妥協するという割り切りが感じられる言語です。

向いている用途

現状のフロントエンド開発ではデファクトスタンダードと言えるでしょう。またサーバーサイドJavaScriptエンジンであるNode.jsを組み合わせることでサーバサイド開発でも利用することができます。

特にフロントエンド・サーバサイドを跨いだJavaScriptフレームワークであるNext.jsNuxt.jsなどは迅速な開発が必要なスタートアップなどで非常に人気があり、TypeScriptと組み合わせて採用するケースが増えているように思います。

静的型検査があるため開発規模が大きくなってもある程度はスケールしますが、コンパイルターゲットがJavaScriptであり速度面では他言語に劣ること、標準で利用されるnode_modulesが肥大化しやすくコンテナイメージが膨らみやすいことなどから、大規模開発では他の言語に分があるといえそうです。

とはいえTypeScript界隈はコミュニティが非常に活発であり、新たなツールでこうした課題が改善されていく可能性は十分ありそうに思えます。

その他の言語

他に、個人的に最近使われていると感じたり気になっている言語は以下のとおりです。

  • Swift : iOS開発開発のデファクトスタンダード。サーバーサイドSwiftという選択肢もあるが、事例は少なそうに感じる
  • Kotlin : Androidアプリ開発のデファクトスタンダード。サーバーサイドKotlinという選択肢もあるが、やはり事例は少なそうに感じる
  • Scala : オブジェクト指向と関数型プログラミングをいい感じに組み合わせたJVM言語。書ける人が書くと非常に生産性が高いが、設計方針が統一されていないとパラダイムが混ざってカオスになる。あとimplicit parameterを多様するとコンパイルがどんどん重くなる
  • Rust : 所有権管理が厳格なため、低レイヤー処理を安全に記述できる言語。ミドルウェアやドライバーなどハードウェア寄りのソフトウェアで特に強みを発揮するイメージ
  • F# : .NETに対応した関数型言語で、OCamlの文法をベースにしている。型プロバイダなど表現力が高いが、Scala以上に関数型寄りのパラダイムを持つためOCaml・Haskell辺りの経験がないと学習コストが高い

まとめ

いろいろなプログラミング言語がありますが、それぞれに設計者の思想や目的がある以上、用途に合わせてプログラミング言語を選ぶことで特徴を活かして輝かせたいですね。(雑なタイトル回収)*7

弥生 Advent Calendar 2020、明日の担当はmasuda_Mtenさんです。お楽しみに!

*1:routes.rbのコードはRailsガイド - Rails のルーティングから引用

*2:現時点ではRuby 3.0 preview 2がリリースされており、試すことができます。

*3:J#? 知らない子ですね…

*4:コードはC# - クエリ式から引用

*5:LINQやクエリ式はVB.NETやF#でも利用できるので、.NET系の言語共通の強みと言ったほうが正確かもしれません。

*6:コードはTypeScript Deep Dive 日本語版 - リテラル型から引用

*7:蛇足ですが、タイトルの元ネタは「君の青春は輝いているか」です。

CodePipelineのステージ間で変数を受け渡しする

この記事は弥生 Advent Calendar 2020 の10日目の記事です。

こんにちは。@kosappi です。最近は SRE としてインフラの改善に取り組んでいます。

Misoca 開発チームでは AWS を利用してインフラを構築しています。 AWS CodePipeline で使える変数の受け渡しが便利だったので、今回はこれを紹介します。

CodePipeline の基本機能について知りたい方は AWS による CodePipeline の紹介をご覧ください。

AWS CodePipeline(継続的デリバリーを使用したソフトウェアのリリース)

CodePipeline における変数の受け渡し

CodePipeline では各アクションの成果物をアーティファクトでやりとりできます。 近頃は、これに加えて「変数」という成果物も扱えるようになっています。

参考: AWS CodePipeline で実行中のアクション間での変数受け渡しが可能に

なお、AWS から提供されている変数についての資料は下記の2つです。

まとめ

  • アクションは変数を作成することができる
    • アクションによっては作成できない場合もある(後述)
  • 変数を作成するには名前空間を指定する必要がある
    • アクション毎に名前空間が必要
    • 名前空間が指定されていない場合は参照することができない
  • 作成された変数は #{NAMESPACE.VARIABLE_NAME} で参照できる

変数を作成することができるアクション

下記のアクションではユーザが変数を作ることができます。

  • CodeBuild
  • AWS CloudFormation
  • Lambda

上記以外のアクションではユーザは新たに変数を作ることはできず、定義済み変数のみ利用できます。 どのような変数が利用可能かは公式のドキュメントに一覧で載っています。

パイプラインアクションで使用できる変数

なお、定義済み変数を利用する場合でも名前空間の指定は必要です。

変数を下流ステージに提供する(CodeBuildの場合)

それでは、実際に CodeBuild で変数を下流ステージに提供していきましょう。 全体のイメージはこんな感じです。

f:id:rkosaka:20201207172506p:plain
パイプライン全体図

名前空間を指定する

名前空間を指定します。 コンソールからだと、ここにあります。

f:id:rkosaka:20201207173137p:plain
CodeBuild設定画面

CLI や terraform 等からもできますが、ここでは割愛します。 今回は CodeBuildVariables という名前空間にします。

buildspec 内で変数を作成する

buildspec 内で変数を作成するには env/exported-variables を利用します。 env では他にも変数を指定できますが、下流ステージから参照できるのは exported-variables のみです。

今回は BUILT_IMAGE_TAG という変数を作ります。 ここにビルドする際のタグ名を書いて、下流ステージでタグを使えるようにするイメージです。

version: 0.2

env:
  exported-variables:
    - BUILT_IMAGE_TAG
phases:
  pre_build:
    commands:
      - ...
      - TZ=Asia/Tokyo date +"%Y-%m-%d-%H-%M-%S" > ./TIMESTAMP
      - export BUILT_IMAGE_TAG=FUGAFUGA.`cat TIMESTAMP`
      - ...
  build:
    commands:
      - ...
      - docker build -t $BUILT_IMAGE_TAG -f ./Dockerfile .
      - docker push $BUILT_IMAGE_TAG
      - ...
  post_build:
    commands:
      - ...
artifacts:
  files:
    - ...

提供されている変数を参照する

下流のステージでは変数を参照するには #{NAMESPACE.VARIABLE_NAME} のように書きます。 今回は名前空間が CodeBuildVariables で変数名は BUILT_IMAGE_TAG なので #{CodeBuildVariables.BUILT_IMAGE_TAG} と書けば参照できます。

この書式で各種項目を書いてあげれば、動的に値を指定することができます。

例えば、CodeBuild の編集画面では、環境変数に上流から提供された変数を指定することができます。

f:id:rkosaka:20201208150938p:plain
上流ステージの変数を指定する

最後に

CodePipeline の隠れた(と個人的には思っている)便利機能である、変数を紹介しました。 ステージ間で情報をリレーしたいけど、アーティファクトでは立派すぎる...という場面で重宝するのではないでしょうか?

余談ですが、今回紹介した変数についてのドキュメントの日本語訳が非常に読み辛い状態になっています。 なんとなく、機械翻訳したものがそのまま使われているような気配を感じるのですが...。

変数の操作

宣伝

Misoca 開発チームでは AWS の便利な機能を使ってインフラを改善してくれるエンジニアを募集しています。

アプリケーションの不調を発見し、チームで改善できた話

弥生 Advent Calendar 2020 8日目の記事です。

開発本部の id:mizukmb です。普段はMisocaのインフラの面倒を見たりしています。

Misocaの開発チームでは毎週SLOの状況を共有する時間をとっています。SLOを設定した話は別記事で紹介しています。

tech.misoca.jp

先日、開発チームに共有したところ普段よりもレスポンスタイムが悪化してることがわかりました。これ以上の悪化はサービスに大きな悪影響を及ぼしてしまう事も懸念されました。

そこで、年末年始をトラブルなく過ごせるように開発チームでボトルネックを見つける会を開き、原因の特定から実際に改善し効果が表われたことを確認できました。

問題発見

Misocaには時期によってアクセス数の波があります。月末・月初はアクセス数が普段と比べて多くなり、レスポンスタイムもそれに伴って増加しやすい傾向にあります。

下図を見てもらうとわかりますが、9, 10月は連続してレスポンスタイムの95, 99パーセンタイル値が増加しています。さらに、9月は四半期締めの時期で特にアクセス数が多い月なのですが、それよりも10月の方がレスポンスタイムが長くなってしまっています。この時点では原因は特定できてなかった状態で、早急に手を打つ必要がありました。

f:id:mizukmb:20201207164639p:plain
Redashで可視化しているレスポンスタイムのグラフ。濃い赤が95、薄い青が99パーセンタイルのレスポンスタイム値。9月と比べて10月の方が大きくスパイクしていることがわかる

しかし、開発チームメンバーは各自プロジェクトのタスクを抱えており、どのくらいのタスクが発生するかわからないパフォーマンス改善を各自が率先して引き受けるのは心情的に難しい状態でした。

そこで、過去に開発者ブログでも紹介した「お気持ち会」の要領で有志で集まり、最低限下記を明らかにすることにしました。

  • レスポンスタイム悪化のボトルネック特定
    • フロントエンド、アプリケーション、バックエンド、ネットワーク等様々な観点から見つける
  • ボトルネックを解消する方針の策定

上記までを会のゴールとして、実際に解消する作業は別途行うこととしました。

f:id:mizukmb:20201204140203p:plain
開発チームでどうするか話し合って、有志でボトルネックを見つける会を開くことが決まった

お気持ち会の記事はこちら。

tech.misoca.jp

ボトルネックを見つける会

後日、ボトルネックを見つける会を開きました。当日はRedashで可視化しているレスポンスタイムのグラフやSkylightでRailsのアクション毎のレスポンスタイムやイベントシーケンスを確認しながら、ボトルネックとなっている処理をみんなで探しました。結果として、いくつかボトルネックと思われる処理の特定ができ、さらにはそれに対する具体的な改善策まで話し合って決めることができました。

f:id:mizukmb:20201207143141p:plain
当日の議事録。当日は多くの人に参加してもらったことで一般的な高速化のテクニックからドメイン固有の知見を活かしてボトルネックと思われる箇所を複数発見できた

最終的に、この見つける会ではボトルネックを思われる箇所をいくつかリストアップし、その中から特に大きな効果を期待できそうな改善策を決めて修正するというところまで決めて終わることができました。

コードの修正〜リリース後の効果測定

見つける会では今後の作業者のアサインまでは決められませんでしたが、修正箇所の実装に詳しいメンバーが積極的にタスクを拾い上げてくれたおかげで、会を開いたその日に修正のプルリクエストが出来あがるという驚きの仕事の速さでした。

f:id:mizukmb:20201204150658p:plain
頼れる開発チームメンバー

f:id:mizukmb:20201207143032p:plain
本当に2時間後にプルリクエストを作っていた

リリース直後から効果は表れていて、効果の高いところだと普段よりも95パーセンタイル値で40%程高速になっていました。

f:id:mizukmb:20201207174846p:plain
Skylightのレスポンスタイムグラフ。薄い赤が95パーセンタイル値。リリース直後からグラフが全体的に下がっていて改善されていることがわかる

山場だったアクセス数が増加する期間も大きくスパイクすることがなくなり、パフォーマンスの改善効果があったことを確認できました。

f:id:mizukmb:20201207182411p:plain
Redashで可視化しているレスポンスタイムのグラフ。濃い赤が95、薄い青が99パーセンタイルのレスポンスタイム値

まとめ

アプリケーションのレスポンスタイム悪化から、実際に開発チームで改善するまでの流れを紹介しました。

ボトルネックの発見方法については、過去にISUCON 1 というコンテストに参加していた個人的な経験が活きたかなあと思いました。

また、こうした緊急度の高い問題に対してきちんと開発チームとして問題の提起から解決、結果の測定まで手を動かした事も良かったですし、それをしっかり実行できるMisocaの開発チームは強いチームだなあと改めて実感しました。

採用

チームで問題解決に取り組むことができるMisocaに興味のある方からの応募を待っています!


  1. 与えられたアプリケーションの高速化を競うコンテスト http://isucon.net/

Serverless Framework で DocBase の日報を Slack に流す Bot を作った話

この記事は弥生 Advent Calendar 2020の1日目の記事です。

こんにちは。@KawamataRyoです。
最近嬉しかったことは、6 年ぶりにリアルの新刊を読めたことです。

さて、Misoca開発チーム では DocBase に日報を書くのですが、みんなの日報を読むために毎回ブラウザで DocBase を開くのが地味に面倒でした。 なので、日報の内容を Slack に流す Bot を作ってみました。
この記事ではその実装方法を紹介します。

🍿 何を作った?

DocBase に日報を投稿すると、こんな感じにフォーマットされたメッセージを Slack の専用チャネルに通知する Bot です。DocBase 上でのコメントの投稿にも対応しています。

f:id:ba068082:20201130135321p:plain

一応 DocBase 自体で Slack 連携に対応しているのですが、以下のように投稿の全文が表示されず、毎回 DocBase を見に行く手間が発生するので今回は独自で作りました。

f:id:ba068082:20201126152042p:plain

help.docbase.io

🛠 実装方法

サクッと作りたかったので、DocBase からの Webhook を受ける用の AWS Lambda を作り、レスポンスを Slack 投稿用に整形したうえで Slack の Incomming Webhook に投げる方式としました。
構成はこんなイメージです。

f:id:ba068082:20201201083416p:plain

次項から簡単に実装を説明します。

1. Slack Appの作成

最初に Slack API から Slack App を作成して Incoming Webhooks を設定をします。

https://api.slack.com/apps

f:id:ba068082:20201126151540p:plain

ここで発行した Incoming Webhook の URL を次項で作る Lambda の送信用URLとして使います。

2. Serverless Framework で Webhook 応答用の Lambda を作成

AWS の Lambda の実装です。
Lambda は Serverless Framework を使って開発しました。
Serverless Framework は Lambda や cloud functions の開発環境構築・デプロイを楽に実現するためのライブラリです。

www.serverless.com

今回は以下コマンドで、TypeScript の Lambda 環境を作りました。

$ npx sls create -t aws-nodejs-typescript -n docbase-nippou-notifier -p docbase-nippou-notifier

これだけで指定のパスに Lambda の関連コードが生成されます。
あとは生成されたディレクトリに移動してコードを編集します。

今回は以下のような関数を作りました。
DocBase の Webhook からの POST リクエストを受け取り、Slack の Incoming Webhook に投げるだけです。

import { APIGatewayProxyHandler } from 'aws-lambda';
import 'source-map-support/register';
import { DocBaseWebhookPayload } from "./lib/types";
import { createPostBlock } from "./lib/createPostBlock";
import { createCommentBlock } from "./lib/createCommentBlock";
import fetch from 'node-fetch';

// DocBaseからのイベントを受け取り、整形したデータをSlackにPOSTする
export const webhook: APIGatewayProxyHandler = async (event, _context) => {
  try {
    const payload = JSON.parse(event.body) as DocBaseWebhookPayload;

    if (isTargetTeamPost(payload)) {
      await postToSlack(payload);
    }
  } catch (error) {
    console.error(error);
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'ok',
    }, null, 2),
  };
}

// 対象チームの投稿かどうかの判定
const isTargetTeamPost = (payload: DocBaseWebhookPayload) => {
  return payload.post.tags.some((t) => t.name === process.env.TEAM_TAG_NAME);
}

// Slack Incoming WebhookへのPOST
const postToSlack = async (payload: DocBaseWebhookPayload) => {
  const messageBody =
      "comment" in payload
          ? createCommentBlock(payload)
          : createPostBlock(payload);

  const options = {
    method: "POST",
    headers: { "Content-type": "application/json" },
    body: JSON.stringify(messageBody),
  };
  await fetch(process.env.SLACK_WEBHOOK_URL, options)
}

Slack への投稿の整形は以下で行っています。
レイアウト指定の JSON は、Slack Block Kit Builder を使って構築するのがおすすめです。

import { DocBaseWebhookPayload } from "./types";

export function createPostBlock(payload: DocBaseWebhookPayload) {
  const post = payload.post;

  return {
    blocks: [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: `:pencil2: 日報 posted by ${post.user.name}`,
        },
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `<${post.url}|${post.title}>`,
        },
      },
      {
        type: "divider",
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: post.body,
        },
        accessory: {
          type: "image",
          image_url: post.user.profile_image_url,
          alt_text: "user thumbnail",
        },
      },
      {
        type: "divider",
      },
    ],
  };
}

あとは、serverless.tsを編集して Lambda、API Gateway、環境変数、デプロイ先を設定して以下コマンドを実行すれば指定の Lambda にデプロイされ、API Gatewayまで作成されます。

$ npx sls deploy

Serverless Framework 便利!

3. DocBase の Webhook 設定

最後に DocBase の Webhook の設定を行います。 以下記事の通りに設定すればOKです。設定するURLは Serverless Framework でデプロイした Lambda のエンドポイントとなります。 これで完成です 🎉

help.docbase.io

終わりに

Lambda はちょっとした Bot を作るのにも使いやすいですね。
Misoca の slack には他にも Lambda で作られた Bot が多数生息しています。

tech.misoca.jp

今後も業務・個人で色々作っていきたいなと思いました。

🎺 宣伝

Misoca 開発チームでは日常の些細なことでも技術で解決していくエンジニアを募集しています。

Thinreports に SectionReport フォーマット機能を追加する pull request を作成した

こんにちは、Misoca開発チームの日高(@hidakatsuya)です。

以前、Thinreports の SectionReport フォーマット機能を公開したという記事を書きました。

tech.misoca.jp

この度、この機能を pull request 及び issueとして、Thinreports コミュニティに提案しましたので、その内容について紹介します。また、SectionReport フォーマットでは何ができるのかを知ってもらうために、いくつかの特徴もご紹介します。

SectionReport フォーマット機能のコミュニティへの提案

提案の概要

github.com

機能の概要や使い方、SectionReport フォーマット機能の今後の開発方針などを説明しています。SectionReport フォーマットという機能がどういうもので、どのような背景があり、どのような方針で実装されているかなど、この issue を読むことでそれらを把握することができます。

内容は概ね次の通りです。

  • コミュニティで提案されている仕様 をベースに SectionReport フォーマットを実装した
  • 未実装の仕様や既知の課題が残っているが、それらについてはコミュニティと一緒に開発していきたい
  • SectionReport フォーマットの仕様の説明として、いくつかのサンプルコード (Rubyコード、テンプレート、出力結果PDF) を作成したので参考にして欲しい
  • SectionReport フォーマットの使い方として Hello World を説明
  • 私たちが実装した SectionReport フォーマットは未完成で問題もあるが、今後はコミュニティと一緒に開発を進めたい。その提案として、一旦現状で取り込み、仕様の議論とタスクの整備を行って、広く開発に参加できる環境を作って進めてはどうだろう

実装の詳細 (pull request)

Thinreports の Generator (rubygem) と Editor (テンプレートデザイナ) の実装は、それぞれの pull request として作成しています。

github.com

github.com

pull request の説明では、使い方や実装ステータス (未実装、独自実装、既知の問題) について詳しく記載しています。

ぜひご意見ください

私たちが提案した SectionReport フォーマットは、未完成でいくつかの問題もあります。SectionReport フォーマットの機能としての仕様はもちろん、今後の開発の進め方など、広くディスカッションした上で、より良い形で開発を進めたいと考えています。

issue の内容への質問や意見、実際に動かしてみた感想や質問、コードに対する指摘などなど、ぜひご意見をいただけると嬉しいです。

SectionReport フォーマットで何ができるのか

現行の Thinreports のフォーマット (以降、現行のフォーマットと呼ぶ) との比較として、SectionReport フォーマットの特徴を二つ紹介します。

そもそも、SectionReport フォーマットって何?という方は、まず Thinreports コミュニティで提案されている仕様 をご覧ください。

ヘッダーやフッター、明細行を任意の数だけ定義し、組み合わせて出力することができる

現行のフォーマットでは、ヘッダーやフッターといった概念自体がありません。近い機能として「リスト」というツールがありますが、ヘッダーやフッター、明細行は一つだけしか定義することができません。

SectionReport フォーマットでは、次のように、いずれも任意の数だけ定義することができ、名前(ID)によって、それらを組み合わせることができます。

例えば、次のようなテンプレートを用意します。

f:id:hidakatsuya:20201012180259p:plain
テンプレート (Thinreports Editor)

次のコードで PDF を生成します。

report_params = {
  type: :section,
  layout_file: 'example1.tlf',
  params: {
    groups: [
      {
        headers: {
          header1: { items: { text_block1: 'タイトル' } },
          header2: { display: false }
        },
        details: [
          { id: :detail_a, items: { text_block1: '明細1行目' } },
          { id: :detail_a, items: { text_block1: '明細2行目' } },
          { id: :detail_b, items: { text_block1: '明細3行目' } },
          { id: :detail_b, items: { text_block1: '明細4行目' } },
          { id: :detail_a, items: { text_block1: '明細5行目' } }
        ],
        footers: {
          footer1: { display: false }
        }
      }
    ]
  }
}

File.binwrite('example1.pdf', Thinreports.generate(report_params))

出力される PDF は次のようになります。

f:id:hidakatsuya:20201012174550p:plain
出力結果PDF

パラメータによって、テンプレートに定義されたヘッダーを非表示にしたり (display: false)、複数の明細の定義を組み合わせて出力するといったことが可能です。

自動的に領域の高さが伸縮する

現行のフォーマットは、設定した用紙をキャンバスとして、テキストや図形などを配置してレイアウトを作成します。そのため、出力する PDF の高さも用紙の高さで固定され、テキストや図形などの描画位置やサイズも固定です。そのため、テキストの内容によって、図形の位置を下にずらしたり、領域の高さを動的に変更することが困難です。

SectionReport フォーマットでは、ヘッダーやフッター、明細行の高さの動的伸縮をサポートしています。また、動的伸縮によって、図形などの描画位置も動的に追従することが可能です。次の例をご覧ください。

次のようなテンプレートを用意します。

f:id:hidakatsuya:20201012175515p:plain
テンプレート (Thinreports Editor)

次のコードで PDF を生成します。

report_params = {
  type: :section,
  layout_file: 'example2.tlf',
  params: {
    groups: [
      {
        headers: {
          header1: { items: { title: '長いタイトル ' * 15 } },
        },
        details: [
          { id: :detail_a, items: { detail_text: '短い明細' } },
          { id: :detail_a, items: { detail_text: '長い明細 ' * 18 } },
          { id: :detail_a, items: { detail_text: '短い明細' } }
        ]
      }
    ]
  }
}
File.binwrite('example2.pdf', Thinreports.generate(report_params))

出力される結果は次のようになります。

f:id:hidakatsuya:20201012175703p:plain
出力結果PDF

定義した領域を超えるテキストをセットした場合でも、ヘッダー1 の領域の高さが自動的に拡張し、ヘッダー2 や明細A がその下に続いて正しく描画されます。また、明細Aの二行目では、拡張した領域に合わせて四角形の高さが拡張していることがわかると思います。

最後に

興味のある方はぜひ SectionReport フォーマットで遊んでみてください。

宣伝

Misoca 開発チームでは、積極的に OSS に貢献していきたいエンジニアを募集しています。

www.wantedly.com