2024年1~3月に読んだ本

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方

大変参考になったので記事にした。

sasamuku.hatenablog.com

メタプログラミングRuby 第2版

大変参考になったので記事にした。

sasamuku.hatenablog.com

ChatGPT vs. 未来のない仕事をする人たち

タイトルに釣られて買ってみた。ChatGPT に心はあるのか、というテーマが面白かった。ChatGPT は9歳児相当の心を持っていると評価されている。機械なのだからそれは本当の心ではないと反論できそうだが、人間の心も外から観測不能ブラックボックス)なので、それらしく振る舞っていれば心があると評価できるのもなるほどと思った。

Rubyのしくみ -Ruby Under a Microscope-

メタプログラミングRuby 第2版 を踏まえて読むと、概念が構造として形になる感覚が得られてとてもよかった。例えば、オブジェクトにメソッドはなく、インスタンス変数とクラスへの参照があるだけだ、と説明されて終わるより、構造体(クラスポインタやインスタンス変数の配列)を図として捉える方が理解しやすい。Ruby を通じてコンピュータサイエンスの基本的な概念(ハッシュテーブル、クロージャなど)に触れられるのもよかった。

Web API The Good Parts

厳格な REST から一定の距離を置いたスタンスで記述されているのがよかった。例えば、検索のエンドポイントに search という単語を URI に含めてよいかは、REST 的には NG だが本書ではアリとしている。厳密な REST に従うことが必ずしも正しいとは限らない。エンドポイントの設計、HTTP の仕様、セキュリティなど API 設計に必要なことがある程度網羅されていた。

RSpec 向けの tree コマンドを作った

RSpec の追記やレビューをしていると全体構造を把握するのが辛いときがある。context が根深くなっていたり、単純に行数が長くなっていたり。(そんなテストを書いちゃいかんというのもある)

せっかくなので Gem の勉強も兼ねて CLI ツールを作ってみた。

github.com

こんな感じで使うことができる。

$ rspec_tree all /path/to/your_spec.rb
desc: Sample
desc: First describe
├─────ctx: First context
├───────it: should do something
├───────ctx: First nested context
├─────────it: should do something
├───────it_behaves_like: shared example
desc: Second describe
├─────ctx: Second context
├───────it: should do something else

ちなみに RubyMine だとエディタ上で綺麗に表示する機能があるらしい。

VSCode には OUTLINE という類似機能があるが満足する表示内容ではなかった。

実装

実装は spec ファイルを文字列として受け取り eval を実行している。モンキーパッチも当てまくっておりなかなかにひどい内容になっている。とりあえずは目的に叶うので時間があるときに見直していきたい。

有識者の方から parser gem を使って構文木を作成してみたらどうかとアドバイスをいただいた。コードを実行する必要はなく、テスト構造をツリー形式で出力できればよいだけなのでこちらも試してみたい。

メタプログラミング Ruby を読んだ

感想

書くとき、読むとき、レビューするときに、そのコードの解像度が広がるというか、奥に広がる世界にまで意識が届くようになった。読んでいても知識を押し付けられる感覚がないので、楽しみながら Ruby の奥深さを学ぶことができる。

Ⅰ部

1章 頭文字 M

メタプログラミングとは、コードを記述するコードを記述することである。

C++ のようなコンパイル型の言語では、コンパイルすると変数やメソッドはその実体を失う。コンパイル後にインスタンスメソッドのことをクラスに質問できない。Ruby のようなインタプリタ型の言語では、あらゆる言語要素 (変数、メソッド、クラス等) が実行時にも存在している。irb のような対話的にコードを打ち込めるシェルを使っていると、この辺りは実感としてはある。

Active Record は Movie#titleMovie#director= といったメソッドをこっそり定義している。

class Movie < ActiveRecord::Base
end

Active Record は目に触れる機会の多いメタプログラミングなのかもしれない。attr_reader とかも暗黙的にメソッドを定義しているので身近なメタプログラミングと言えるのだろうか。

2章 月曜日: オブジェクトモデル

オープンクラス

いつでも既存のクラスを再オープンして、その場で修正できる。この技法をオープンクラスと呼ぶ。

オブジェクトの中身

インスタンス変数

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new # この時点では @v は存在しない
obj.my_method # はじめて @v が存在できる

メソッド

オブジェクトにはメソッドはなく、インスタンス変数とクラスへの参照があるだけだ。

インスタンスそのものにメソッドが定義されているわけではない。ただ、「クラス MyClass が my_method を持つ」というのも誤解がある。この明確な呼び分けとして「インスタンスメソッド」と「クラスメソッド」がある。MyClass#my_methodMyClass.my_method のように表記する。

クラスの真相

Ruby のオブジェクトモデルを学ぶときに最も重要なのは「クラスはオブジェクト」ということだろう。

クラスはオブジェクトであり、クラスにもクラスがある。クラスのクラスは Class である。

String.class
#=> Class

Class クラスのインスタンスにはメソッドがある。

Class.instance_methods(false)
#=> [:allocate, :superclass, :subclasses, :new]

new なんかは分かりやすい。

Array クラスは Object クラスを継承している。つまり「配列はオブジェクトである」と言うことができる。

Array.superclass
#=> Object

Ruby ではあらゆるクラスのスーパークラスは Object になる。Object クラスは to_s のようなあらゆるオブジェクトで便利に使えるメソッドを持っている。

モジュール

Class のスーパークラスは Module だ。

Class.superclass
#=> Module

