Kawaii Lab

プログラミングとかサービス開発とか

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 クエリ

  • コマンドとは実行により副作用(データへの変更)をもたらすもの a.k.a. 操作系
  • クエリとは実行により副作用をもたらさないもの a.k.a. 参照系

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ケースの環境を作りたい場合は、RailsEnvtestを設定して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を使わなくてもクラスなどを利用できる機能があるのだがこのメリットが死滅している。
(もしかしたら私の設定方法が悪いのかもしれないが)

ドキュメントがめっちゃある

新規ユーザー(初心者)が多く使う言語なので、問題があっても困ったら調べればすぐに出る。    環境設定以外は...

コマンドによって実行環境が異なる場合がある

これには盛大にハマった
例えばrspecbundle 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

外部コンポーネントを呼び出した時の以下結果を担保する

  • 健全なレスポンスを取得した場合、例外が存在しないこと
  • 不健全なレスポンスを取得した場合、想定された例外が投げられていること

  • 注意点

現時点では外部APIのメソッド自体をモック化するが、将来的には外部API自体のモックを作成する。

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やテストも実装したかったのですがとりあえずここまでです。