コード日進月歩

しんくうの技術的な小話、メモ、つれづれ、など

早期returnにおける使い方のパターン(防御的プログラミング、あるいは分岐処理)をざっくりまとめる

自身の書き方を言語化するシリーズ

この文章内での早期returnとは

メソッドなどで処理の終端に到達する前に return をして戻り値を先行して返すこと。

早期returnを利用できるパターン

早期returnが有用なケースをパターン分けすると以下の2つになると考えている

  • 防御的プログラミングでの早期return
  • 分岐処理の早期return

それぞれに関して記述していく

防御的プログラミングでの早期return

メソッドの引数の値がおかしい場合に実処理を行わないなどの防御的プログラミングの文脈でのやりかたは以下の考え方

def half_value(number)
  return_value = nil
  # ゼロを除算するとエラーになるので0より大きな数値
  if number > 0
    return_value = number / 2
  end
  return_value
end

このような事前チェックが必要なタイプはチェックのやり方が有用になる。後置ifが使えるRubyだと更にスッキリできる。

def half_value(number)
  return nil if number > 0
  number / 2
end

分岐処理での早期return

もしかすると 早期return とはこちらの意味合いを保つ場合のほうが多いかもしれないが、こちらの文脈は以下のようなパターン。

お題が以下のようなものだったとする

与えられた数字が2なら与えられた数、2でなかったら与えられた数と1〜10の数を2乗した値を足して返す

def calc(number)
  return_number = 0
  if number == 2
    return_number = number
  else
    rand_set_number = rand(10)^2
    return_number = number + rand_set_number 
  end
  return_number
end

この場合は与えられた数( number )が2だったら後続処理の必要は無いのでreturnできる

def calc(number)
  return number if number == 2
  rand_set_number = rand(10)^2
  return rand_set_number 
  end
  return_number
end

ただし、この分岐処理の早期returnは明確にスイッチング処理になる場合はswitch case文のような記述を行ったほうが見通し良い場合もあるので状況によって書き換えるといい

参考リンク

変数代入を複数回行うと何が良くないのか、どうするといいのか

変数の代入を複数回行う、すなわち変数の再代入をすることは何が辛いか、また変数の代入を1回にすると何が嬉しいかを書く。

今回の題材

例えばRubyで以下のようなメソッドがあるとする

def make_aisatsu_text(people_name: "Suzuki", is_san: false)
  text = is_san ? "さん" : ""
  text = people_name + text
  text = text + "、こんにちわ!" 
  return text
end

この記述を例に辛い部分は大きく以下の2つ

  • 変数の状態がメソッドの最初から最後まで変わり続けるので変更に弱い
  • (型付き言語だと発生しないが)途中の状態が削除されると読み取り難易度が増す

1つずつ簡単に説明していく

変数の状態がメソッドの最初から最後まで変わり続けるので変更に弱い

上記の例だと text という内容が行によって変わり続ける。 最後にreturnする内容をするのを text にしたいという意図はあるかもしれないが この方法は今後拡張したときに厳しくなる。

例えば

  • Satoさんへの挨拶の場合は、「久しぶり」という言葉を、そうでない人には「はじめまして」を挟む
  • 天気がいいかを調べる、 sunny? メソッドがtrueなら「天気がいいですね」という言葉を挟む

という変更が加わるとすると以下のようになる

def make_aisatsu_text(people_name: "Suzuki", is_san: false)
  text = is_san ? "さん" : ""
  text = people_name + text
  if people_name == "Sato" 
    text = text + "、久しぶり"
  else
    text = text + "、はじめまして"
  end
  if sunny?
    text = text * "、天気がいいですね"
  end
  text = text + "、こんにちわ!" 
  return text
end

このようになるが、この時点でかなり読みづらくなっている(と私は思っている) さらに「こんにちわ!の位置は文の最初の方に…」などの変更が入ったら更にカオスになる。

途中の状態が削除されると読み取り難易度が増す

上に書いた仕様追加されたコードで更に「Tanakaさんの場合は名前を呼ぶ部分はいらなくなりました」といって削除を行うとする。 単純にやると以下のような事が起きる。

def make_aisatsu_text(people_name: "Suzuki", is_san: false)
  text = is_san ? "さん" : ""
  text = people_name + text
  if people_name == "Tanaka"
    text = ""
  end
  if people_name == "Sato" 
    text = text + "、久しぶり"
  else
    text = text + "、はじめまして"
  end
  if sunny?
    text = text * "、天気がいいですね"
  end
  text = text + "、こんにちわ!" 
  return text
end

textを "" してリセットするという処理を入れたが、増々読みにくくなった。

