getOrElse関数が便利
最近、fp-tsについて調べる機会がありました。
TypeScriptで関数型言語っぽい事が出来るライブラリです。 プロジェクトに盛り込むことも考えましたが 現時点ではメリットが少なく導入には至りませんでした。
調べていく中でgetOrElse関数を見つけて、これ使えるんじゃねと思ったので備忘録として記録します。
getOrElse関数とは
fp-tsのgetOrElse関数はSomeの場合は中の値を返却しNoneの場合は設定した値を返却します。
本来は関数型言語の考え方に則ってSomeやNoneで処理するべきですが プロジェクトに盛り込めなかったということでNullをNoneとして扱う[getOrElse関数もどき]を自作して使ってみました。
自作版 getOrElse関数
以下、自作したgetOrElse関数です。
値が存在する時とNULLだった時で返す型を一致させないとダメなので ジェネリクスで返却時の型を統一しています。
const gerOrElse = <T>(func: () => T) =><T2>{ return (val: T2 | null) =>{ if(val === null){ return func() }else{ return val } } }
自作版getOrElse関数を使う
自作版getOrElse関数は、以下の条件の時に使えます
- サーバ側からのレスポンスがnumber | nullやstring | nullの型を持っている
- nullだった場合デフォルト値を適用して処理を継続しなければならない
自作版getOrElse関数を使ってコードを修正してみました。 まずは改善前のコードを掲載します。
// 改善前のコード type Info = { name: string, kana: string, testFlg: boolean, zip: null | string, birthday: null | string, other: null | string, age: null | number } const createEditForm = (info: Info) =>({ name: info.name, kana: info.kana, testFlg: info.testFlg, zip: info.zip === null ? "" : info.zip, birthday: info. birthday === null ? "" : info.birthday, other: info.other === null ? "" : info.other, age: info.age === null ? 0 : info.age })
次に改善後のコード。
// 改善後のコード const gerOrElseNumber = getOrElse(()=> 0) const gerOrElseStr = getOrElse(()=> "") type Info = { name: string, kana: string, age: number testFlg: boolean, zip: null | string, birthday: null | string, other: null | string, } const createEditForm = (info: Info) =>({ name: info.name, kana: info.kana, testFlg: info.testFlg, zip: gerOrElseStr(Info.zip), birthday: gerOrElseStr(info.birthday), age: gerOrElseNumber(info.age), })
三項演算子が多く見栄えが悪かったですが、getOrElse関数で一掃しました。
今年採用してよかったドキュメント類
今年からプロジェクト内で属人化を防ぐために
ドキュメントの運用文化をプロジェクトに取り入れてきました。
採用してよかったドキュメント類をまとめていきます。
ユーザストーリー
取り入れた理由
何のために存在しているのかが理解できない機能が増えてきているので取り入れました。
機能を担当していた人が退職して情報が何も残っていないパターンです。
その機能を消していいのかも分かりません。
記載する内容
- 依頼者は誰なのか
- その機能がどんな目的で誰が使うのか
- 受入条件は何なのか
テンプレート
依頼者: 依頼者を記載する 概要: 何故この機能が必要になったのか この機能を使って達成したい目的は何なのかを記載する 受入条件: 受入条件を記載する ストーリー: 実装された機能を実際にどうやって使うのかを記載する
サンプル
実際にどのような使われ方をするのかをストーリー形式で記述するのが特徴です。
依頼者: B部門XXXさん 概要: [B部門]が [顧客のパスワード]を[リセットしたい] なぜなら、[顧客側の環境でパスワードがリセットできない時に社内側で対応したいから] 受入条件: B部門が顧客のパスワードをリセットできるようになる事 ストーリー: [B部門]: 顧客側からパスワードをリセットしたいが出来ないと連絡が入った。 [B部門]: 特別にこちらで対応しよう。 [B部門]: システムのAページに移動してパスワードリセットボタンを押下。 [B部門]: 対応完了。
ストーリー形式でまとめることによって、どのようなフローで機能が使われるのかを
知る事が出来ます。
自分担当しない機能が実装された時、背景を知るのに便利でした。
ガッチリしたドキュメントでなく楽に書けるのでそこもポイント高いです。
今までドキュメントを運用する事がなかったチームでも楽に書けると思います。
ADR
取り入れた理由
ソフトウェアアーキテクチャ・ハードパーツを読みADRの利便性に気が付き取り入れました。
新しいアーキテクチャの適応や最適化業務に対して より詳細に分析するフェーズを設けられるようになったのは、この本のおかげです。
記載する内容
- 何故その設計を採用したのか
- その設計を受け入れた場合どのような恩恵があるのか
- その設計を受け入れた場合のデメリット
- 他に検討した案
テンプレート
コンテキスト: 何故、このADRが生まれたのかを記載。 問題が何なのか。 決定: 複数ある案の中から何故この決定を選択したのかを記載する 影響: この決定を受け入れた場合に発生する影響を記載する 例えば、テストが増えるとか新しい作業工程が発生するとか
サンプル
サンプルの方はソフトウェアアーキテクチャ・ハードパーツの方に
沢山掲載されているので、そちらを覧ください。
ここで適当なサンプルを見るより絶対にそっちを見た方がいいと思います。
開発環境の立ち上げ手順
取り入れた理由
新規メンバーが増えてきて各マイクロサービスの立ち上げ方法を説明するのが面倒になったので
githubのReadme.mdに載せて各メンバーが参照できる状態にしました
記載する内容
- git cloneするところからの立ち上げ手順
- どのような状態になったら開発がスタートできるのかを明確に記載する
テンプレート
なし
サンプル
コマンド列挙するだけでOK
前準備が長い場合はmakeコマンドにまとめちゃいましょう
以下のコマンドを実行して環境を構築する git clone xxx cd xxx make build make reboot http://localhost:9999にアクセスして画面が出たら完了
Railsで組んでるシステムにCQRSのQ入れたら結構良かった
Railsで組んでるシステムにCQRSのQ層を導入しました。
その事について記載します。
問題点
Q層を導入する前に発生していた問題点について記載します。
戻り値の中身がコード上で判別できない
例えば以下のようなコードです。
class PostRepository class << self def get_posts Post.joins(:category, :comments) end end end
何が戻り値になっているのかをコード上で判別できません。
サンプルのような簡単なコードだと、まだ何とかなるかもしれませんが.
膨大な生クエリの場合もう無理です。
ActiveRecordかhashなのか判別できない
例えば以下のようなコードです。
name = member["name"]
パッとみてhashっぽいですが実はActiveRecordの可能性もあります。
メソッドで呼び出すパターンと文字列で呼び出すパターンの2通りあります。
name = member["name"] or name = member.name
コードが膨らんでいる場合、処理を追わないとhashなのかActiveRecordなのか判別できません。
テスト時にmockを使う必要がある
出来る事ならRepositoryをstubする場合の戻り値は実データを使いたいですが
ActiveRecordが戻り値になっているなら難しいです。
minitestでテストを書いている場合はMiniTest::MockでActiveRecordのmockを作って
Repositoryのメソッドにstubする感じになると思います。
外部依存が含まれる場合のみmockにしたいので要素を参照しているだけなら.
可能な限り避けたいです。
改善
Q層を導入して上で挙げた問題点を改善していきます。
Q層の導入
実装は簡単で以下のようにDtoを作るだけです。
class PostQuery attr_reader :name, :category, :comment, :title, :content def initialize record @name = record["name"] @category = record["category"] @comment = record["comment"] @title = record["title"] @content = record["content"] end end
使い方
以下のような形でmapと併用します。
class PostRepository class << self def get_posts Post.joins(:category, :comments).map{ |m| PostQuery.new(m) } end end end
これで、Q層に書いたオブジェクトを見に行けば直ぐに中身が判別できるようになります。
テスト時に関してもPostRepositoryからの戻り値をActiveRecordのMockでなく実データで作ることが出来ます。
test_data = [ PostQuery.new({ "name" => "test_name", "category" => "test_category", "comment" => "test_comment", "title" => "test_title", "content" => "test_comment" }), PostQuery.new({ "name" => "test_name2", "category" => "test_category2", "comment" => "test_comment2", "title" => "test_title2", "content" => "test_comment2" }), ]
まとめ
Q層を導入する場合、Repository層からの戻り値を全て調べてDtoとして再実装するコストが発生する。 Q層を取り入れる事によって以下のような恩恵を受けることが出来る。 - テスト実装コストの削減 - 不具合発生時の解決までの時間短縮 - コードの可読性向上 - 副作用が発生するコードの削減 1年以上の長期運用を想定するシステムの場合には 主に保守性の向上からコストを上回る恩恵を受ける事が出来ると考える。
システムの権限処理を改善する
「プロジェクト内の権限管理が扱いづらいので改善したい」という要望が上がり"簡単な権限処理"に関して改善しました。
その時のことを備忘録として記載します。
簡単な権限処理と難しい権限処理
冒頭で"簡単な権限処理"について対応したと記載しました。 本記事内における"簡単な権限処理"と"難しい権限処理"の違いについて説明します。
簡単な権限処理
組織や役職で管理しているパターンが簡単な権限処理に該当します。 例えば以下のような感じ。
def has_register_authority? staff # リーダーなら登録可能 return true if staff.is_leader? return false end
リーダだったらxxxできる。事務だったらxxxできる。 が簡単な権限処理です。
難しい権限処理
簡単な権限処理に対して難しい権限処理は、役職などの情報 + 権限付与対象のレコード状態を見る必要があります。
例えば以下のような感じ。
MASKING_TEXT = "*******" def has_reffer_authority? user, staff # リーダーは全て閲覧可能 return true if staff.is_leader? # 退会しておらず、年齢が20歳以上のメンバーのみ閲覧可能 user["status"] == true && user["age"] > 20 end # マスキングされたユーザを取得 masking_users = User.all.reduce([]) do |acc, r| acc.psuh({ name: MASKING_TEXT, age: MASKING_TEXT }) if has_reffer_authority?(member, staff) acc["name"] = r["name"] acc["age"] = r["age"] end return acc end
参照権限の付与部分がビジネスロジックを持ってしまい 簡単に切り剥がすことが出来ません。
なので、一旦保留にしてメインサービスの方で引き続き対応することにしました。
改善前の状態
簡単な権限処理に関して改善する前の状態について記載します。
イメージとしては以下のようなメソッド群があらゆるファイルに散っていました。
TANAKA = 191 def has_xxx_authority? staff return true if staff.is_xxx? return true if staff.is_xxx? return true if staff.is_xxx? return true if staff.is_xxx? return true if staff.is_xxx? return false end def has_xxx2_authority? staff return true if staff.is_xxx? return true if staff.is_xxx? return true if staff.is_xxx? return true if staff.is_xxx? return true if staff.is_xxx? return false end def has_xxx3_authority? staff return true if staff.id == 123? return true if staff.id == 433? return false end def has_xxx4_authority? staff return true if staff.id == TANAKA return false end
この状態だと以下のようなデメリットが発生します。
- 権限更新時にソースコードを修正する必要がある
- 誰が何の権限を今持っているのかを理解するのに一々コードを追う必要性がある
- 酷いものになると権限の判定部分にIDが貼り付けられておりコードのみならずテーブルに格納されているstaffの情報まで見る必要性がある
放置しておくと組織の移動イベントが発生した時に 確認作業の時間だらけになり地獄と化します。
この状態を脱却すべく改善を始めたのでした。
権限表を使った改善
あらゆるファイルに散った権限情報を一元管理したいという思いがありました。
そこでシステム内の権限を全てかき集めてテーブルにして管理することにしました。
イメージとしては以下のような感じです。
権限DB
# 役職などのスタッフに関する要素で権限情報テーブルを複数作成
# reffer_xやregister_xの中身はboolean
セクション権限テーブル
|ID|セクションID|reffer_x|register_x|delete_x|
役職権限テーブル
|ID|役職ID|reffer_x|register_x|delete_x|
.
.
.
スタッフ権限テーブル
# staff.id == TANAKAなどの個人に対する権限を表現するテーブルを作成
|ID|スタッフID|reffer_x|register_x|delete_x|
ブロックテーブル
#「xxの部署はAの機能を使えるのですが、xxの部署のbさんだけその機能を使えないようにしてください」という要望がきた場合の権限テーブル
# スタッフ個人に機能制限をかける
|ID|スタッフID|reffer_x|register_x|delete_x|
権限表からスタッフの権限を取得するためには以下のクエリを実行します
( スタッフが所属するセクションの権限レコード OR スタッフが担当している役職の権限レコード ... OR スタッフの権限レコード) AND NOT スタッフのブロックレコード => その社員が持つ全権限
改善対応としてこの権限表にシステムの権限を全て移植しました。
権限表に変えた結果
権限表に変えたメリットを以下に記載します。
今までシステムのコードを彷徨って集めていた権限情報がクエリ1本で取得できる。
xxx(部署とか人)が何の権限を持っているのかが テーブルを参照しただけで理解できる。
権限を変更する場合のコードの修正作業が無くなる。(テーブルのboolean変更するだけ)
コードからテーブルに移したのでUI側で権限を制御できるようになる(現在の権限情報を表示したり管理者向けに権限編集機能を入れたり等)
同じような悩みを抱えていたら権限表による改善いかがでしょうか。
docker-composeを使ってマイクロサービスをやってる時に便利だったデバッグ手法
現在のプロジェクトで便利なデバッグ手法出てきたので備忘録として記載します。
今回例に挙げるフォルダ構成とdocker-composeファイルを下記に記載します。
フォルダ構成
workフォルダがrootでその下に、それぞれのマイクロサービスのdocker-compose.ymlがある感じ。
app開発するならappに移動してdocker-compose up。
analyze開発するならanalyzeに移動してdocker-compsoe up。
work └ app ├ docker-compose.yml └ migration ├ docker-compose.yml └ analyze └ docker-compose.yml
docker-compsoeファイル
appマイクロサービス
appはバックとフロントどっちも入ってるRailsみたいなのを想像してください
appの開発に使用するdocker-compsoe.ymlを記載します
version: "3" services: migration: image: xxx container_name: migration tty: true app: build: xxx volumes: xxx container_name: app analyze: image: mock_xxx(openAPIから作ったmock) container_name: analyze
migrationマイクロサービス
migrationはRailsのmigration機能を引っこ抜いて単体で動かせるようにしたのを想像してください
migrationの開発に使用するdocker-compsoe.ymlを記載します
version: "3" services: migration: volumes: xxx container_name: migration tty: true
analyzeマイクロサービス
analyzeはappマイクロサービス から叩かれる分析用のAPIサーバーを想像してください
analyzeの開発に使用するdocker-compsoe.ymlを記載します
version: "3" services: migration: image: xxx container_name: migration tty: true analyze: build: xxx volumes: xxx container_name: analyze
問題点
例で挙げたようなdocker-composeファイルを利用したマイクロサービスの開発は連携部分をチェックする事が多くなるかと思います。
連携部分をチェックする時に、都度ビルドしてイメージで固めてタグ付けてdocker-composeファイル修正するのが非常に面倒です。
具体的には下記のような要望を満たしたいのです。
新規のmigration回しながらappで確認したい
analyzeで修正した内容をイメージで固める前にappで確認したい
analyzeをmockでなくて実体で起動してapp側で確認したい
そこで以下で紹介するデバッグ手法を開発に取り入れました。
デバッグ手法
イメージではなく実体でデバッグしていけるようにdocker-compose.debug.ymlファイルを作成します。
docker-compsoe.debug.ymlファイル
appマイクロサービス
migrationとanalyzeを実体で起動できるようにします。 パス指定を環境変数にします。
version: "3" services: migration: build: ${PATH_TO_MIGRATION} volumes: - ${PATH_TO_MIGRATION}:xxx container_name: migration tty: true app: build: xxx volumes: xxx container_name: app analyze: build: ${PATH_TO_ANALYZE} volumes: - ${PATH_TO_ANALYZE}:xxx container_name: analyze
他マイクロサービスを実体で起動する時は以下のコマンドを実行します
PATH_TO_MIGRATION=../migration PATH_TO_ANALYZE=../analyze docker-compose -f docker-compose.debug.yml up
これでanalyzeやmigrationをイメージで固めなくてもフォルダをいじっただけで即反映される環境が出来上がります
migrationマイクロサービス
連携部分がないのでdocker-compose.debug.ymlを作成する必要はありません。
analyzeマイクロサービス
migrationを実体で起動できるようにします。 そパス指定を環境変数にします。
version: "3" services: migration: build: ${PATH_TO_MIGRATION} volumes: - ${PATH_TO_MIGRATION}:xxx container_name: migration tty: true analyze: build: xxx volumes: xxx container_name: analyze
他マイクロサービスを実体で起動する時は以下のコマンドを実行します
PATH_TO_MIGRATION=../migration -f docker-compose.debug.yml up
この手法を取り入れてから実体で確認できるようになったので開発効率も向上し楽になりました。
1人の人間が複数マイクロサービス を担当していて確認作業を実施する回数が多い場合、効果を発揮するかと思います。
MUI dataGridのfiled部分を型チェックする方法
MUI x Ts x Reactで最近開発を進めています
データの出力部分にMUIのDataGridを使っているのですがfiledに型が効かないので拡張して対応しました
MUI DataGrid
DataGridは便利で素晴らしいコンポーネントなのですがfiledに型が効きません。
※ MUI data-gridのページから引用 const columns: GridColDef[] = [ { field: 'id', headerName: 'ID', width: 90 }, { field: 'firstName', <- ここ headerName: 'First name', width: 150, editable: true, }, { field: 'lastName', <- ここ headerName: 'Last name', width: 150, editable: true, }, { field: 'age', <- ここ headerName: 'Age', type: 'number', width: 110, editable: true, }, ];
内部のコード見た感じfiledがstirng型として定義されています。
なので間違って存在しないfield名を入れても通っちゃうんですね。
Row型の定義
DataGridで使用するrecordに型を付けます。
type Row = { id: number, firstName: string, lastName: string, age: number, } const info: Array<Row> = [ { id: 0, firstName: "xxx", lastName: "xxx", age: 0, }, { id: 1, firstName: "xxx", lastName: "xxx", age: 0, } ]
GridColDefの拡張
以下のようなinterfaceでMUI DataGridのGridColDefを拡張して対応することにしました。
interface ColDef<T extends string> extends GridColDef { field: T }
fieldの型に互換性を持たせるためにstring型を継承させています。
また、項目を追加したい場合、以下のように | で繋ぐと表現できます。
interface ColDef<T extends string> extends GridColDef { field: T | '操作' }
使い方
今回、interfaceやtypeを同一ファイルに入れていますが分離させてください
(例えばColDefに関してはDataGridのwrapperコンポーネントでexportするなど)
interface ColDef<T extends string> extends GridColDef { field: T } # GridColDefをColDefに修正 const columns: ColDef<keyof Row>[] = [ { field: 'id', headerName: 'ID', width: 90 }, { field: 'firstName', headerName: 'First name', width: 150, editable: true, }, { field: 'lastName', headerName: 'Last name', width: 150, editable: true, }, { field: 'age', headerName: 'Age', type: 'number', width: 110, editable: true, }, ];
これで以下のようにfirst_nameでタイポしててもエラーが出てくれます。
const columns: ColDef<keyof Row>[] = [ { field: 'id', headerName: 'ID', width: 90 }, { field: 'first_name', headerName: 'First name', width: 150, editable: true, }, { field: 'lastName', headerName: 'Last name', width: 150, editable: true, }, { field: 'age', headerName: 'Age', type: 'number', width: 110, editable: true, }, ];
エラー文: [ 型 '"first_name"' を型 'keyof Row' に割り当てることはできません。'"firstName"' でよろしいですか? ]
Rowに存在しないフィールド名を付けたい場合はこんな感じです。
| で繋ぐ。
const columns: ColDef<keyof Row | "fullName">[] = [ { field: 'id', headerName: 'ID', width: 90 }, { field: 'firstName', headerName: 'First name', width: 150, editable: true, }, { field: 'lastName', headerName: 'Last name', width: 150, editable: true, }, { field: 'age', headerName: 'Age', type: 'number', width: 110, editable: true, }, { field: 'fullName', headerName: 'Full name', description: 'This column has a value getter and is not sortable.', sortable: false, width: 160, valueGetter: (params: GridValueGetterParams) => `${params.row.firstName || ''} ${params.row.lastName || ''}`, }, ];
filed名間違えてタイポすることって結構あると思います。
それを事前に型で防ぐための対策でした。
goのWebFrameWork "gin" を使ってみた
概要
goのWebFrameWork "gin" を使ってみた。
DB(MySQL)からデータを取ってきてJSON化させて返却するところまで実装。
ソースコード
以下のサンプルが実装されています。
- gormの設定
- sql-migrateの設定
- migrationコード
- seedコード
- gormを使ったデータの取得
main.go
データを取得してデータを返却する。
localhost:5050/zoosでアクセスするとデータが帰ってくる。
package main import ( "github.com/gin-gonic/gin" "hasGoriraAPI/app/service" _"encoding/json" ) func main() { r := gin.Default() r.GET("/zoos", func(c *gin.Context) { var zooService service.ZooService zoos := zooService.GetZoos() c.JSON(200, zoos) }) r.Run(":5050") }
Migration
Migrationは、sql-migrateを使用。
マイグレーションファイルの例
-- +migrate Up CREATE TABLE IF NOT EXISTS prefectures ( id int(15) AUTO_INCREMENT, name varchar(255), created_at datetime default current_timestamp, updated_at timestamp default current_timestamp on update current_timestamp, PRIMARY KEY (id)); -- +migrate Down DROP TABLE IF EXISTS prefectures;
Seed Data
Gormを使って生で書いた。 テストデータは全部適用。
package main import ( "hasGoriraAPI/app/model" "hasGoriraAPI/db" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) func main() { connection := db.DBConnect() defer connection.Close() createPrefectureSeedData(connection) createZooSeedData(connection) } func createPrefectureSeedData(connection *gorm.DB){ prefs := []model.Prefecture{ { Name: "北海道"}, { Name: "青森県" }, { Name: "岩手県" }, { Name: "宮城県" }, { Name: "秋田県" }, { Name: "山形県" }, { Name: "福島県" }, { Name: "茨城県" }, { Name: "栃木県" }, { Name: "群馬県" }, { Name: "埼玉県" }, { Name: "千葉県" }, { Name: "東京都" }, { Name: "神奈川県" }, { Name: "新潟県" }, { Name: "富山県" }, { Name: "石川県" }, { Name: "福井県" }, { Name: "山梨県" }, { Name: "長野県" }, { Name: "岐阜県" }, { Name: "静岡県" }, { Name: "愛知県" }, { Name: "三重県" }, { Name: "滋賀県" }, { Name: "京都府" }, { Name: "大阪府" }, { Name: "兵庫県" }, { Name: "奈良県" }, { Name: "和歌山県" }, { Name: "鳥取県" }, { Name: "島根県" }, { Name: "岡山県" }, { Name: "広島県" }, { Name: "山口県" }, { Name: "徳島県" }, { Name: "香川県" }, { Name: "愛媛県" }, { Name: "高知県" }, { Name: "福岡県" }, { Name: "佐賀県" }, { Name: "長崎県" }, { Name: "熊本県" }, { Name: "大分県" }, { Name: "宮崎県" }, { Name: "鹿児島県" }, { Name: "沖縄県" }, } for _, pref := range prefs { connection.Create(&pref) } } func createZooSeedData(connection *gorm.DB){ zoos := []model.Zoo{ { PrefId: 1, Name: "北海道ゴリラ大学", HasGorira:true }, { PrefId: 12, Name: "ぽんぽこ動物園", HasGorira:false }, { PrefId: 3, Name: "わにわに博物館", HasGorira:false }, { PrefId: 33, Name: "ゴリラの里", HasGorira:true }, { PrefId: 22, Name: "白鳥の森", HasGorira:false }, } for _, zoo := range zoos { connection.Create(&zoo) } }
大規模なテストデータを管理する時は他の専用ライブラリを使ったほうが良さそう。
Model
モデル層を実装。
Prefecture Model
package model type Prefecture struct { Name string }
Zoo Model
package model type Zoo struct { PrefId int Name string HasGorira bool }
Service
サービス層を実装。
データをJOINして返却する。
gormが結構便利だった。
package service import ( "hasGoriraAPI/db" ) type zooRecord struct { Name string PrefName string HasGorira bool } type ZooService struct{} func (s *ZooService) GetZoos() []zooRecord{ connection := db.DBConnect() defer connection.Close() var records []zooRecord connection. Table("zoos"). Select("zoos.name, prefectures.name as pref_name, zoos.has_gorira"). Joins("left outer join prefectures on prefectures.id = zoos.pref_id"). Scan(&records) return records }