これはちょっと意外だった。Class はインスタンスを生成して使う、モジュールはインスタンスを生成せずに include して使う、のように使われ方の違いが明確であり、Class は Module であると言われると今一つしっくりこない。

クラスはオブジェクトの生成 new や継承元クラスを確認する superclass などの4つのインスタンスメソッドを追加した「モジュール」だ。[筆者要約]

定数

大文字で始まる参照は、クラス名もモジュール名も含めて、すべて定数だ。

定数と変数の違いはなにか。重要な違いは「スコープ」にある。定数のスコープは独自ルールに基づいている。

モジュールおよびクラスがディレクトリで、定数がファイルだ。

module M
  class C
    X = 'constant'
  end

  C::X
end

M::C::X

Rake の例

module Rake
  class Task
  ...
end

Task のような汎用的な名前の定数が衝突しないよう Rake というモジュールでまとめる。このようなモジュールを「ネームスペース」と呼ぶ。Task の完全な名前は Rake::Task となる。

いろいろまとめると

3章 火曜日: メソッド

  • 静的言語
    • 静的型チェックを持つ
      • すべてのメソッド呼び出しに対して、合致するメソッドをオブジェクトが持っているかどうかをコンパイラがチェックする
      • => コードを実行する前に、コンパイラがミスを指摘してくれる
  • 動的言語
    • 型チェックを持たない
      • メソッドの呼び出しをチェックするようなコンパイラは存在しない
      • => オブジェクトにメソッドが実装されていなくても実行可能

動的メソッド

メソッドを呼び出すというのは、オブジェクトにメッセージを送っていることなんだ。

Object#send をイメージすると分かりやすい。send を使うとメソッド名にシンボルが使える。コード実行時に動的に呼び出すメソッドを決定できる。これを動的ディスパッチと呼ぶ。

define_method を使えば、実行時にメソッド名を決定できる。これを動的メソッドと呼ぶ。

class MyClass
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = MyClass.new
obj.my_method(2) # => 6

ゴーストメソッド

オブジェクトにメソッドが見つからなければ、元のオブジェクトの method_missing を呼び出す。BasicObject の private インスタンスメソッドにそれはある。

method_missing をオーバーライドすると不明なメッセージを途中でキャッチして振る舞いを変えることができる。

class Lawyer
  def method_missing(method, *args)
    puts "You called: #{method}(#{args.join(', ')})"
    puts "(You also passed it a block)" if block_given?
  end
end

bob = Lawyer.new
bob.talk_simple('a', 'b') do
  # block
end
# =>
You called: talk_simple(a, b)
(You also passed it a block)

この特性をうまく活用して存在しないメソッド呼び出しに「あたかもそのメソッドがあるように」見せる手法をゴーストメソッドと呼ぶ。

動的メソッド vs ゴーストメソッド

可能であれば動的メソッドを使い、仕方なければゴーストメソッドを使う

ゴーストメソッドにはバグが生まれやすい。

4章 水曜日: ブロック

ブロックがスコープを制御するのに強力なツールだってことは、まだ知らないんじゃないかな?スコープというのは、変数やメソッドがどのコード行まで見えるかというものだ。

ブロックの基本

ブロックを定義できるのはメソッドを呼び出すときだけ。メソッドに渡されたブロックは yield を使ってコールバックされる。

def a_method(a, b)
  a + yield(a, b)
end

a_method(1, 2) {|x, y| (x + y) * 3} # => 10

例外が発生しても実行しなければいけない処理をシンプルに書くこともできる。

module Kernel
  def with(resource)
    begin
      yield
    ensure
      resource.dispose
    end
  end
end

# 呼び出し側
r = Resource.new
with(r) do
  # 何かしらの処理
end

ブロックはクロージャ

束縛

ブロックは「コード自体」と「束縛の集まり」の2つから構成される。ローカル変数、インスタンス変数、self といったものが束縛される。ブロックを定義した時点でそこにある束縛を取得し、メソッドに束縛ごと一緒に渡す。

def my_method
  x = "Goodbye"
  yield("cruel")
end

x = "Hello"
my_method {|y| "#{x}, #{y} world" } # => "Hello, cruel world"

x は「ブロックを定義したとき」に束縛される。ブロックからメソッドのローカル変数である x は見えない。

スコープ

local_variables を使ってローカル変数を確認することで、スコープの遷移を追跡できる。このコードでは、「トップレベルのスコープ」「MyClass のスコープ」「my_method のスコープ」の3つを往来している。あるスコープから他のスコープのローカル変数は見えない。

v1 = 1
class MyClass
  v2 = 2
  local_variables # => [:v2]
  def my_method
    v3 = 3
    local_variables
  end
  local_variables # => [:v2]
end
obj = MyClass.new
obj.my_method # => [:v3]
puts local_variables # => [:v1, :obj]

スコープゲート

スコープが変化する場所は3つある。これらはスコープゲート (スコープの出入り口) として振る舞う。

  • クラス定義
  • モジュール定義
  • メソッド

フラットスコープ

ローカル変数はスコープゲートを超えられない。

my_var = "Hello, World!"

class MyClass
  # my_var をここに表示したい

  def my_method
    # my_var をここに表示したい
  end
end

Class.newdefine_method を使えばスコープをフラット化できる。この技法をフラットスコープと呼ぶ。

my_var = "Hello, World!"

MyClass = Class.new do
  puts my_var

  define_method :my_method do
    puts my_var
  end
end

puts MyClass.new.my_method

