CodilityのLesson1をGoで解いてみた(解説付き)
概要
オンラインコーディングの試験であるCodilityのLesson1を自分なりに解いてみました。
BinaryGap coding task - Learn to Code - Codility
使用したのはGo
になります。
問題の要約
以下の範囲の整数値が与えられた時、バイナリ化し1
で囲まれた0
ブロックの最長の長さを求めよ
ただし、バイナリのなかで一つも1
で囲まれた0
ブロックがない場合は、0
を返すこと
[1..2,147,483,647]
コード
先にコードを載せておきます
package main import ( "fmt" "strings" ) func main() { // 問題文の仮定リスト int_list := []int{1, 2, 20, 147, 483, 647} // 仮定リストを回す for _, val := range int_list { fmt.Println(`integer is :`, val) fmt.Println(`max bynary gap is :`, Solution(val)) fmt.Println(``) } } func Solution(N int) int { // 10進数から2進数(バイナリ)に変換する // 終端が0で終わる場合は消し去る binary := strings.TrimRight(fmt.Sprintf("%b", N), "0") fmt.Println(`bynary is :`, binary) // バイナリの中の1が出てくる回数を数える delimiter_count := strings.Count(binary, `1`) // 1が一回しか出てこない場合、両端が1で囲まれるケースは存在しないので0を返す if 2 > delimiter_count { return 0 } // 1を区切りにして0のブロックを配列として取得する binary_gaps := strings.Split(binary, `1`) // 0の配列の中で一番長さが大きいものを取得する max_binary_gap_length := 0 for _, binary_gap := range binary_gaps { if max_binary_gap_length < len(binary_gap) { max_binary_gap_length = len(binary_gap) } } return max_binary_gap_length }
結果 integer is : 1 bynary is : 1 max bynary gap is : 0 integer is : 2 bynary is : 1 max bynary gap is : 0 integer is : 20 bynary is : 101 max bynary gap is : 1 integer is : 147 bynary is : 10010011 max bynary gap is : 2 integer is : 483 bynary is : 111100011 max bynary gap is : 3 integer is : 647 bynary is : 1010000111 max bynary gap is : 4
解説
コード自体の説明はコードに書いてあるのでそこは省略します。
主に上記の点以外で意識した点は以下の点になります。
- コードで使用されている記述方法が言語間で汎用的に使えること
- Goにおける正規表現は排他制御を行なっているためスケールする際にネックとなり得るため使わない
- 人間が読みやすく、かつ、問題文に対して(工数的な意味で)迅速に解決できるようにスコープを絞る
まとめると、リプレイスしやすくスケールしやすいコードを書いたわけです。
今後Goに成り代わる言語が採用される場合も予想されるので、そういった場合に特定の言語に依存した文法を使うとリプレイスの障害になる場合があります。
汎用的な文法を使っていれば、リプレイス先の言語でも容易に置き換え方法が見つかるでしょう。
正規表現の件もそうですが、ネックになりそうな点はあらかじめ代替え策を探し、できそうであればそちらを使うようにします。
技術的な負積ではなく、戦略的な負積を積むようにチームで意識することで取り組むべき目標も見やすいかなと。
CQRSとCQS
What is CQRS
Command Query Responsibility Segregation
和訳すると責務分離によるコマンドとクエリパターン
になる
What is CQS
Command Query Separation
和訳すると分離によるコマンドとクエリパターン
になる
What is コマンド
and クエリ
difference is CQRS
and CQS
責務
という概念を取り入れるかどうかの違いになると思う
具体的にいうと
CQS
はメソッドレベルで参照系と操作系を切り分けることCQRS
はドメインレベルで参照系と操作系を切り分けること
ここでのドメインとは複数の意味を指す言葉であるのだが、分かり易い例としてオニオンアーキテクチャにおけるドメインの単位毎に切り分けると考えた方がいいと思った。
what is advantage
スコープの厳密化
参照系と更新系を切り分けることによって、トランザクションを操作系だけにかけたりすることができる。
modelの管理
アプリケーションは廃棄容易性を高めなければ、非常に脆い存在になりかねない。
参照系と操作系のModelを分けることによって、参照系のViewをパッケージに切り分けることができるので管理がしやすくなる。
require Event Sourcing
?
これには賛否両論あると思うが、個人的には必要がないように思える。 処理の一連の流れもアプリケーションのドメインの一部だと思うので。 これはデータ操作のトリガー自体はDBの範囲内ではないという考えに基づいている。
Rails のテストにおけるログレベルの変更ベストプラクティクス
Railsでは以下の方法でテストのログレベル変更ができるようです。
Rails.logger.level
に動的にレベルを付与するconfig
ファイルでレベルを設定する
基本的にログはテスト結果のコンソールに出さないほうがいいでしょう。
UTの結果をPRに貼る場合に加工しないと見づらくなるためです。
なので config
ファイルでレベルfatal
を設定し、ログ情報を取得するようなテストの場合Rails.logger.level
で変更するのが、ベストだと考えます。
少し詰まった点としてどのconfig
を使用するかはRailsEnv
の変数で決定するようです。
testケースの環境を作りたい場合は、RailsEnv
にtest
を設定してconfig/environments/test.rb
に書けばいいということですね。
また、Rails.logger.level
は場所に関係なく所属するclassのすべてのケースに影響するようです。
これもハマりっぽいので覚えておきます。
Railsで作ったAPIのテストを全通させて学んだこと
RailsWay から外れるのは辛い
今回はModelを使わないRailsAPIを作っているのだが、アプリケーションからDBを切り離そうとするとそれだけで苦戦するので、最初からRubyだけで作った方が楽なのかもしれない。
また、gem同士の依存関係が強くRSpecもDBの接続ありきで作られているので大変だ。それ以外にもStorageとかなんやかんや.... (苦悶の表情)
魔法が多すぎる
これは覚えろというしかないのだが、自明的でない機能が多い気がする。対象が糖衣に包まれすぎると慣れた者にとっては作業が早くなるかもしれないが、その他大勢にとっては理解の妨げにしかなりえないと感じている。
IDEによる予測変換が頼りにならない
今回VSCodeを使っているのだが、Solargraphなどの予測変換機能がrequire
で導入元を明示しないと動かないのだ。
Railsでは一定のルールを守るとrequire
を使わなくてもクラスなどを利用できる機能があるのだがこのメリットが死滅している。
(もしかしたら私の設定方法が悪いのかもしれないが)
ドキュメントがめっちゃある
新規ユーザー(初心者)が多く使う言語なので、問題があっても困ったら調べればすぐに出る。 環境設定以外は...
コマンドによって実行環境が異なる場合がある
これには盛大にハマった
例えばrspec
とbundle exec rspec
この二つは何が違うのだろうか?
これはつまりRuby上で実行する
かRails上で実行する
かの違いだ。
これを知らないとソース上でRailsの機能を使用している場合rspec
を使うとケースが全落ちしてしまう。
他の言語だと意識しなかったところでもあるため気をつけたい。
クラスが定義されてない系のエラーが出たら、実行環境の問題を疑うことを心がけような。
RSpecの知見を得た
導入
こちらの記事で暫定的なテストルールを策定したが、実際に描いている内に不可能なこと、非効率的なことが判明してきた。 there-you-moon.hatenablog.com
この記事ではそれらの原因を踏まえて改善策を策定する
問題
before
の中で let
は書けない
テストの検証作業以外はbefore
の中でまとめたかったのだがそれはできないらしい。
let
でなんでもできる
let
は遅延評価らしく、before
は即時評価なので基本的にlet
推奨らしい
改善策
基本的にlet
で準備する
結論としてはこれにたどり着いた。
クラスのインスタンス化、検証用のオブジェクト生成はlet
内で行う。
モックのインスタンス生成は、呼び出し回数の検証と引数の担保が含まれているためit
対象なので除外する。
じゃあ、before
で何すんねんって話になるが、ログレベルの設定や環境変数の設定が主になると思われる。
個人的にはlet
はobeject生成ごとに一つ必要なため、思考が分断されてしまうのだがそれは利点とのトレードオフと考えれば妥当だと思われる。
また例外的なケースとして遅延評価ではパスできないテストが存在する場合に即時評価に切り替えるlet!
が存在するが、不等価記号は混乱を招くのでbefore
を使用する(コメント要)
RSpec テストルール
各Roleにおけるルール
describe
ユーズケースが記述される
e.g: 800m圏内のレストランを取得する
context
ユーズケースの中で前提条件が複数存在する場合に記述される
e.g.: 営業中のレストランが存在しない時
before
exampleが依存するオブジェクトを準備する
e.g: クラスインスタンスの生成、環境変数のセット、ログレベルの変更
依存する外部要素は全てモック化すること e.g: 外部API,対象クラスに含まれるメソッド以外の外部メソッド
after
exampleの成果物を削除する
e.g: 出力ファイル(.csv, .log)の削除
it
exampleの結果のみ担保する
対象メソッドが想定されている結果を返しているか
対象メソッド内で呼び出されている外部メソッドが以下の条件を満たしているか
- モック化されている
- 想定された引数である
- 呼び出されている
例外の場合は以下の条件が想定されている結果と一致するか
- 型
- メッセージ
各レイヤー層におけるルール
controller
リクエストハンドリングを行なってレスポンスを返しているかを担保する
- 注意点
レスポンス内容は関与しない
domain
ユーズケースに対応する成果物が生成されているかを担保する
- 注意点
ユーズケースに基づいてテストをする為、publicなメソッドのみテスト対象とする
1 usecase == 1 public method
infrastructure
外部コンポーネントを呼び出した時の以下結果を担保する
- 健全なレスポンスを取得した場合、例外が存在しないこと
不健全なレスポンスを取得した場合、想定された例外が投げられていること
注意点
GCSのCLIツールをGoとオニオンアーキテクチャで作ってみた
リポジトリはこちら
https://github.com/KamabokoHouse/go-storage
機能的を説明すると引数で受け取ったバケットのオブジェクトを一覧表示するというCLIツールです。
アーキテクチャ
ディレクトリは以下のようになっています。 (全部は説明しないので特徴的なところだけ)
go/go-storage/ ├── commands │ ├── application │ │ ├── list.go │ │ └── root.go │ ├── domain │ │ ├── exit.go │ │ └── list │ │ └── service.go │ └── infrastructure │ └── gcs.go └── main.go
- main.go
application内のUseCaseを呼び出します。 起動時なのでroot.goですね。
- application/root.go
CLIツールのrootコマンドの定義を行なっています。
rootコマンドを実行すること自体がUseCaseなのでdomainに分離せず、直接書いています。
- application/list.go
サブコマンドの定義を行なっています。
ここではUseCaseの処理手順の定義だけを行い、処理自体はdomain層に移譲しています.
- domain/list/service.go
サブコマンド list
の処理内容を定義します。
処理単位でメソッドを定義しています。
また、domain層とinterface層ではinterfaceの定義を必須としており、層を跨いだ依存を無くしています。
これでMockも作りやすいですしね。
- infrastructure/gcs.go
実際に外部のGCSと処理通信を行う層です。
それぞれのユニーク性を持たせると無作為にメソッドが増えていくためparameterは外部から注入する形にしています。
ここではGet
のみですが、状況に応じてGetByQuery
などを追加するといいでしょう。
余談ですがAPIやコンポーネントを作成するときの注意点として、パラメーターが追加される毎に対象は絞られていかなければなりません。 もしこの原則に反するようになっていたら設計がおかしくなっているので見直すようにしたいですね。
ここ以外の注意点としては、エラーを各層で握り潰さずmainまで必ず返すことです。
エラー処理の処理方法を属人化させないようにします。
本当はDIやテストも実装したかったのですがとりあえずここまでです。