LoginSignup
313
331

More than 3 years have passed since last update.

Rubyで学ぶ1年目に知っておきたいプログラミング技法8選

Last updated at Posted at 2019-09-01

はじめに

駆け出しプログラマだった頃の自分に教えたい、8つのプログラミング技法について纏めます。本エントリでは主にRubyを用いますが、ここで紹介する技法はあらゆる言語に適用できます。

8つの技法はリーダビリティ関心の分離の2つに分類できます。サンプルコードを添えて可能な限り具体的に解説していきます。

対象読者

  • コードを書くのに慣れてきた人
  • けどもっと良い書き方はないか?と考えている人

リーダビリティ - Readability

リーダビリティとは、アーキテクチャに関係しない純粋なコーディングの綺麗さです。可読性とも呼ばれます。私達は仕事時間の相当量をコードの読解に当てています。コードの読解は短期記憶をフル活用するため、コーディング作業そのものよりも知的負担が大きくなりがちです。同僚の書いたコードは勿論、先週の自分が書いたコードでさえ記憶の奥底から掘り出すのに時間が掛かることもしばしばです。ましてや一年前、二年前に書いたコードなら尚更でしょう。

リーダビリティの低下は、バグを生む、機能追加のコストが上昇する、仕様変更に弱くなる、メンバーの精神が不安定になるなど、種々の厄災の温床になります。ではリーダビリティを維持するためにどのようなテクニックがあるのでしょうか。

1. Guard Clause

boolean型の2つの引数flag_a, flag_bを取るメソッドf1を考えます。f1flag_aがtrue且つflag_bがfalseの場合にだけメソッドf2をコールします。
駆け出しの私は恐らく次の様なコードをCVS1にコミットしていたことでしょう。

悪いコード.rb
def f1(flag_a, flag_b)
  if flag_a
    unless flag_b
      f2
    end
  end
end

これは不必要にネストを深くしてしまっている悪いコードです。もう少しマシなバージョンは次のようなコードです。

少しマシなコード.rb
def f1(flag_a, flag_b)
  if flag_a && !flag_b then
    f2
  end
end

ネストを下げられましたが、まだ読み手の脳に充分に優しいとは言えません。なぜなら条件文が論理演算子(andやor)を含むとリーダビリティは途端に下がるからです。
今の私ならこのように書きます。

良いコード.rb
def f1(flag_a, flag_b)
  return unless flag_a
  return if flag_b
  f2
end

私達の脳もコンパイラと同様、コードを一行ずつ読んで理解します。そのため一行に多くの状態が含まれていると、理解に要する時間が長くなってしまいます。Guard Clauseは平易な条件文を用いて即座にメソッドの処理を終える(returnする)ことで、深いネストや難しい条件文を排除してくれます。

2. 表現的命名

はじめに神は0と1を創造された。
神は「アセンブルあれ」と言われた。するとアセンブルがあった。
神はアセンブルを見て、良しとされた。神はアセンブルを自動アセンブルとハンドアセンブルに分けた。
神は「手続き型言語」と言われた。すると手続き型言語があった。
神は手続き型言語を見て、良しとされた。神は手続き型言語をFORTRANとLISPに分けた。
第一日である。

旧約聖書 〜創世記〜 第1章

プログラミング言語の歴史は置き換えの歴史でありました。0と1で構成された機械語はアセンブリに置き換えられ、アセンブリは英文に近い手続き型言語に置き換えられた後、現代の私達の手に馴染む言語へと変貌を遂げました。お蔭でdef is_money_important_than_lives?; true; endのような人間に優しい書き方でプログラミング出来るのです。これを活かさない手はありません。

送金元ユーザ、送金先ユーザ、金額、通知メールの送信可否の4フラグを受け取る送金メソッドを考えます。

悪いコード.rb
def submit(number, sendmail, userA, userB)
  userA.minus(number)
  userB.add(number)
  userB.notify if sendmail
end

submitnumberなど、広い意味を持つ英単語が使われています。変数名やメソッド名を適当に決めてしまうことは、駆け出しプログラマが必ず通る道です。

上のコードを読んだ同僚(もしくは半年後の自分)は、このsubmitメソッドをコールしている箇所を探し出し、前後の文脈からsubmitメソッドの用途を推測しなくてはいけません。

良いコード.rb
def send_money(amount, should_send_email, sender, receiver)
  sender.pay_money(amount)
  receiver.get_money(amount)
  receiver.notify if should_send_email
end

限定的な意味を持つ英単語に置き換えました。それでもまだ意味の絞り込みが不十分な場合は動詞原形+目的語の英文にしています。またboolean値の場合は接頭辞shouldを付けることで、変数名だけでその変数がboolean型であると察知できます。