余談だけど JavaScriptRuby のようにブロックを使わなくても関数自体がクロージャとして働いている。

JavaScript の関数はクロージャとなるためです。クロージャは関数とその関数が作られた環境という 2 つのものの組み合わせです。 クロージャ - JavaScript | MDN

instance_eval

instance_eval はレシーバを self にしてから評価される。スコープは移らないのでローカル変数にもアクセスできる。

class MyClass
  def initialize
    @v = 1
  end
end

v = 2

obj = MyClass.new
obj.instance_eval do
  puts self # => #<MyClass:0x00000001007353b0>
  puts @v # => 1
  @v = v
  puts @v # => 2
end

呼び出し可能オブジェクト

コードを塊として保管しておき、あとから呼び出す方式には以下がある。

  • ブロック
  • Proc
  • lambda
  • メソッド

ブロックはこれまで触れた通り、他の3つを確認していく。

Proc

Proc はブロックをオブジェクトにしたもの。

z = 3
inc = Proc.new { |x| x + z }
puts inc.call(2)  # => 5

lambda

Proc オブジェクトを生成する別の方法。

dec = ->(x) { x - 1 }
puts dec.class # => Proc
puts dec.call(2) # => 1

Proc と lambda は「return の挙動」と「引数チェックの有無」に違いがある。

メソッド

メソッドも Method オブジェクトとして取り出し可能。

class MyClass
  def initialize(value)
    @x = value
  end

  def my_method
    @x
  end
end

obj = MyClass.new(1)
m = obj.method :my_method
puts m.class # => Method
puts m.call # => 1

ブロックや Proc が定義されたスコープで評価されるのに対し、メソッドはオブジェクトに束縛され、オブジェクトのスコープで評価される。

5章 木曜日: クラス定義

Ruby のクラス定義は実際に「コードを実行」している。

クラス定義

カレントクラス

クラス定義の中では、そのクラス自身がカレントオブジェクト self になる。それと同様に「カレントクラス」という概念も持っている。クラス内でメソッドを定義すると、それはカレントクラスのインスタンスメソッドとなる。

  • def で定義される全てのメソッドは、カレントクラスのインスタンスメソッドとなる
  • クラス定義の中では、「カレントオブジェクト self = カレントクラス」となる
  • クラスへの参照があれば class_eval でクラスをフラットスコープでオープンできる

クラスインスタンス変数

クラスは Class クラスのインスタンスであり、インスタンス変数を持つことができる。全てのインスタンス変数はカレントオブジェクト self に属している。クラスも例外ではない。

class MyClass
  @my_var = 1
  def self.read; puts @my_var; end
  def write; @my_var = 2; end
  def read; puts @my_var; end
end

obj = MyClass.new
obj.read # => nil
obj.write
obj.read # => 2
MyClass.read # => 1

このようにクラスに属するインスタンス変数を「クラスインスタンス変数」と呼ぶ。

クラス変数

ちなみに @@ プレフィックスをつけた「クラス変数」もある。クラスインスタンス変数とは異なり、サブクラスやインスタンスメソッドからもアクセスできる。さらに、クラス階層間で共有される特性がある。

class MyClass
  @@v = 1

  def self.read
    puts @@v
  end
end

MyClass.read # => 1

class SubClass < MyClass
  @@v = 2
end

MyClass.read # => 2
SubClass.read # => 2

特異メソッド

特定のオブエジェクトに追加したメソッドを「特異メソッド」と呼ぶ。特異メソッドは、オブジェクトのクラスに影響を与えない。つまりそのオブジェクトにのみ追加される。

str = 'hogehoge'

def str.title?
  self.upcase == self
end

puts str.title? # => false

クラスメソッドはクラスの特異メソッド

特異メソッドの構文は常にこうなる。

def object.method
  # メソッドの中身
end

クラスメソッドもこの構文に漏れない。つまりクラスメソッドはクラスの特異メソッドである。Class クラスのオブジェクトにメソッドを追加している、と言い換えても同じ。

class MyClass; end

def MyClass.read
  puts @v
end

def MyClass.write
  @v = 1
end

MyClass.write
MyClass.read

#### クラスマクロ クラス定義の中で便利に使えるクラスメソッドを「クラスマクロ」と呼ぶ。attr_* 族のようにクラス定義の中でキーワードのように便利に使えるものを指す。

特異クラス

特異メソッドはどこに定義されているのだろうか。オブジェクトはクラスへの参照を持つだけであり、インスタンスメソッドはクラスに定義されているはずだ。

def MyClass; end

obj = MyClass.new
def obj.my_method; end

obj.my_method # MyClass に my_method はない

この答えが「特異クラス」である。

class << an_object という特別な構文を使うことで、特異クラスのスコープに連れて行ってくれる。

obj = Object.new
puts obj.class # => Object

singleton_class = class << obj
  self
end

puts singleton_class.class # => Class

このような手続きを踏まなくても singleton_class メソッドを使うことで簡単に特異クラスを参照できる。

puts obj.singleton_class # => #<Class:#<Object:0x000000010097d5c8>>

特異クラスの特徴

  • Object#singleton_class や class << を使わないと見れない
  • 特異クラスはインスタンスを1つしか持てない
  • 継承ができない
  • 特異クラスはオブジェクトの特異メソッドの住処
  • 特異クラスは継承チェーンの一番下に置かれる

メソッド探索

class C
  def a_method
    'C#a_method()'
  end
end

class D < C; end

