はじめに
駆け出しプログラマだった頃の自分に教えたい、8つのプログラミング技法について纏めます。本エントリでは主にRubyを用いますが、ここで紹介する技法はあらゆる言語に適用できます。
8つの技法はリーダビリティと関心の分離の2つに分類できます。サンプルコードを添えて可能な限り具体的に解説していきます。
対象読者
- コードを書くのに慣れてきた人
- けどもっと良い書き方はないか?と考えている人
リーダビリティ - Readability
リーダビリティとは、アーキテクチャに関係しない純粋なコーディングの綺麗さです。可読性とも呼ばれます。私達は仕事時間の相当量をコードの読解に当てています。コードの読解は短期記憶をフル活用するため、コーディング作業そのものよりも知的負担が大きくなりがちです。同僚の書いたコードは勿論、先週の自分が書いたコードでさえ記憶の奥底から掘り出すのに時間が掛かることもしばしばです。ましてや一年前、二年前に書いたコードなら尚更でしょう。
リーダビリティの低下は、バグを生む、機能追加のコストが上昇する、仕様変更に弱くなる、メンバーの精神が不安定になるなど、種々の厄災の温床になります。ではリーダビリティを維持するためにどのようなテクニックがあるのでしょうか。
1. Guard Clause
boolean型の2つの引数flag_a
, flag_b
を取るメソッドf1
を考えます。f1
はflag_a
がtrue且つflag_b
がfalseの場合にだけメソッドf2
をコールします。
駆け出しの私は恐らく次の様なコードをCVS1にコミットしていたことでしょう。
def f1(flag_a, flag_b)
if flag_a
unless flag_b
f2
end
end
end
これは不必要にネストを深くしてしまっている悪いコードです。もう少しマシなバージョンは次のようなコードです。
def f1(flag_a, flag_b)
if flag_a && !flag_b then
f2
end
end
ネストを下げられましたが、まだ読み手の脳に充分に優しいとは言えません。なぜなら条件文が論理演算子(andやor)を含むとリーダビリティは途端に下がるからです。
今の私ならこのように書きます。
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フラグを受け取る送金メソッドを考えます。
def submit(number, sendmail, userA, userB)
userA.minus(number)
userB.add(number)
userB.notify if sendmail
end
submit
やnumber
など、広い意味を持つ英単語が使われています。変数名やメソッド名を適当に決めてしまうことは、駆け出しプログラマが必ず通る道です。
上のコードを読んだ同僚(もしくは半年後の自分)は、このsubmit
メソッドをコールしている箇所を探し出し、前後の文脈からsubmit
メソッドの用途を推測しなくてはいけません。
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. 説明変数
書いている本人は理解できるものの、同僚にとっては理解に時間が掛かるコードは沢山あります。例えば次のようなものです。
if Time.parse(params[:datetime]) < DateTime.now.prev_year.to_time
# params[:datetime]が一年以上前の時に実行する処理
end
条件文が複雑でどういう意図のif文なのかすぐには理解できません。そこで説明変数の出番です。
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. インデント
インデントはタブであるべきかスペースであるべきかという宗教戦争は、未だ決着を見ずインターネットの各地で繰り広げられています。非プログラマ達は「死ぬほど興味がない」と思いこの戦争を眺めていますが、筆者の見解も「どっちでもいいんじゃない」です。それよりも重要視するべきは一貫性です。赤軍も白軍もこの点においては同意してくれることでしょう。
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
どこまでがクラス定義でどこまでがメソッド定義か、使われているキーワードを読まないと判別できません。ここまで読み辛いコードなら、新人プログラマであっても何が悪いか気付けるでしょう。しかし最初はみんな上のようなコードを書くものです。
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ボタンを押下してくれるでしょう。しかしもう少しやれることは残っています。
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(後述)のメソッドをコールし、自身でビジネスロジックを持ちません。
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がビジネスロジックを持ってしまっています。
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。
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つのインスタンスメソッドがクラス内に定義されています。
# 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メソッドがあったとします。
# 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に処理を任せてしまいましょう。
# 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
-
Linus TorvaldsがGitを創造する前の世界であまねく使われていたバージョン管理ソフト。突っ込んだバイナリファイルがしばしば壊れる。 ↩
-
【Linter…糸くずを除く機械】という語源通り、ソースコードの構文をチェックし、より良いコードを提案してくれる優れもの。プロジェクトの始動から半年経ってLinterを導入し、Warning件数が10,000件を超えるという様式美が存在する。 ↩
-
1つのアプリケーションを構成するソースコード群のこと。単にソースコードを格好良く言いたいときにも使う。 ↩
-
Mixinは定義が曖昧で、プログラミング言語ごとに意味が微妙に異なる。広義の定義は「それ自体では動作せず、他のクラスから継承されることでそのクラスに機能を提供する」というようなもの。 ↩ ↩2