3. 説明変数

書いている本人は理解できるものの、同僚にとっては理解に時間が掛かるコードは沢山あります。例えば次のようなものです。

悪いコード.rb
if Time.parse(params[:datetime]) < DateTime.now.prev_year.to_time
  # params[:datetime]が一年以上前の時に実行する処理
end

条件文が複雑でどういう意図のif文なのかすぐには理解できません。そこで説明変数の出番です。

良いコード.rb
is_over_a_year_ago = Time.parse(params[:datetime]) < DateTime.now.prev_year.to_time
if is_over_a_year_ago
  # params[:datetime]が一年以上前の時に実行する処理
end

分かりやすい名前の変数に一旦代入してあげることで、コードの意図を読み手に伝えることができます。

4. インデント

インデントはタブであるべきかスペースであるべきかという宗教戦争は、未だ決着を見ずインターネットの各地で繰り広げられています。非プログラマ達は「死ぬほど興味がない」と思いこの戦争を眺めていますが、筆者の見解も「どっちでもいいんじゃない」です。それよりも重要視するべきは一貫性です。赤軍も白軍もこの点においては同意してくれることでしょう。

悪いコード.rb
class Product
attr_reader :id, :category_id
attr_accessor :name, :price, :created_at

def initialize(params)
@id = params[:id].to_i
@category_id = params[:category_id].to_i
@name = params[:name]
@price = params[:price].to_i
@created_at = Time.parse(params[:created_at].to_s)
end
end

どこまでがクラス定義でどこまでがメソッド定義か、使われているキーワードを読まないと判別できません。ここまで読み辛いコードなら、新人プログラマであっても何が悪いか気付けるでしょう。しかし最初はみんな上のようなコードを書くものです。

マシなコード.rb
class Product
  attr_reader :id, :category_id
  attr_accessor :name, :price, :created_at

  def initialize(params)
    @id = params[:id].to_i
    @category_id = params[:category_id].to_i
    @name = params[:name]
    @price = params[:price].to_i
    @created_at = Time.parse(params[:created_at].to_s)
  end
end

これならコードレビューを担当した先輩プログラマもGitHubのApproveボタンを押下してくれるでしょう。しかしもう少しやれることは残っています。

良いコード.rb
class Product
  attr_reader   :id,
                :category_id

  attr_accessor :name,
                :price,
                :created_at

  def initialize(params)
    @id          = params[:id].to_i
    @category_id = params[:category_id].to_i
    @name        = params[:name]
    @price       = params[:price].to_i
    @created_at  = Time.parse(params[:created_at].to_s)
  end
end

インデントを足して、アクセサの属性に改行を入れました。これだけで読むのが非常に楽になりますね。

ただしここまで多くのインデントと改行の挿入には批判もあります。批判は行数が増えるインデントを調整するのに時間が掛かるの2つに集約できそうです。それぞれの批判については次にような反論が可能ですが、やり過ぎかどうかは個人の好みの問題になってきます。

批判1. 行数が増える

原則として、1ファイル当りの行数は少ないのが正義です。例えばRubocopというRubyのLinter2は、1クラスが100行を超えるとデフォルトで警告を出します。
しかしRubyを始めとする主要なオブジェクト指向プログラミング言語は、後述するMixinを備えています。Mixinにより1つのクラスファイルを複数のファイルに分割可能なので、行数を気にせずジャンジャン改行できます。

批判2. インデントを調整するのに時間が掛かる

インデントの調整時間はリーダビリティと明白にトレードオフの関係にあります。リーダビリティと書く速さのどちらを優先すれば開発効率が上がるか定量的に判断することは難しいです。しかし一般に、コードは書かれる時間よりも読まれる時間の方が長いものです。

関心の分離 - Separation of Concerns

ソフトウェア開発は複雑性との闘いです。放っておくとコードベース3は加速的に複雑化し、もはや誰も触ることはできないが何故か動いているブラックボックスと化すのです。

関心の分離はプログラムを関心(=担う責任)毎にモジュールと呼ばれるソースファイルに分割し、ソフトウェアをそれらモジュールの総体として捉える概念です。

それでは関心の分離に分類される技法にどのようなものがあるか、確認していきましょう。

5. Do one thing well

UNIXのコミッターは巨大なコードベースを相手にしている根っからのハッカーです。そんな彼等が共有している設計思想をUNIX哲学といいます。Do one thing wellはUNIX哲学の中でもあらゆる開発現場で有効という点で特に重要な設計思想です。

例えば、次のUNIXコマンドは今いるディレクトリの.jsonファイルの個数を出力してくれます。