obj = D.new
obj.a_method # => "C#a_method()"

このコードのオブジェクトモデルの世界を表すと下図になる。

特異クラスをオープンしてインスタンスメソッドを追加する。

class << obj
  def a_method
    'D#a_method()'
  end
end

obj.a_method # => "D#a_method()"
obj.singleton_class # => #<Class:#<D:0x000000010092cad8>>
obj.singleton_class.class # => Class
obj.singleton_class.superclass # => D

これをオブジェクトモデル図に反映すると下図になる。

特異クラスとクラスメソッド

特異クラスとは特定のオブジェクトに追加されたメソッドが置かれる場所だった。クラスメソッドも同様に Class クラスのオブジェクトに特別に追加されたメソッド、つまり特異メソッドである。

クラスメソッドを上記のコードに追加してみる。

class C
  class << self
    def a_class_method
      'C.a_class_method'
    end
  end
end

C.a_class_method # => "C.a_class_method"

特異クラスとそのスーパークラスを訪ねてみる。

C.superclass # => Object
D.superclass # => C
C.superclass.superclass # => BasicObject
C.superclass.superclass.singleton_class # => #<Class:BasicObject>
C.singleton_class # => #<Class:C>
D.singleton_class # => #<Class:D>
D.singleton_class.superclass # => #<Class:C>
C.singleton_class.superclass # => #<Class:Object>

オブジェクトモデル図にまとめるとこのようになる。

特異クラスのスーパークラスが、スーパークラスの特異クラスになっている。どうしてこんな複雑なことをするのか。それはこう配置することでサブクラスからもクラスメソッドを呼び出せるようになるからだ。

D.a_class_method # => C.a_class_method

説明を付け加えると、クラス D がクラスメソッド a_class_method を実行するとき、それは D の特異クラス #D のインスタンスメソッドである。インスタンスメソッドは継承チェーンを上に登っていく。#D にないのであれば、次に見に行くのは...。

クラス拡張

クラスメソッドをモジュールでインクルードできるか。

module MyModule
  def self.my_method; puts 'hello'; end
end

class MyClass
  include MyModule
end

MyClass.my_method # => NoMethodError

なぜエラーになるかというと、クラスがモジュールをインクルードして得られるのはインスタンスメソッドだからだ。クラスメソッドを得るには、「特異クラスのインスタンスメソッド」にしなければならない。

module MyModule
  def my_method; puts 'hello'; end
end

class MyClass
  class << self
    include MyModule
  end
end

MyClass.my_method # => hello

my_method は MyClass の特異クラスのインスタンスメソッドである。つまり、my_method は MyClass のクラスメソッドになった。この技法を「クラス拡張」と呼ぶ。

わざわざ特異クラスをオープンしなくても、Object#extend を使えばよい。これはレシーバの特異クラスにモジュールをインクルードするためのショートカットである。

module MyModule
  def my_method; puts 'hello'; end
end

class MyClass
  extend MyModule
end

MyClass.my_method # => hello

6章 金曜日: コードを記述するコード

Kernel#eval

コードを文字列として実行して、その結果を返す。

arr = [10, 20]
element = 30
eval('arr << element') # => [10, 20, 30]

Binding オブジェクト

スコープをオブジェクトにして返す。Binding でスコープを取得すれば、そのスコープを持ち回ることができる。eval と組み合わせて後からそのスコープでコードを実行できる。

class MyClass
  def my_method
    @v = 1
    binding
  end
end

b = MyClass.new.my_method

eval '@v', b # => 1

irb は標準入力やファイルをパースして、各行を eval に渡している。Binding を使って異なるコンテキストでも実行できるようになっている。

# workspace.rb
eval(statements, @binding, file, line)

eval vs. block

Kernel#eval と class_eval や instance_eval は、コードを文字列で実行するか、ブロックとして実行するかの違いしかない、というのは誤りである。instance_eval もコードを文字列で評価できる。

ではどちらを使うべきなのか。基本的にはコード文字列を避けるべきである。

コード文字列を避けるべき理由

  • シンタックスハイライトや自動補完が効かない
  • コードインジェクションの標的になる

フックメソッド

クラスが継承されたときや新しいメソッドを獲得したとき、このようなイベントが起きたときに実行されるメソッドを「フックメソッド」と呼ぶ。イベントに「フックをかける」ことからこのように呼ばれる。

Class#inherited はクラスが継承されたときに Ruby が自動的に呼び出してくれる。デフォルトでは何もしないので、オーバーライドして使う。

class String
  def self.inherited(subclass)
    puts "#{self} was inherited by #{subclass}"
  end
end

class MyString < String; end

# Output:
# String was inherited by MyString

クラスのライフサイクルにプラグインする Class#inherited などと同様に、モジュールのライフサイクルにプラグインするものもある。

module M1
  def self.included(othermod)
    puts "M1 was included into #{othermod}"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2 was prepended to #{othermod}"
  end
end

class C
include M1
prepend M2
end

# Output:
# M1 was included into C
# M2 was prepended to C

フックメソッドを活用した最終的なサンプルコードはこのようになる。

module CheckedAttributes
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
      define_method "#{attribute}=" do |value|
        raise 'Invalid attribute' unless validation.call(value)
        instance_variable_set('@#{attribute}', value)
      end

      define_method attribute do
      instance_variable_get("@#{attribute}")
      end
    end
  end
end

class Person
  include CheckedAttributes
  
  attr_checked :age do |v|
    v >= 18
  end
end

Ⅱ部

