【雑記】改善の難しさについて

コードの改善、という作業をここ数日ちゃんと向き合った中で気づいたことをまとめてみる。

Overview

あるモデルレイヤーでユースケースごとに無限にメソッドが増えていきそうなポイントをある程度柔軟にパラメータでユースケースを指定して処理を共通化し、コードの見通しを良くする作業です。 該当のコードは僕が現在の案件にジョインしてからも歴史的な経緯を優先してユースケースごとに(同じようなコードが微妙に異なって書かれている)メソッドを追加することで要件の実現を図ってきた箇所であるが、あるユースケースを実現するときに流石にそろそろ限界では?と僕自身が常々感じていたことを改善するために時間を取ってコードの改善を行いました。

パラメータを柔軟に出し入れするために functional options pattern を使ってみたりいろんなコード書きながら、都度方針を説明して、レビューしてもらってあれこれやりくりした結果、ユースケースごとの責務は一つ下のレイヤー(apiとの接合部分)で負わせることにしてモデルレイヤーではユースケースのみを指定するという方針でコードを書き直しました。
※ もっとキレイにリファクタリングできるとbestだったんですが、色々な制約のもとで場所を移す、という方針になりました。(まぁありがちですが)

結果 モデルに露出していた複雑性は一つ下の階層に移動したので、モデル自体はある程度わかりやすくなりました。

わかったこと

以下の2点を痛感しました。

  • ある時点で動いているコードの改善は難しい
  • 非機能要件を求めるには基礎が必要

ある時点で動いているコードの改善は難しい

コードが動いている という状態はそれ自体に価値があり、それを改善という大義名分のもとにいじることは、いらぬバグを混入する可能性もあり、既存の挙動を保証しないということにも繋がるので、慎重さが求められます。
また、歴史的な経緯があって、 今時点の状態 が作られている、という事実もあるので、その歴史的な経緯を理解した上で、改善の内容を定義(何をもって改善とするのか) して レビューを受ける 必要があります。

つまり、普通に考えれば 超めんどくさい 作業です。しないほうがさっさと次の作業に進めますし、問題を未来に先送りすることそれ自体は悪いことではないと思います。

非機能要件を求めるには基礎が必要

同僚のエンジニアの皆様にも助けてもらいながら、時間はかかったものの、ある程度ステークホルダーが納得する形で改善することができましたが、その議論を進めていく中で自分には 基礎がない ことを痛感しました。

リファクタリング、コードの改善なんていうものは言ってしまえは 非機能要件 であり エンドユーザーにはそれ自体は一切メリットがない ことです。やらなくたってサービスにすぐに悪影響があるわけではありません。(すぐにと言ったのは長期的には悪影響があるからです。ex. 拡張性のなさとかメンテナンス性の低下とか)

実装パターンだったり、設計パターンだったり、そもそもの設計の知識やノウハウが不足していて、議論を追っかけるのが精一杯だったり、僕の見えてなかったところをガンガンfeedbackもらったり、その結果思ったより時間がかかって心折れそうになったり、とまぁこういったことがあって、そこそこ大変だったわけです。

エンジニアとして 機能要件を満たすこと はさほど難しいことではないけれど、一度非機能要件にこだわると基礎が必要になるということを実感しました。

cf. エンジニアになる覚悟 - Speaker Deck
上記のスライドがすごく勉強になります。

このケース以外にもいろんなケースで非機能要件を追求するには、基礎が必要な場面があると思います。

改善しようと思った理由

思ったより大変だったことに対して、避けることもできたんですが、なんでやろうと思ったかと言うと 将来の自分が絶対に今の自分を恨むだろうなー と思ったからです。

改善したかった箇所は僕もずっと触れてる中で「なんで同じような実装があるメソッドが大量にあるねん、ユースケースごとに分けてもいいけど中の実装は共通化しろや」って割と温度感高めに感じていた箇所でもありました。
今まで自分が書いてきた箇所も、ある程度違和感は感じつつ、それに蓋をして、目の前の案件を仕事を進めるためにコードの綺麗さを犠牲にしてきたのも事実です。

今回もそうすることもできましたが、別案件で、今までに想定してなかったユースケースを提示されて、流石にこれを実現するにはまたメソッドと実装を増やすしかないなと感じて、改めてコードを目の当たりにしたときに「これ以上増やしては行けない」という思いがふつふつと湧いてきたし、まだ案件に入っていなかったので、リファクタリングの時間を取るなら今しかないと思って「えいや」で進めました。

違和感で立ち止まる勇気