$ ls
bar.json    foo.json    fuga.gif    hoge.txt    piyo.json
$ ls -l | grep '\.json$' | wc -l
3

UNIXではプログラムを小さな粒度に保つべきという掟があります。複雑な処理を行いたいときは、上の例のように、それら小さなプログラムをパイプ|で連結して実現するのです。

つまり個々のプログラムは、小さな責任を冴えたやり方で果たせ、すなわちDo one thing wellであれという訳です。

一人の技術者が設計の全てを把握できていた古き良き時代は遠に過ぎ去り、現代は複雑なシステムを複数人が分担する構造になっています。さらにシステムは常に機能の追加と変更、そして不具合に襲われます。したがって、現代のシステムは、上手く動く細かいプログラムを組み合わせて作るという設計が望ましいとされています。Do one thing wellはそのための思想と言えるでしょう。

6. Fat model, Skinny controller

Ruby on Rails等のMVCデザインパターンで定石とされる考え方です。Model層にビジネスロジックを集め、Controller層は薄く保ちます。ControllerはModelやService(後述)のメソッドをコールし、自身でビジネスロジックを持ちません。

悪いusers_controller.rb
class UsersController
  def create
    # 新規User作成
    user = User.create!(params)

    # 作成したUserに通知
    mailer = Mailer.new
    mailer.to = user.email
    if user.admin? then
      mailer.subject = '管理者さん、ようこそ'
    else
      mailer.subject = 'ユーザーさん、ようこそ'
    end
    mailer.send
  end
end

Userを作成した後、そのUserにメールを送信しています。が、Userインスタンスの属性で条件分岐をしたりと、Controllerがビジネスロジックを持ってしまっています。

良いusers_controller.rb
class UsersController
  def create
    user = User.create!(params)
    user.sendmail
  end
end

Userへのメールの送信処理は、Userクラスに作ったインスタンスメソッドsendmailに移しました。お蔭でControllerには一行のメソッドコールを書いておくだけで済みます。

7. Mixin

前項でビジネスロジックはModel層に寄せるのがセオリーであると書きました。しかし1つのModelクラスにメソッドを書いていたのでは、Modelクラスの行数が嵩みます。そこでメソッド定義はModelクラスからモジュールとして外に追い出し、Modelクラスはそのモジュールを読み込むように作ります。これがMixinです4

悪いコード.rb
class User
  attr_accessor :family_name, :first_name, :age, :hobby

  def initialize(params)
    @family_name = params[:family_name]
    @first_name  = params[:first_name]
    @age         = params[:age]
    @hobby       = params[:hobby]
  end

  def full_name
    "#{family_name} #{first_name}"
  end

  def introduce_myself
    "#{full_name}です。#{hobby}が好きな#{age}歳です。"
  end
end

user = User.new family_name: '秋山', first_name: '雅之', age: 29, hobby: 'パソコン'
p user.introduce_myself
# "秋山 雅之です。パソコンが好きな29歳です。"

4つの属性を持つクラスです。フルネームと自己紹介文を返す2つのインスタンスメソッドがクラス内に定義されています。

良いコード.rb
# mixins/greetable.rb
module Greetable
  def full_name
    "#{family_name} #{first_name}"
  end

  def introduce_myself
    "#{full_name}です。#{hobby}が好きな#{age}歳です。"
  end
end

# models/user.rb
class User
  include Greetable

  attr_accessor :family_name, :first_name, :age, :hobby

  def initialize(params)
    @family_name = params[:family_name]
    @first_name  = params[:first_name]
    @age         = params[:age]
    @hobby       = params[:hobby]
  end
end

user = User.new family_name: '秋山', first_name: '雅之', age: 29, hobby: 'パソコン'
p user.introduce_myself
# "秋山 雅之です。パソコンが好きな29歳です。"

2つのインスタンスメソッドをGreetableモジュールとしてクラス外に追い出しました。クラスはGreetableモジュールをinclude文によってMixinしています。これによりModel層にビジネスロジックを集約させても、クラスファイルの肥大化を避けられます。

ちなみにRuby on RailsにはConcernというフレームワーク独自のMixin機構が備わっており、通常はそちらを使用します。

参考:[Rails] ActiveSupport::Concern の存在理由

8. Service layer

Mixinにも弱点はあります。主に次の2つです。

内部状態に依存する

理想的な関数とは、内部状態を持たず、出力が入力だけに依存する関数です。しかしソフトウェアは理想的な関数だけでは構築できません。ここでいう内部状態とは、関数に渡される引数以外の情報という程度の意味です。データベースであったり、外部APIであったり、ログイン中のユーザーの性別であったりします。