9章 Active Record の設計

オートローディング

require 'active_record' したときに読み込まれるファイル。

github.com

Active Record は Active Model と Active Support の2つのライブラリに大きく依存している。Active Support::Autoload モジュールを extend して autoload をクラスマクロとして使用する。これはモジュールを初めて呼び出したときに自動的にソースコードを require するというもの。これにより active_record を require するだけで配下の様々なモジュールを利用できる。

ActiveRecord::Base

ActiveRecord::Base にロジックはなくモジュールを include あるいは extend するだけ。オートローディングの仕組みによって require してからモジュールを include する必要がない。

github.com

ActiveRecord::Validations

ActiveRecord::Base クラスは ActiveRecord::Validations モジュールを include している。valid? メソッドはここで定義されている。

github.com

10章 Active Support の Concern モジュール

ActiveSupport::Concern モジュールがあることで、クラスが include するモジュールにいちいちフックメソッドを定義しなくてよくなる。

Concern 以前の Rails

ActiveRecord::BaseValidations を include すると以下のことが起きる。

module ActiveRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def validates_length_of(*args)
        # ...
      end
    end

    def valid?
      # ...
    end
  end
end

このコードの課題は、モジュールに重複したフックメソッドが定義されること。

class Base
  include Validations
  extend Validations::ClassMethods
  # ...
end

このように書けば同じ目的を達成できる。extend の1行は追加されるが問題ないと思うかもしれない。これにはもっと深刻な問題が隠されている。それは、モジュールを入れ子で include したときに起きる。

module SecondLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def second_level_class_method
      "Second level class method"
    end
  end

  def second_level_instance_method
    "Second level instance method"
  end
end

module FirstLevelModule
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def first_level_class_method
      "First level class method"
    end
  end

  def first_level_instance_method
    "First level instance method"
  end
end

class BaseClass
  include FirstLevelModule
end

BaseClass.new.first_level_instance_method # => "First level instance method"
BaseClass.new.second_level_instance_method # => "Second level instance method"

BaseClass.first_level_class_method # => "First level class method"
BaseClass.second_level_class_method # => NoMethodError

second_level_class_methodBaseClass のクラスメソッドではなく、FirstLevelModule のクラスメソッドとなる。

ActiveSupport::Concern

クラスメソッドを定義するためにフックメソッドを定義しなくてもよくなる。

require 'active_support'

module MyConcern
  extend ActiveSupport::Concern

  def an_instance_method; "an instance method"; end

  module ClassMethods
    def a_class_method; "a class method"; end
  end
end

class BaseClass
  include MyConcern
end

BaseClass.new.an_instance_method # => "an instance method"
BaseClass.a_class_method # => "a class method"

ActiveModel::Validations

validateActiveRecord::Base クラスのクラスメソッド (クラスマクロ) として利用される。ソースコードより ActiveSupport::Concern を extend し ClassMethods モジュールに validate メソッドが定義されていることが分かる。

github.com

オブジェクト指向設計実践ガイドを読んだ

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山 泰基 |本 | 通販 | Amazon

重要だと思った箇所をメモに残した。 八、九章で力尽きてしまったのでまた気が向いたら読む。

第一章: オブジェクト指向設計

設計とは

設計とは、同一の製品をつくる組み立てラインではなく、アトリエなのです。

原則

  • SOLID 原則
    • 単一責任 (Single Responsibility)
    • オープン・クローズド (Open-Closed)
    • リスコフの置換 (Liskov Substitution)
    • インターフェース分離 (Interface Segregattion)
    • 依存性逆転 (Dependency Inversion)
  • DRY (Do not repeat yourself)
  • LoD (Low of Demeter)

手続き型言語との対比

手続き型言語は、データと振る舞いは完全に別物になっているのが特徴。Ruby はデータと振る舞いを1つのオブジェクトにまとめる。Ruby は文字列もオブジェクトであり、言語構文に組み込まれているわけではない。Ruby ではプログラミング言語に期待されるデータ型の全てに対し、前もってクラスが用意されている。

まとめ

オブジェクト指向の目的は「変更を容易にすること」にある。よい設計は、理論を実践に変換する能力にかかっている。

第二章: 単一責任のクラスを設計する

凝集度

クラス内の全てがそのクラスの中心的な目的に関連していれば、「凝集度が高い」と言える。

DRY

DRY はただ同じ記述を繰り返すな、と言っているわけではない。単一責任のクラスを実現すればどのような振る舞いもただ1箇所にのみ存在するようになる。原則を満たした結果として表出するのが「DRY という状態」であり、DRY にすること自体が目的ではない。