進めるにあたっての課題感は前述したとおりで、自分には基礎がなかったので、リファクタリングを始める時点でどういう形が最終的にいいのかはイメージしきれていませんでした。

そもそも何がイケててイケてないのか、ということもまだまだわからないことが多いです。でもその中でも、日々コードを書いている中で 違和感 を感じることはできます。
※ ex) なんか同じコード多いなー、とかそういうやつ

エンジニアがコード書いてるときに違和感を感じたときは、多分何かがおかしい(責務とか、設計とか)ことが多いです。
その何かが具体化できなくても、違和感に蓋をせずに一度立ち止まって何がおかしいのか考えてみることは大事です。もしわからなければ有識者に違和感をそのまま伝えてみるのもいいかと思います。
過去の歴史的経緯を知ることで、違和感の正体を具体化し、必要なことが見えてきます。

立ち止まってリファクタリングすることまでできたら最高ですが、違和感を放置せずに、正体を知る努力をするだけでもコードへの理解は一段進むかも知れません。(ほぼ自戒)

仕事を進めることはもちろん大事です。ただ、なにか違和感を感じたときに、仕事の手を止めて考える時間を取るには勇気がいります。
手を止めて改善策を考えた結果、もっと大変になりそう、、、という未来が見えることがあるかも知れません。その結果仕事を進めてるほうが楽だから、とりあえず臭いものには蓋をしておこうと思っても不思議はありません。

でもあえて手を止めて一度考えてみるという機会を設けてみることで、今のコードに対する見方が変わるかも知れません。

安易に安い解決策を求めた結果あとで苦労する or 恨まれるくらいなら今時間とっても大した事はありません。

まとめ

せっかく買ってあったのに積読状態だったからこれ読もう www.amazon.co.jp

gitでよく使うコマンド(たまに更新)

なんとなく使っていたgitコマンドたちを最近commitやmergeをする前に意図を持って使うようになったのでよく使ってるコマンドを整理しました。
※ ちょくちょく更新していきます。

Commands

remoteのmasterを取ってくる時

作業ブランチからmasterにcheckoutして最新の状態に戻すときに、追跡されてないファイルとかを削除してブランチの状態をきれいにしてからmergeします。

追跡されてないファイルを確認する

git clean -n

追跡されてないファイルを削除する

git clean -f

追跡されてないファイルとディレクトリを削除する

git clean -fd

これをするとvendorディレクトリなどの依存ライブラリを管理しているディレクトリも削除してしまうので、改めて入れ直す必要があります。

pullする

癖でmaster出し git pull をいつも使ってましたが、最近は予め fetch origin master してから merge origin master するようにしてます。

remoteのmasterの状態とlocalのmasterの状態が異なってしまうときにどのファイルでコンフリクトが起きそうなのかを予めlocalにmergeする前にわかっておくと、誰がいつなんで変更したファイルなのかを知ることができます。
resolve conflict というcommitメッセージを極力残さないように注意するようになりました。

remoteにpushするとき

差分を確認する

コードを変更したらまず意図した差分かを確認します。

git diff

これだけでHEADの状態とlocalの作業ブランチの状態を比較します。

ただ、具体的なコードレベルの差分よりも、commit前になったら差分があるファイル一覧だけ欲しい時があります。

git diff --name-status HEAD

これで差分があるファイル一覧を確認します。

新規ファイルを作成したりしたときは

git status

で意図したファイルが作成されているかをcommit前にかならず確認します。

indexに登録する

手癖で git add . を使って一括でHEADに上げることをしていました。
ただ、これだとよくわからないファイルを上げてしまったり、予期してなかった変更を上げてしまうことが増えてしまって、レビュー時に指摘されてしまったりということが増えてきました。

そのため 一括で HEAD に登録する git add . をやめました。

commitするときは diff --name-status HEAD を確認したファイルをindex登録します。 この時できるだけ細かい粒度でaddするようにはしています。 ※ めんどくさいと git add -A でaddすることもあります、

最近知ったんですが、 -p オプションなるものがあるらしく対話形式で厳密にファイルを index に登録していくことができるらしいです。

git add -p .
# diffが表示
Stage this hunk [y,n,q,a,d,e,?]?
# yを選ぶと index に登録される。

a を選ぶと残りを全てindexに登録してくれます。

再度差分を確認する。

add し終えたら再度差分を確認します。

git diff --cached

commitする

commitメッセージ注意するくらいかな。最近はGUIでsquash and mergeすることが増えてきたのでとりあえず適当に書いておいてあとで直せばいいや、くらいのノリでいますが。

