最近、オリオンビールの定期配送を申し込みました、べあーです。
毎日オリオンビールの製品が飲めるのに、うっかり追加で WATTA をワンケース頼んじゃいました。新作のトロピカルグァバ味です。(写真左)
WATTA はオリオンビールが沖縄県で売ってるチューハイです。
最近は一部の味が関東でも買えるのでコンビニなどで見かけから買ってみてください。
パッションフルーツ味とシークワーサー味が買えます。
パッションフルーツ味、オススメです。(写真真ん中)
お断り
今回の記事は人にやさしいコードの書き方についてですが、これはあくまで一つの評価軸として捉えてください。
タスク状況やコードレビュー者の好み、言語やライブラリによる制約により、適用できない場合があります。
人にやさしくないコードとは?
人間は脳みそを使って思考します。
よくワーキングメモリという、短期的な記憶領域を使ってコードレビューを行います。
ワーキングメモリは人によって大きさは違いますが、気にしておくべき項目が減るのは誰にとっても有効に働くはずです。
またコードの利用者や読む人にとっても、気にする項目が少ないことにより、本来の目的を素早く達成できます。
今回は上記の視点から、以下の3種類の「人にやさしくないコード」を「人にやさしいコード」に変えていこうと思います。
人にやさしくないコード
- すべての条件をクリアしないとメソッドを離脱しない。
- エラー出たけど、何をしていいかわからない。
- 条件部分が複雑でわからない。
すべての条件をクリアしないとメソッドを離脱しない。
まずは次のコードを見てください。
1 2 3 4 5 6 7 8 9 10 11 |
def callSomeApi # ConfigObject は json 形式のファイルをハッシュとしてプログラムから利用できるインスタンスとする。 api_id = ConfigObject['some_api']['api_id'] unless api_id.blank? api_secret_token = ConfigObject['some_api']['api_secret_token'] unless api_secret_token.blank? # api_id と api_secret_token を使う処理 return "Some Api Result" end end end |
ここでは二重の if 式の else 部分を通らないと本来の処理にたどり着けません。
この場合、api_id の条件式と api_secret_token の条件式をワーキングメモリに残して、本来やりたい処理である「api_id と api_secret_token を使う処理」の部分に進むことになります。
2 つの条件であればいいですが、三重、四重となるともうワーキングメモリはいっぱいです。
また、テストケースを考える際には二重の条件を考慮しないといけなくなります。
そこで、if 式を入れ子ではなく、並列にすることを考えます。
条件に合わない場合、即時でメソッドを脱出することでワーキングメモリから不要なメモリを削除でき、本来の処理に集中することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def call_some_api # ConfigObject は json 形式のファイルをハッシュとしてプログラムから利用できるインスタンスとする。 api_id = ConfigObject['some_api']['api_id'] if api_id.blank? raise "値がセットされていません。" end api_secret_token = ConfigObject['some_api']['api_secret_token'] if api_secret_token.blank? raise "値がセットされていません。" end # api_id と api_secret_token を使う処理 return "Some Api Result" end |
またテストケースを考える際にも、api_id と api_secret_token の条件式を独立して考慮できるため、テストケースの節約にもつながります。やったね!
エラー出たけど、何をしていいかわからない。
コンフィグファイルや DB の設定値を読み込むような処理の場合、
想定した値が入ってなく、エラーを出す場合があると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def call_some_api # ConfigObject は json 形式のファイルをハッシュとしてプログラムから利用できるインスタンスとする。 api_id = ConfigObject['some_api']['api_id'] if api_id.blank? raise "値がセットされていません。" end api_secret_token = ConfigObject['some_api']['api_secret_token'] if api_secret_token.blank? raise "値がセットされていません。" end # api_id と api_secret_token を使う処理 return "Some Api Result" end |
ここで、エラー文言に注目すると「値がセットされていません。」と何をどうすればいいか読み取れないエラーが記載されています。
こうなると、このメソッドの利用者はこのメソッドの実装を読み、原因を判定する仕事が始まります。
そこで、エラー文言を以下のようにしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def call_some_api # ConfigObject はYAML 形式のファイル(config.yml)の中身をハッシュとしてプログラムから利用できるインスタンスとする。 api_id = ConfigObject['some_api']['api_id'] if api_id.blank? raise "config.yml の some_api/api_id がセットされていません。" end api_secret_token = ConfigObject['some_api']['api_secret_token'] if api_secret_token.blank? raise "config.yml の some_api/api_secret_token がセットされていません。" end # api_id と api_secret_token を使う処理 return "Some Api Result" end |
エラーメッセージから、何をすればエラーを解消できるかがわかります。
これで一つ仕事を減らすことができました。やったね!
条件部分が複雑でわからない。
先程の条件文では、値が blank? に該当するかどうかだけを気にしていました。
実際の業務の際には、これよりも複雑な条件を扱うと思います。
以下の条件を考えてみましょう。
- 平日の 9:00〜19:00 の間は値を返します。
- ただし、水曜日はノー残業デーなので、9:00〜17:00 の間だけ値を返します。
- さらに、当日が祝日だった場合は値を返しません。
素直に書いていくと下記のようになると思います。
1 2 3 4 5 |
# is_holiday は 引数に渡した日付が祝日だった場合に true を返すメソッドとします。 now = Time.now if ((! (now.saturday? || now.sunday?)) && now.hour.between?(9, 19)) && (now.wednesday? && now.hour.between?(9, 17)) && (! is_holiday(now)) # 何かの値を返す処理 end |
ただ、このままだと条件式や括弧が多く、ワーキングメモリを圧迫します。
そこで、条件ごとに変数に切り出してみましょう。
1 2 3 4 5 6 7 |
now = Time.now is_weekday = !(now.saturday? || now.sunday?) is_weekday_and_working_time = is_weekday && now.hour.between?(9, 19) is_no_extra_working_time_day = now.wednesday? && now.hour.between?(9, 17) if is_weekday_and_working_time && is_no_extra_working_time_day && (! is_holiday(now)) # 何かの値を返す処理 end |
リーダブルコードの 2章「名前に情報を詰め込む」にもある通り、変数名を明確にしていくこと、if 文内の条件式の見通しがよくなりました。
引用: https://www.oreilly.co.jp/books/9784873115658/
また、当初設定した条件(=仕様)と同じ粒度にすることで仕様とコードの乖離を減らし、ワーキングメモリの負担を減らすことができます。やったね!
終わりに
以上、今回は人にやさしいコードを目指して、ワーキングメモリを減らす視点でどうやってコードを書いていくかを考えてきました。
コードを書くことに夢中になってしまい、思いついたコードをそのまま書いてしまうことは多々あります。
後々そのコードを読むことになる自分に向けて、やさしいコードを書いてあげましょう。
おまけ
この記事を書くにあたって、演算子を少し調べていたところ、気になったのでおまけで書きます。
論理否定・論理積・論理和は、記号と英字で優先度が違う。
まずは次の式を見てください。
1 |
(−4)+3×(−2) |
さて、この答えは -10 になると思います。なぜでしょうか?
足し算と掛け算を思い出してください。
計算式含まれる掛け算と割り算は、足し算と引き算より優先して計算し、括弧がない場合、左側から計算して行きます。
(左結合の演算子のみを考慮します。)
なので、下記の順序で計算されます。
1 2 3 4 5 |
(−4)+3×(−2) (−4)+(-6) -10 |
そこで次の一見同じ処理をする二つの式を見てください。
1 |
false and false or true |
1 |
false and false || true |
それぞれの結果が以下のようになります。
1 2 3 4 5 6 7 |
irb(main):001:0> false and false or true => true irb(main):002:0> false and false || true => false |
通常、論理積は論理和より優先されるはずです。
ではなぜ、二つ目の式は false になるのでしょうか?
Ruby では、! と not、&& と and、|| と or では、演算子を計算する順番が違います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
高い :: [] +(単項) ! ~ ** -(単項) * / % + - << >> & | ^ > >= < <= <=> == === != =~ !~ && || .. ... ?:(条件演算子) =(+=, -= ... ) not 低い and or |
引用元: https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html
そこで、演算子の順番を括弧でくくると下記のようになります。
1 2 3 4 |
false and false or true (false and false) or true (false) or true true |
and 演算子は or 演算子よりも優先されるので括弧でくくられ、想定通りの洗いとなります。
1 2 3 4 |
false and false || true false and (false || true) false and (true) false |
一方、|| 演算子は and 演算子より優先度が高いです。(上記引用の表を参照してください。)
優先度に従うと、|| 演算子が先に括弧でくくられ、その後に and 演算子の演算を行います。
記号と英字で書かれるだけの違いですが、意外にハマりやすいポイントなので気をつけましょう。
おまけ、おしまい。