変更を歓迎するコード

  • インスタンス変数の隠蔽(カプセル化
    • 「クラス自身からインスタンス変数を隠蔽する」のはインスタンス変数の直接参照にリスクがあるから
    • 直接参照は変更に弱いためラッパーメソッドで抽象化し、必要な知識を最小限に留めるが目的だと解釈
  • カプセル化とは
  • 本書内における「メッセージを送る」とは
  • データ構造の隠蔽
    • 複雑な構造への直接参照は混乱を招く
    • Ruby の Struct は配列などのデータ構造に関する知識(とりわけどのインデックスにどのデータがあるなど)を剥がすのに便利
    • Struct とは
      • 新しいクラスを作るほどではないが、いくつかの属性を1つに束ねておくのに便利

第三章: 依存関係を管理する

依存関係とは

オブジェクトとオブジェクトの間に生まれる関係の1つ。A が B のことをどれだけ知っているか、それを知っていることは適切なことなのか、を考える必要がある。A を変更するとき、B も変更しなければいけないなら、B は A に依存していると言える。

疎結合なコードを書く

  • 依存オブジェクトの注入
    • オブジェクトのクラスではなく「送ろうとしているメッセージ」こそが重要!!
    • この視点の逆転こそがオブジェクト指向の真髄
    • これがダックタイピングに通ずる道になる
    • 特定の振る舞いを持つ任意のオブジェクトであれば誰とでも共同作業できる

依存方向の選択

自身より変更されないものに依存しなさい

第四章: 柔軟なインターフェースをつくる

インターフェースとは

クラス内にあるメソッドのこと。パブリックインターフェースは外部から呼ばれることを想定して公開しているメソッドを指す。

他の意味のインターフェースとして、要求されるメソッドを実装するクラスはどんなクラスであれその「インターフェース」のように振る舞うというものがある。これは「型」としてのインターフェースであり、ダックタイピングを扱う上で重要な概念になる。

インターフェースの定義

レストランとお客さんの例が分かりやすい。厨房では多くのことが行われるが、お客さんに公開されるのはメニューだけ。どの料理を頼むかだけを指定すればよく、それが中華鍋で作られるのか、レンジで作るのかは知る必要がない。お客さんが「料理の仕方」を知ってしまうとき、料理方法が変わったらお客さんにも訂正しないといけなくなる。

パブリックインターフェース・プライベートインターフェース

パブリック | プライベート

  • クラスの
    • 主要な責任を明らかにする | 実装の詳細に関わる
  • 外部から実行され
    • る | ない
  • 変更され
    • にくい | やすい
  • 依存するのは
    • 安全 | 危険
  • テストで
    • 文書化される | されないことが多い

コンテキストを最小限にする

パブリックインターフェースを構築するときは、そのパブリックインターフェースが他者に要求する「コンテキストが最小限」になることを目指す。

第五章: ダックタイピングでコストを削減する

ダックタイピングとは

インターフェースで定義したメソッドを持つオブジェクトはその型として扱う(それが本当は何であれ)。重要なのは「何であるか」ではなく「何をするか」なのだ。これが冒頭にもあったオブジェクトの「クラスではなくメッセージが重要だ」という主張につながる。

隠れたダックを認識するために

以下が出てきたらダックタイピングを導入する余地があると考える

  • クラスで分岐する case 文
  • kind_of?
  • respond_to?

具象的なコードの危険性

具象的なコードは理解するのは簡単だが、拡張するにはコストを伴う。いつだって抽象は分かりにくいが、その拡張性は大きな力になる。

ダックを信頼する

オブジェクトを信頼する。信頼に足るオブジェクトを設計するのが設計者の仕事。

第六章: 継承によって振る舞いを獲得する

継承とは

本質的には「メッセージの自動委譲」の仕組みと言える。オブジェクトが理解できなかったメッセージの転送経路を定義するもの。メッセージの自動委譲によるコード共有方法には「モジュール」もある。

スーパークラスの作り方

継承のルール

  • オブジェクトが「一般 - 特殊の関係」になっている
  • 正しいコーディングテクニックを使っている

正しいコーディングテクニックとは

スーパークラスとサブクラスを疎結合にする

「フックメッセージ」を作る。フックメッセージは、サブクラスがぞれに合致するメソッドを実装することで、情報を提供できるようにするための専用のメソッド。具体的にはスーパークラスでメッセージ送信と実装の両方を行い、サブクラスで実装をオーバーライドする。サブクラスはスーパークラスについて知るべきことを少なくできる。サブクラスは実装したメソッドが何らかのオブジェクトによって、何らかのタイミングで呼び出されると想定するだけでよい。サブクラスに必要なのはテンプレートメソッドを実装するだけ(申請フォームに必要事項を記入するかのように)。

第七章: モジュールでロールの振る舞いを共有する

モジュールとは

Ruby において、ある振る舞いをオブジェクト (クラスやクラスのインスタンス) に混ぜ入れる方法のこと。メソッドの集合に名前をつけてグルーピングできる。ダックタイピングがメソッドのシグネチャを共有するのみだったのに対し、一箇所に定義された特定の振る舞い (多くの場合は複数のメソッドから成る) をオブジェクト間で共有することができる。

クラスとモジュール

「である (is-a)」「のように振る舞う (behaves-like-a)」の違い。クラスは揺るぎないが、モジュールは役職(ロール)のように取り外し可能。

extend

include がクラスにメソッド探索の経路を追加する (つまり応答できるメッセージが増える) のに対し、extend は何をしてくれるのか。extend はモジュールの振る舞いをオブジェクトに直接追加する。クラスをモジュールで extend すると「そのクラス自体に」クラスメソッドとして追加される。クラスのインスタンスを extend すると「そのインスタンス自体に」インスタンスメソッドとして追加される。これはクラスも単なるオブジェクトに過ぎないことを表す。

リスコフの置換原則 (LSP)

SOLID 原則の「L」。スーパークラスが使えるところではサブクラスが使えるという原則。派生型は上位型と常に置換可能であるということ。

第八章: コンポジションでオブジェクトを組み合わせる

後で書く。

第九章: 費用対効果の高いテストを設計する

後で書く。

ISUCONに初挑戦しました

ISUCON13 に会社の同僚と初挑戦しました!
チーム全員が ISUCON 初挑戦な上に、あまり時間も取れずほぼ準備なしで臨みました。

結果

7,970 点でした!(全チームスコア
初期値から1点でも上がれば御の字くらいのモチベーションだったので普通に嬉しいです。

言語

業務で全員が使っていたので Ruby で全会一致でした。

リポジトリ

https://github.com/sasamuku/isucon13

準備

ISUCON の前々日がたまたま祝日だったので、全員で最初で最後の MTG をしました。流石に右も左も分からないまま終わるのは嫌だったので最低限のすり合わせをしました。

  • 同僚が用意してくれたサーバに過去問の環境を作成
  • Git を設定して pull してデプロイまでの流れを確認
  • パフォーマンスツールの設定と見方の確認
    • stackprof
    • Newrelic APM

時間が余れば実際に改善してみようと思ったのですが、夜遅くなってしまい断念😌

当日の流れ

マニュアル読み合わせ

初参加ということもあり、失格するのが一番避けたいことだったので、当日マニュアルはしっかり読み合わせました。

環境構築と初回のベンチ実行まで問題なく進み一安心。

インスタンスが3台あるけどスコア取られるのは1台だけだよな?ん?となりましたが、DB サーバと分けたり、アプリケーションサーバ複数台構成とかするためだと後から知る。

また、途中に DNS の記載がありましたが、僕らはおそらく手を付けることはないだろうと読み飛ばしました。

Git の設定

前々日に手順を確認していたのでチームメンバーがスイスイと設定してくれました。デプロイ担当は1人の方がいいとどこかで読んだので毎度同じメンバーに依頼してデプロイしてました。

パフォーマンスツールの設定

stackprof と Newrelic APM を入れました。どちらかでよいとは思いつつ、stackprof を使いこなすまで僕らが成熟してなかったので、Newrelic APM で全体感だけでも分かればいいなくらいの気持ちでした。APM でクエリのスパンまで見れたらよかったのですが、設定が足りなかったのか見れず。

それでも `GET /api/user/:username#statistics` がなんかゲロ遅いなとか気付く材料にはなりました。

(実際の画面)

開発環境構築

コードや設定を変更してデグレるのが怖かったので、docker-compose で立ち上がる開発環境を作ろうとしました。ここにメンバー全員と3~4時間という膨大なリソースを投入してしまいました...

そのため実質改善に回せた時間は2時間もなかったように思います 涙
この期限を過ぎたら諦めるみたいな縛りが必要だったかも。
しかも作った開発環境は結局使う余裕がなかった😂

改善

チームメンバーが Index を貼りまくり、僕は適当にそれっぽいところに LIMIT 句を追加したり、怪しそうなクエリを消して、実装側で処理させるようにしたりました。

多分実際にスコアに効いたのは Index だけだったのかな。

とにかく時間がなかったのでバーっとやって、最後にパフォーマンスツールを停止して無事タイムアップでした。

反省点

一番の反省はやっぱり開発環境構築にこだわりすぎてしまったことです。ローカルで試さずに本番デプロイなんてできない!という真面目なメンバーが集った結果ですw

もちろん過去問を解いたりも事前にできてればよかったのですが時間なかったのでしゃあなし。

感想

Web エンジニアなら誰もが耳にする ISUCON。
それに実際に出てスコアを上げることもできて御の字でした。

チームを組んでくれた同僚と運営の皆さんに感謝したいと思います。
来年こそは2万点くらい取れるようにしっかり準備したいです!

チームトポロジーを読んだ

ざっくり感想

慣習的なチーム構造での経験が大半を占める自分にとっては目からウロコな内容だった。慣習的なチーム構造とは、大きな1つのチームがあり、複数の案件に担当者をアサインするというもの。往々にして案件は兼務になっていて、1人あたり2~4の異なるコンテキストを日々行き来する。チームトポロジーはこうしたチーム構造へのアンチテーゼになっている。

チームトポロジーというと、ストリームアラインドやら X-as-a-Service といった概念が先行してしまいがちだったが、もっと普遍的で汎用的なチーム設計の考え方を学べる1冊となっている。

以下に要約と個人的補足を書く。

チームトポロジーとは

典型的な組織図に縛られない「新しいチームの構造」であり、ソフトウェアを構築・運用する上での効果的なアプローチとされている。

4つのチームタイプと3つのインタラクションモードを定義している。

  • チームタイプ
    • ストリームアラインドチーム (*1)
    • イネイブリングチーム
    • コンプリケイテッド・サブシステムチーム
    • プラットフォームチーム
  • インタラクションモード

役割の異なる4つチームタイプを、3つのインタラクションモードで接続するというコンセプトである。API やシステムの設計と考え方が似ているため、エンジニアにとってイメージしやすいものになっている。

(*1) ストリームとは、ビジネスドメインとして独立可能な業務の流れ(開発、リリース、フィードバック)を指している。

何を解決するのか

本書ではチームトポロジーの最終ゴールをこう述べている。

最終的なゴールは、顧客のニーズに合うソフトウェアをチームがより簡単に構築、実行し、オーナーシップをもてるようにすることである。

チームトポロジーに順じた組織設計により、チームの責任境界が明確になり、認知負荷が軽減され、フローを邪魔する調整やコンテキストスイッチも少なくなる。結果として、ソフトウェアデリバリーを高速化し品質も向上できる。

チームトポロジーを支える思想

チームトポロジーの土台となる思想が2つある。「逆コンウェイ戦略」と「チームファースト思考」である。

コンウェイ戦略

チームトポロジーコンウェイの法則を確たる土台として設計される。

コンウェイの法則:
システムを設計する組織は、その構造をそっくりまねた構造の設計を生み出してしまう。

コンウェイの法則によれば、組織が先にあるとそれにソフトウェアの構造が依存する。結果として得られるアーキテクチャは本来あるべき姿とは異なっている可能性がある。

この法則を逆手に取ったのが逆コンウェイ戦略である。

コンウェイ戦略:
システムに反映したいアーキテクチャーに合うようなチーム構造にする

あるべきソフトウェアアーキテクチャの青写真を先に描き、それに一致する構造のチームを組織する。すると、コンウェイの法則によって「自然と」望ましいソフトウェアアーキテクチャが得られる。

チームファースト思考

チームファースト思考は、個人プレーよりチームプレーを尊重し、チームとしていかに効果的に働くかを考える。中心的な関心事には、サイズ、寿命、認知負荷などがある。

  • チームのサイズ
    • 効果的に働けるチームの最大サイズ: 7~9人
    • いわゆる Two-pizza チーム
  • チームの寿命
    • 立ち上げから一体的に働けるようになるまで3ヶ月かかる
    • メンバーのアサインし直しは無価値
    • チームを長く安定させ仕事が流れ込むようにする
  • チームの認知負荷
    • チームが扱える認知負荷には限度がある
    • 限度を超えるとチームは「ただの個人の集まり」のように振る舞う
    • チームが扱うシステムや作業範囲を制約する必要がある

チームトポロジーへの帰着

コンウェイ戦略によって、あるべきソフトウェアのアーキテクチャに合わせてチームの構造を決定することで自然な力学のもとで両者のアーキテクチャが一致するようになる。

そうしてチーム構造を決定するときに、チームファースト思考を取り入れながら、チームのサイズ、認知負荷は適切なものかケアしなければならない。

読んでいる最中はチームタイプやインタラクションモードなどキャッチーな概念に目が行っていたが、こうして振り返ると、この2つの考え方こそが普遍的かつ汎用的で、どの時代どの組織でも役立つ「チーム設計の指針」になるのかもしれない。

 

Lean と DevOps の科学を読んだ

この本の主張

この本のキーメッセージは以下の通り。

ソフトウェアデリバリのパフォーマンス向上はビジネスの競争優位性に好影響をもたらす。ソフトウェアデリバリのパフォーマンスは Four Keys と呼ばれる4指標によって計測される。

  • リードタイム
    • ファーストコミットから本番リリースまでの時間
  • デプロイ頻度
    • 時間あたりのデプロイ回数
  • 平均修復時間(MTTR
    • インシデントが発生してから復旧するまでの平均時間
  • 変更失敗率
    • 本番稼働に失敗した数 / 本番リリース数

この本がすごいのは、経験論ではなくアカデミックな調査によって一般化されているところ。つまり、こうした指標の向上が組織にとってプラスであると根拠を持って主張しているのだと t_wada さんが Podcast で仰っていた。

open.spotify.com

読んだきっかけ

読もうと思ったのは開発生産性カンファレンスへの参加がきっかけ。今現在チームを束ねる立場に就いている訳ではないが、開発生産性を上げるためのヒントを求めて参加した。ちなみに参加時点では Four Keys もチームトポロジーも知らなかった。

dev-productivity-con.findy-code.io

読んでみた感想

組織や開発生産性の改善に向けて取り組む"入り口"として最適な本だと感じた。ソフトウェアデリバリのパフォーマンス改善が結果としてビジネスにも好影響をもたらすという事実は、これから改善に取り組む人にとって強力な後押しになる。

イマイチな点は、理解の難易度がかなり高かった。というか腹落ちしてない箇所のが多い。章同士の繋がりが分かりにくく、全体像の把握がとても難しかった。その意味で本書は開発生産性の改善に向けた"入門本"としては相応しくないと思う。

印象に残った言葉

>パフォーマンスの改善と、安定性と品質の向上との間に、トレードオフの関係はない (P.26)

リリース速度を上げるなら、品質を犠牲にしなければいけないというのが固定観念になっていたが、事実として、リリース速度が高い組織は品質も高いのだ。

>DevOps のプラクティスを実践すれば組織文化に好影響を与え、改善しうる (P.38)

こう言い切ってくれるのは勇気をもらえる。

>関係者の思考方法を変えることではなく、関係者の言動、つまり皆が何をどう行うかを変えること (P.48)

先に変えるのは思考ではなく行動であると。そして行動を変えた結果に思考が追いつくということだろう。最近 Timee 社の発表でも似たようなことが話されていた。

ボトムアップでSLOを導入 2年半運用して分かった失敗と変化 - Speaker Deck

>個人と組織の価値観の一致度を上げればアイディンティティを強化でき、パーンアウトを緩和できる (P.127)

仕事の中でも個人と組織の価値観一致の重要性はひしひしと感じている。価値観一致を果たすツールとして Mission/Vision/Value や理念があったりする。逆にそれらツールの浸透がおざなりになっているとここはかなり難しい。

>リーダーこそがその権限や予算を使って変革を行えるのである (P.141)

当たり前のことなのだけど、やはり権限を持たないメンバー層が変革を行うのはハードルが高いよなと。もちろん発案は大切だが、実行権が伴わないので発案止まりになりがち。権限の委譲が進んでいないなら、ハードルを下げるための工夫がないとボトムアップな改革は望めないなと改めて感じた。