Mixinはクラスを拡張するという性質上、出力が入力以外、特にクラスが持つインスタンス変数に依存するケースが多いです。

では、出力が入力以外に依存すると何故困るのでしょう。それはコードの見通しが悪くなるためです。入力以外に依存する関数の挙動を把握するために、プログラマは依存箇所をコードベースからあちこち探す必要があります。Mixinにビジネスロジックを書いたけど、そのロジックはMixinを継承するクラスによって動きが変わるというケースは多いです。Mixinだけで処理が閉じていない以上、テストケースを書くコストまで上昇します。

名前空間の共有

Mixinのもう一つの弱点は名前空間の共有です。クラスにメソッドや属性を追加するというMixinの性質上、それらのメソッドや属性は同じ名前空間を共有します。そのためメソッドや属性の命名は冗長になりがちです。つまりMixinモジュールは他のMixinと名前が被らないように、メソッド名をaddではなくaddFriendにしなくてはいけないということです。

そこでService Layer

Service layerはこの2つの弱点を低減させる力があります。複数のModelを使う または 単に複雑 なビジネスロジックは、Service layerと呼ばれるMVCとは独立した層にクラスとして実装されます。

例えば、ユーザーから別のユーザーへ送金するControllerメソッドがあったとします。

悪いコード.rb
# controllers/banks_controller.rb
class BanksController
  def transfer
    User.transaction do
      sender   = User.find(params[:sender_id]) # 出金元
      receiver = User.find(params[:target_id]) # 入金先
      sender.bank.minus(params[:price]) # 出金
      receiver.bank.add(params[:price]) # 入金
      Log.store!(receiver, sender, params[:price])
    rescue => e
      ...
    end
  end
end

トランザクションしたり、ユーザーを2件取得したり、入出金したり、ログを作ったりしていますね。これだけならまだ見通しは良いですが、送金先が複数件になったり、ユーザーにメールを送ったりという要件が加わると、途端にロジックが複雑になります。

それではService layerに処理を任せてしまいましょう。

良いコード.rb
# controllers/banks_controller.rb
class BanksController
  def transfer
    sender   = User.find(params[:sender_id]) # 出金元
    receiver = User.find(params[:target_id]) # 入金先
    Transfer.new(sender, receiver, params[:price]).do
  end
end

# services/transfer.rb
class Transfer
  def initialize(sender, receiver, price)
    @sender   = sender
    @receiver = receiver
    @price    = price
  end

  def do
    User.transaction do
      sender.bank.minus(@price)
      receiver.bank.add(@price)
      log
    end
    true
  end

  private

  def log
    Log.store!(@receiver, @sender, @price)  
  end
end

行数は嵩みましたが、Controllerからビジネスロジックを排除することができました。
送金処理をクラスとして実装できたので、入力と出力の関係が分かりやすくなり、テストが容易になっています。コードの再利用性も向上しました。名前空間も気にせず分かりやすく短い命名が可能です。

これなら仕様変更で送金処理が複雑化しても、そんなに怖くないですね4。精神が安定するのが分かります。

おわりに

使用頻度が高い技法を独断と偏見を持って8つに絞り紹介しました。「それを挙げるならこれも要るだろ」「その技法はむしろバッドプラクティス」など反対意見もあるはずです。一つの仕様でも十人いれば十通りの設計があるのがプログラミングです。時と場合によって引き出しから最適な技法を取り出す必要があります。

ここで紹介した技法は極々一部に過ぎません。SOLID原則, DRY原則, リスコフの置換原則, YAGNI, GoFの23のデザインパターン 等など、様々な原則、デザインパターン、テクニックが存在します。それらを武器に複雑なソフトウェアをクリーンなアーキテクチャとして構築することは、職業プログラマの責任であると共に最高の遊びです。

Twitter: @aki202


  1. Linus TorvaldsがGitを創造する前の世界であまねく使われていたバージョン管理ソフト。突っ込んだバイナリファイルがしばしば壊れる。 

  2. 【Linter…糸くずを除く機械】という語源通り、ソースコードの構文をチェックし、より良いコードを提案してくれる優れもの。プロジェクトの始動から半年経ってLinterを導入し、Warning件数が10,000件を超えるという様式美が存在する。 

  3. 1つのアプリケーションを構成するソースコード群のこと。単にソースコードを格好良く言いたいときにも使う。 

  4. Mixinは定義が曖昧で、プログラミング言語ごとに意味が微妙に異なる。広義の定義は「それ自体では動作せず、他のクラスから継承されることでそのクラスに機能を提供する」というようなもの。 

313
331
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
313
331