add + commitを一度に行う

git commit -a

commit メッセージを書くときにどのファイルがindexに登録されているかもすぐに分かって良さみがある。最近一番良く使う。

goappコマンドを入れ直した話

Overview

GAEを使う場合、localにapp-engineのSDKを入れる必要があります。
少し前までは brew install app-engine-go-64 を叩くと goapp コマンドがlocalにinstallされ、使うことができましたが、ある時から formulla から消えてしまい、brew 経由でDLすることができなくなりました。最新のツールを使ってないと、新しくGCPで使える機能が使えなかったりするので最新の状態に保ちたいと思います。

brewで入れられなくなって以降、goapp コマンドは Google Cloud SDK 経由で入れるようになっています。

Google Cloud SDK経由で入れる

手順は以下の3つ

  1. Google Cloud SDK を入れる。
  2. AppEngineのSDKを入れる。
  3. Google Cloud SDK経由で入れたAppEngineのSDKにpathを通す。

以上です。

Google Cloud SDK を入れる。

※ GoのSDKを使います。

以下からinstallすると gcloud コマンドが使えるようになります。

https://cloud.google.com/appengine/docs/standard/go/download

AppEngineのSDKを入れる。

gcloud component install app-engine-go

※ 上記SE(Standard Environment)のinstall方法に記載してるとおりです。

ちなみにGCP関連の全てのSDKを突っ込む場合は

gcloud component update

すると全て入ります。

Google Cloud SDK経由で入れたAppEngineのSDKにpathを通す。

Google Cloud SDK で入れたAppEngineのSDKにpathを通します。

Google Cloud SDKで入れたSDK郡が入っているpathは個々人の環境で差があると思います。
僕の場合は /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/platform/google_appengine にあります。

これをshellの設定ファイルにPATHとして登録します。自分はfishを使っているので以下のようになります。

set -x PATH /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/platform/google_appengine $PATH

最後に google_appengine 配下にあるコマンド郡の権限を設定します。

ex) appengineの場合goappコマンドに対して実行権限を付与します。

chmod -x /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/platform/google_appengine/goapp

デプロイする場合は appcfg.py に実行権限付与します。

これで最新のツールを揃えることができます。

goimportsでlocalのパッケージのsort順序を編集する。

-local付きでgoimportsする

goのformatとimportパッケージをよしなにしてくれる goimports コマンドですが、localで独自に定義したpackageをimportするときにこんな風に並んでほしいことが多いです。

import(
  // 標準pkg
  
  // github.comとか有名所pkg
  
  // localの独自pkg
)

goimportsは何も指定しないと https://github.com/golang/tools/blob/165bdd618e6d174c61ee7bd73a28f3e5c6fdced1/imports/fix.go#L44-L67 で示された順序で import の内部をsortします。

ここでは https://github.com/golang/tools/blob/165bdd618e6d174c61ee7bd73a28f3e5c6fdced1/imports/fix.go#L48 にあるようにlocalで定義されているpackageのPrefixを指定してあげます。

以下のようにして goimports を叩きます。

goimports -w -local "localのpackage名" foo.go

-w オプションはgoimportsをかけた出力結果でgoのファイルを上書きするオプションです。

注) importをsortするときは一度importの中から改行を消します。
どういうことかというと以下のような感じです。

import(
  "fmt"
  
  "bar"  
  
  "github.com/foo"

)

みたいに整列されているのを

import(
  "fmt" 
  "bar"  
  "github.com/foo"
)

こうして改行を削除して goimports をかけると

import(
  "fmt"
  
  "github.com/foo"
  
  "bar"  
)

こんな普通に意図した結果で sortされるはずです。
bar はlocalのカスタムpkg

なぜ改行を一度外すかと言うと、goimportsは改行があるとすでにgrouping済みと認識してしまい、意図したsortをしない可能性があるからです。

intellij(jetbrains系IDE)での設定

intellijでは以下のように設定するとうまくいきます。
Preference > Tools > ExternalTools > (+) でカスタムコマンド作成

以下のように設定します。この設定した External ToolsにKeymapでショートカットとか割り当てておけば、コード書き終わったあとにgoimportsをかけてソートします。

f:id:ema_hiro:20180608012237p:plain

Working Directory はこのコマンドが動作するディレクトリの範囲を決めることができます。

引数定義の中身は以下

f:id:ema_hiro:20180608012243p:plain

追記