このように継ぎ足していく処理とまっさらにする処理が混在する状況になると「この行における text は何が入っているのか?」というのがよりわかりにくくなる。

改善例

シンプルに再代入をやめることだけでかなりスッキリさせることができる。

def make_aisatsu_text(people_name: "Suzuki", is_san: false)
  san_text = is_san ? "さん" : ""
  full_name_text = people_name + san_text
  return full_name_text + "、こんにちわ!"
end

盛々の仕様変更も耐えやすい

def make_aisatsu_text(people_name: "Suzuki", is_san: false)
  san_text = is_san ? "さん、" : ""
  full_name_text = people_name + san_text
  meet_text = people_name == "Sato" ? "久しぶり、" : "はじめまして、"
  weather_text = sunny? ? "天気がいいですね、" : ""

  main_text = meet_text + weather_text + "こんにちわ!"

  return main_text if people_name == "Sato"
  return full_name_text + main_text
end

参考リンク

X-UA-Compatible メタタグの意味と使い方に関して調べる

今や不要と言われているので調べる

出典

Internet Explorerにはドキュメントモードというものが存在し、下位バージョンのIEと同等の動きをさせるために指定することができた。

例えばIE7相当の表示をしてほしいときは以下のような記述を行っていた

<meta http-equiv="X-UA-Compatible" content="IE=7" >

また、互換モードではなく最新の状態にしてほしい意図のときは以下のようにEdge(最新版という意味のEdgeでWebブラウザのEdgeのことではない)を指定する

<meta http-equiv="X-UA-Compatible" content="IE=edge" >

この仕様をIE以外にも普及させようとしたが定着されず、ほぼIE固有の実装となった。

2020年現在において

現在ではマイクロソフトのドキュメントページを探すと記述がだんだん失われつつあること、加えていよいよIE11がサポート終了するという状況下においてIE11未満の動作を互換モードであるドキュメントモードの必要性は薄れてきており、非常に特殊な環境下出ない限りはこの記述には不要かと思われる。

参考リンク

Rails6において、rails newしたあとに標準機能を間引いていくときにみるべきところ

個別に実装を剥がして行く場合のアプローチ

環境

# bin/rails -v
Rails 6.0.3.1

今回想定するシチュエーション

rails new したときにオプションで間引きたかったが間引き忘れたときにどうやって対応をしていくべきかというアプローチ

見るべきところ

config/application.rb

ここで各種拡張の読み込みを行っている。new時に除外オプションをつけないと以下の様になる

require_relative 'boot'

require 'rails/all'
# 以下略

この rails/all は何をしているかというと以下の通り

# frozen_string_literal: true

# rubocop:disable Style/RedundantBegin

require "rails"

%w(
  active_record/railtie
  active_storage/engine
  action_controller/railtie
  action_view/railtie
  action_mailer/railtie
  active_job/railtie
  action_cable/engine
  action_mailbox/engine
  action_text/engine
  rails/test_unit/railtie
  sprockets/railtie
).each do |railtie|
  begin
    require railtie
  rescue LoadError
  end
end

このようにrequireをまとめてやっている形なので、個別に削りたいケースでは個別のrequireに書き換えて上げると良い。

なお、rails new するときにオプション指定すると以下のように書き換わるので倣っても良い

require_relative 'boot'

require 'rails'
# Pick the frameworks you want:
require 'active_model/railtie'
require "active_job/railtie"
require 'active_record/railtie'
require 'active_storage/engine'
require 'action_controller/railtie'
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require 'action_view/railtie'
require "action_cable/engine"
require "sprockets/railtie"
require 'rails/test_unit/railtie'

config/environments/のファイル

各設定ファイルに呼び出し設定があることがあるので、必要なものを取り除く 大概はRailsコンソールを立ち上げ時にエラーがでるので、そちらをもとに取り除くと良い

参考リンク

HTMLのaタグで別ウィンドウまたは別タブを開くtarget=“_blank”の記述をする際は合わせてrelの記述も確認する

いろいろなところで語られているのでまとめる

問題となるポイント

昔から別ウィンドウや別タブを開くときに書くHTMLは以下の通り

<a href="https://example.com" target="_blank">ここを押すと新しいウィンドウが開きます</a>