Intellijでは Plugin から FIle Watchers をDLします。これでFileのSaveをHookして任意のスクリプトなり、actionを指定することができます。
上記の設定と同様の設定をFile Watchersに設定したので Cmd + S でFileを保存したときによしなに imports を聞かせることができるようになりました。

Vueのテンプレート構文のエンコードでハマる

<!DOCTYPE HTML>
<html lang="ja">
<head>
    <meta charset="utf-8"/>
</head>
<body>
<div id="app">
    <p>This is sample Vue app</p>
    <p>count: {{ count }}</p>
</div>
<script src="https://unpkg.com/vue"></script>
</body>
<script>
    new Vue({
        // describe options
        el: '#app',
        data: {
            count:0,
        },
        methods: {
            countUp: function () {
                this.count++
                console.log("count up")
            }
        }
    })
</script>
</html>

Mustache構文を使った時にブラウザで以下のようなエラーにぶち当たりました。

プレーンテキストドキュメントの文字エンコーディングが宣言されていません。ドキュメントに US ASCII 外の文字が含まれている場合、ブラウザーの設定によっては文字化けすることがあります。ファイルの文字エンコーディングは転送プロトコルで宣言されるか、文字エンコーディングを指定するバイトオーダーマークがファイルに使われている必要があります。

encoding文字しているのに指定しているのにブラウザ側で怒られました。(firefox ver60を使用)
XSS対策関連で怒られてるっぽいのでとりあえず、vue内で使うpropertyを指定できればいいので v-html を使って回避しました。

<p>count: <span v-html="count"></span></p>

テンプレート構文 — Vue.js

あたりを参考にするといいっぽい。vue始めたばかりでよくわかってないけど。

初めてハッカソンに参加してきた話

ハッカソンと呼ばれるもの参加してきたので反省をつらつらと。

内容

Event

先週の土日(5/26,27)にこちらのイベントに参加してきました。

connpass.com

Theme

仙台市の抱える課題をRESASやモバイル空間統計を使って、あぶり出して解決する

というものでした。

Output

地域課題に対するソリューションをKaggleみたいなコンペ形式で提出するプラットフォームを立ち上げるためのプラットフォームを作る。

というものを考えました。

反省

準備について

これは事前にチームが組めたわけではなかったので、環境を用意したりとか、エディターの設定等を事前にすり合わせることができず、全くできませんでした。

次回はGAE/Goである程度動く環境を作っておくとかしたいなと思います。
友達に聞いたら https://ngrok.com/ もいいよっていってました。要は手間をかけずにチーム開発できる準備というのは前もってしておかないと行けないなと思います。

また、準備については例えばCRUD的なアプリケーションについては、いくつか雛形作っておきたいなと思いました。というのも、大体何を開発するかと行ったときに、ベタなCRUDアプリケーションがシステムとしてはベースになると思っていて、それを今回準備できてなかったので、当日の作業の足かせになってしました。

Goで書いたroutingのみの簡単なAPIサーバーの雛形はいくつもあるんですが、そこで満足せず、ローカルのDBに接続するための設定ファイルのテンプレだったり、実際に動作するもの(できればbootstrapらへんでUIの雛形もあるもの)を用意しておくと今回みたいな事前準備ができない場合でも、当日すばやく開発にはいれるなーと実感しました。

当日について

当日のアウトラインは以下のとおりですね。

チームビルディングについて

個々人で企画検討、その後全員でそのアウトプットを見ながら自分がどの企画にJOINしたいかを決める、というものでした。

結果として僕の出した企画に4人集まっていただき、最終的に5人チームが発足しました。
ただし、エンジニアが僕一人しかいなかったので、もう一人くらいいると良かったなと思います。
とはいえ、ここについては運営の企画内容によるので仕方無い部分もあり、与えらた環境で最大限アウトプットを出す方向に切り替えました。

企画内容について

Themeに沿う形で、ある程度、実際のneedsに応えることのできるアウトプットになったのではないか?と思っています。

ただ、これにはすごい反省があって、現場のニーズに応えることに意識が行き過ぎてしまい、ハッカソンのゴール設定としてどうあるべきか というところの観点がすっぽり抜けてしまっていました。

課題解決策を考えているのは楽しいし、それが実際の課題とも紐付いていい感じにできそう、という思いはあったものの、ハッカソンとしてのゴールをどこにおくのか?
そこを考えずに企画を作ってしまい、そこはハッカソン勝ちに行く ということを考えたときにどうなのかな、、、とは感じました。

開発(実装)について

単なる掲示板、しかもデザインなしというものだったので正直、ベタなHTMLのページ作ってあとはプレゼンベースでストーリー補足!という方向にしました。 それ自体が悪かったアプローチではないなと思っていますが、ハッカソンらしい、「なにかを作る!」という点で考えるともっとできることあったなと感じました。

こういうときにデザイナーさんの凄さを実感しました。 採用言語については、普段使い慣れているGoを使いましたが、やーやはりこういうときにGoは向かないw 書く量が多くて、それは確かにいいけれど、railsにすればよかったと思いました。

チームにエンジニアが一人しかいなかったのでそもそも一日という時間で作れるものなんかほぼないと思って先に限界を決めてしまったのが敗因でした。

どんなときでも少しでも(見た目含めて)良くしようという気持ちだけはないと行けないなと実感。

とはいえ、プレゼンについてでも述べますが、企画内容をベースにプレゼンで頑張る、あくまで現実のニーズにフォーカスする、という方向で決めていたので、それ自体は今回の企画に対する自分のアプローチとしては間違ってなかったかなと思っています。

(実装は...頑張れ自分....)

プレゼンについて

普段コンサルをしている方がチームにいらっしゃって、すごい助かりました。

ある程度プレゼンのアウトラインを決めてから各自(自分以外)必要なデータ、エビデンスを揃えてアウトラインごとに作成、あとで間引く、というやり方で作りましたが、これは最初に企画の目的や世界観を共有していたために、思ったよりスライドをマージしたときに全体としてブレずにスムーズに進みました。

実際の発表でも時間ちょうどくらい(デモをするために端末切り替えにかかる時間除く)で終わって良いプレゼンになったと思います。
質疑応答にも自分的には割とスムーズに応えることができました。

このあたり、常々自分が疑問に思っていたことに対するソリューションベースで企画を考えていたために、普段から思考実験できてるし、っていうのもあってのことだったと思います。常日頃課題見つけて、考えていることは大事ですね。アウトプットできたらもっと素晴らしいんだろうと思いますがw

結果

審査員賞でした。

まとめ・感想

本企画、ハッカソンというよりはプロトまで作成するアイデアソンに近いと感じました。
ハッカソンっぽくはないなと思っていましたが、それでも最終的にダサい成果物でもちゃんとプレゼンと合わせて一つのものを出せたことは良かったです。

また、今回については自分の企画にチームが集まってくれたこともあり、少しPM的な立ち位置でチームをどう作るか、アイデアをどう具体化していくのか、というところにも少し足を踏み入れることができました。
これは僕の進め方もうまくはなかったですが、集まっていただいた方々がいい人ばかりでとても助かりました。仕事は何をするかでなく、誰とするか、というのを実感しました。

個人としては、こういったハッカソンに参加するのはほとんど初めてだったので、勉強になることが多かったです。
現実の課題意識が先行しすぎて、ハッカソンとしてどうアウトプットを出すべきなのか、ということをもう少し考えることができたら良かったのかなと思います。現実の課題解決方法を考えるのは楽しいけど、やはり限られた中でハッカソンとして、審査員の欲しがるものを出す というのも重要な観点なんだと思います。

総じていい会でした。一日目の夜に東北らしく日本酒まつりがあり、市場では出回らない希少なお酒を飲むことができてそれも良かったです。5年ぶりくらいに仙台に足を運びましたがやっぱ東北に住むのも悪くないなーと少し思いました。

終わり

go get 時の `import path does not begin with hostname` でハマった話

1年近くGo書いててGOROOTの設定が間違ってたっていう話を書くのもお恥ずかしい話ではあるんですが備忘録のため

環境としては

  • brewでgoを入れている
  • gorootをテキトーにしか設定してなかった($HOME/goみたいな)

エラーの内容は go get コマンドを叩くと

package bufio: unrecognized import path "bufio"
# 以下標準packageのimportに失敗する

というエラーでした。

普段app-engineしかいじってないので正規のgoをいじってpackageを入れようとしたら入らなくて色々こねくり回してました。 最終的には brew で goを入れた場合のlibexecのpathをgorootに指定することで解決しました。

標準packageのところでミスっているのでGOROOT周りの設定がおかしいのでは?っていうところにはすぐ行き着いたのですが、それにしても設定の仕方がおかしかったところで大分時間を食ってしまいました...
local環境も定期的にメンテしておかないと行けないなーと反省しました。

以下 fishの設定

# go
set -x GOROOT /usr/local/opt/go/libexec
set -x GOPATH $HOME/.go
set -x PATH $GOPATH/bin $GOROOT/bin $PATH