この target="_blank" だが、開かれたページ(上記で言うところの https://example.com から window.opener を使うとaタグを埋め込んだ側のページの情報が取得できるため、遷移元のサイトに関して処理を加えることができる。

解決策

openerを無効化するリンク種別情報をつける

aタグはその相手との関係性を rel を用いてリンク種別情報がつけられるタグです。そのためこの relnoopener をつけることで window.opener の設定を無効化することができます。

<a href="https://example.com" target="_blank" rel="noopener" >ここを押すと新しいウィンドウが開きます</a>

詳しい説明をMDNより引用すると以下の通り

これは信頼できないリンクを開く際、 Window.opener プロパティでリンク元の文書を変更できないようにするために特に役に立つリンク種別です。ただし、 Referer HTTP ヘッダーは(noreferrer を使用しない限り)提供します。 - リンク種別 - HTML: HyperText Markup Language | MDN

これは各ブラウザ側が対応するものとなっていますが、最新のブラウザは概ね対応をしています。

ただし古いブラウザでは対応できていないものではあるため、そこまで視野に入ったWebアプリケーションの場合はどうすればいいかというときに出てくるのが noreferrer です

noreferrerをつけてリファラー情報をすべて渡さない

noreferrerの意味は以下の通り

別のページへ移動する際にリンク元ページのアドレスなどの値を、ブラウザーReferer: HTTP ヘッダーでリファラーとして送信しないようにします。 - リンク種別 - HTML: HyperText Markup Language | MDN

こちらは全般的に元ページの情報を渡さないため、前述の noopener 相当の効力を持つことができます。また noreferrer のほうがブラウザのバージョンのカバー率が高いので特にリファラなどを渡さないことに問題ない場合はこちらを使ったほうが良い。

どちらを書くと良さそうか

情報を統合すると

  • リファラ情報などが喪失すると問題がある場合は noopener
  • リファラ情報が遷移先ページで喪失しても問題がない、それよりも多くのブラウザバージョンをカバーしたい場合は noreferrer

という形になると思われる。

余談:主要ブラウザの動き

ChromeFirefoxもrel属性が未指定の場合は noopener 相当の挙動をするのがデフォルトになっているのが昨今の動向なので、この点を意識しなくても問題なくなりつつあるが、認識をしておかないと痛い目を見ることがありそうなので明示的に書いておいたほうがよさそうである。

「Firefox 79」からtarget=“_blank”なリンクの挙動が変更、より安全な仕様に - 窓の杜

参考リンク

Railsのfind***by系のメソッドを軽くまとめる

以下のツイートが気になったの調べた。

環境

$ bin/rails -v
Rails 6.0.3.1

一覧

メソッド名 説明
find_or_create_by 対象を検索して、ない場合はCREATE(INSERT文の発行)をする
find_or_initialize_by 対象を検索して、ない場合はオブジェクトを作成(.new相当)をする
create_or_find_by 新規作成を試みて、 ActiveRecord::RecordNotUnique の場合は既にあるのでfindをする

find_orcreateinitialize の違い

実装を見てもらってもわかりやすい話だが、実際にcreateまで実行してしまうか、オブジェクト生成のみ留めるかの違い。

def find_or_create_by(attributes, &block)
  find_by(attributes) || create(attributes, &block)
end

def find_or_create_by!(attributes, &block)
  find_by(attributes) || create!(attributes, &block)
end

create_or_find_by について

このメソッドに関しては実態のソースに以下のようなコメントがある

1つまたは複数の列に一意の制約を持つテーブル内に、指定された属性を持つレコードを作成しようとします。これらの属性を持つ行が既に存在する場合は のような一意の制約がある場合、そのような挿入が通常発生するであろう例外が捕捉され、それらの属性を持つ既存のレコードがfind_byを使用して発見されます。

これはfind_or_create_byに似ていますが、SELECTとINSERTの間の古い読み込みの問題を回避します。

しかし、create_or_find_byにはいくつかの欠点があります。

  • 基底となるテーブルは、一意の制約で関連する列を定義していなければなりません。
  • 一意な制約違反は、与えられた属性のうち1つだけ、または少なくともすべての属性よりも少ない属性によってトリガされる可能性があります。これは,後続のfind_byが一致するレコードの検索に失敗する可能性があることを意味し,与えられた属性を持つレコードではなく,ActiveRecord::RecordNotFoundの例外を発生させます.
  • find_or_create_byからSELECT -> INSERTの間の競合状態を回避していますが、実際にはINSERT -> SELECTの間に別の競合状態があり、これら2つのステートメントの間のDELETEが別のクライアントによって実行された場合にトリガーされます。しかし、ほとんどのアプリケーションでは、これはヒットする可能性がかなり低い条件です。
  • このメソッドは、制御フローを処理するために例外処理に依存しており、多少遅くなるかもしれません。

このメソッドは、すべての与えられた属性が一意の制約によってカバーされている場合(INSERT -> DELETE -> SELECTの競合条件がトリガされない限り)、レコードを返しますが、作成が試みられ、検証エラーのために失敗した場合、それは永続化されません、あなたはそのような状況でcreateが返すものを取得します。

https://github.com/rails/rails/blob/bca6f7f57693dc4244870f7bd7f37a4f5cbf1976/activerecord/lib/active_record/relation.rb#L178

というように、既存レコードが存在することが設定された値からユニーク成約で発覚するケースにおいては利用することができる。

参考リンク

『銀座Rails #26』をみたよメモ

【オンライン開催】銀座Rails#26@リンクアンドモチベーション - connpass をZoomで視聴したのでそのメモ

各発表の感想

※資料スライドは見つけたら貼ります。


大量データでもサクサク動くRailsになるために

スライドはなさそうなので、発表のメモだけ

発表メモ

SaaSのプロダクトが大企業にも注目され大手顧客対応としてパフォーマンス向上が課題になってきており、そこに関しての取り組みに関しての体験談のお話。

ActiveRecordのValidation問題

validationで他のモデルを参照していると一気に更新処理をかける場合、1件ごとに確認がかかるのでN+1がかかる。 この場合は、一気に処理できるような記述に切り替えると良い。

callback関数の問題

dependnet: destory などがあると一個ずつdeleteがかかるので重くなる。 こちらも、一括で処理できるような処理をする。

ActiveRecordのオブジェクトがでかい問題

何も指定しないと該当レコードのすべてのカラムを撮ってきてしまう。そのためメモリを逼迫してしまう。

selectでカラム名を指定することで参照する時間を減らしたり、pluckでarrayにすることで早くすることができる。

n+1問題

関連レコードをn+1で取得してしまうことがあるので、includeなどを使う。

あぶり出すときにはbulletなどを活用する。ただし鵜呑みにしすぎるとよくないので参考程度にするといい。

DBへのindex問題

リリース重視で開発すると、indexを使わない状態になりがち。

見つけ方としては

  1. slow.logをみる
  2. EXPLAINで検証しつつ、適切なindexをつくる

その他問題になった箇所の問題

  • 仕様を変える、設計をかえる

パフォーマンス改善でやっていて感じること

コードだけで解決できる問題は少ない。DB設計を変更しないといけないケースが3割。

感想

  • 約9ヶ月かけてすごいパフォーマンス改善をした地道なレベルの話
  • activerecord-importにvalidationがあったとは知らなかった…ので勉強になった。

関連リンク


これからの Ruby と今の Ruby について

感想

  • Ruby3.0の新しい機能のひとくち紹介的なスライド
  • 型定義とかどういう風になっていくのかすごく楽しみになった
  • 新しい機能を見ていくと、Rubyのカッコ省略を空スペースで表現できるのがかなり混乱の種だな…と思った。

関連リンク


タイトル

感想

  • DDDのコアドメインに関する話とそれを基軸にRailsでどういう改善活動をしたかのお話
  • ドメイン駆動設計」の「(コア)ドメイン」に重きがおかれており、ドメインってURLの前の部分でしょ?みたいな認識度合いの人に読んでもらうには良さそうな形の内容でした。
  • ここから発展して「MVCのModelとドメインモデルというのは切り口が違う」とか「ドメインが重要なのはわかったけどなんで密結合だと駄目なの?」とか「ドメインの処理とActiveRecord一緒にしちゃ駄目なの?」とかを整理してしゃべれるといいのかもしれない。
  • 疎にするということはコードが増える、コードが増えるということは動作を保証するための作業も増える、その際にRubyはテストコード頼りだが、静的型付けは型でもカバーしてくれる。そういうところが疎に切り離す壁になっている部分は多いなと感じる。
  • 作中で話が上がっていたが「ユースケース」という形に着目してRailsに乗ってみる開発に関してはしたことがあるので、業務の話がでない体験談レベルで切り出してアウトプットしてみたくなりました。

関連リンク


全体を通しての感想

  • DDDの話とRuby3の新しい機能というRailsの直球なお話というよりも取り巻く内容の話が濃い回でした。
    • Ruby3の型が登場する話と、静的型付けじゃない言語は不得手という話が出てきて、地味なコラボレーションがある回でもあるきがしました
  • 自分自身もDDD+Railsには色々思うところがあり、実践できる場面では実践してきたしまだまだ学びが足りない部分もあるので補強してブログなりにしたためていきたいと思いました。
  • 家事の合間に聞けるのですごいありがたいリモート開催。最近懇親会参加できないのを悔やみまくってますが、リアルでできるようになっても中継があると嬉しいなと思います。。

関連リンク