30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Ruby のクラスの初歩

Rubyのクラスの理解が曖昧だったので、整理して記録しておく。
Railsをやるにしても、ここらへんをきちんと理解しておくのは前提だと思う。

動作確認しているRubyのバージョンは2.5.3

用語の整理

オブジェクトとインスタンスは、同じものを指していることが多い。
文脈によっては、オブジェクトをレシーバと呼ぶこともある。メソッドを受け取るという意味。
user.first_nameなら、userは、first_nameのレシーバ。
そしてこの場合、メソッドをメッセージと呼ぶことがある。つまりfirst_nameがメッセージ。そのメッセージを受け取るのが、userというレシーバ。

この記事では出てこないが、インスタンスメソッドのことをクラス名#メソッド名と表記し、クラスメソッドのことはクラス名.メソッド名もしくはクラス名::メソッド名と表記することがある。

Rubyでは万物がオブジェクト

数値やnilもオブジェクト。

1.to_s # => "1"
1.class # => Integer
nil.to_s # => ""
nil.class # => NilClass

そしてクラスもまた、オブジェクトである。

Integer.class # => Class
NilClass.class # => Class

基礎

クラスを定義するにはclass構文を使う。
クラス名はアッパーキャメルケースにするのが慣習。

クラス名.newでインスタンスが作成される。

class User
end
user = User.new
p user # #<User:0x00007fea09926508>

クラス内でメソッドを定義するとそれは「インスタンスメソッド」となり、インスタンスに対して呼び出すことが出来る。

initializeメソッドは特殊なメソッドで、定義すると、インスタンス作成時に自動的に呼び出される。

class User
  def initialize
    p 'This is initialize.'
  end
end
user = User.new # "This is initialize."

インスタンス変数とアクセサメソッド

インスタンス変数とは、インスタンス内で共有される変数。名前は必ず@から始める。
インスタンス変数にアクセスするインスタンスメソッドのことを、アクセサメソッドと呼ぶ。

下記の例では@nameがインスタンス変数で、nameがアクセサメソッド。

class User
  def initialize(name)
    @name = name
  end

  def name
    @name
  end
end
user = User.new('Alice')
p user.name # "Alice"

存在しないインスタンス変数を参照した場合はnilを返す。

class User
  def initialize(name)
    @name = name
  end

  def name
    @foo
  end
end
user = User.new('Alice')
p user.name # nil

アクセサメソッドはattr_accessorで定義することも出来る。

class User
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end
user = User.new('Alice')
p user.name # "Alice"

attr_accessorは指定したインスタンス変数の読み書き両方を許可するが、読み込み専用にするattr_reader、書き込み専用にするattr_writerもある。

クラスメソッド

インスタンスではなくクラスに対して呼び出すメソッドを、クラスメソッドと呼ぶ。
書き方は以下の2種類。

メソッド名の前にself.をつける

class User
  def self.foo
    p 'This is class method.'
  end
end
User.foo # "This is class method."

メソッドの定義をclass << selfendで囲う

class User
  class << self
    def boo
      p 'This is class method also.'
    end
  end
end
User.boo # "This is class method also."

self キーワード

selfは、インスタンスメソッド内ではそのインスタンス自身を指し、クラスメソッド内ではクラスを指す。

class User
  def self.c_method
    p self
  end

  def i_method
    p self
  end
end
User.c_method # User
user = User.new
user.i_method # #<User:0x00007fcaea8828d8>

継承

Rubyの継承は単一継承。単一継承とは、一つのスーパークラスを継承すること。複数のスーパークラスは継承できない。

クラスをレシーバにしてsuperclassメソッドを呼び出すと、継承元のクラスを知ることが出来る。

独自に作成したクラスは、デフォルトでObjectクラスを継承する。
何も定義しなくてもclassメソッドなどを使うことが出来るのはそのため。

class User
end
p User.superclass # Object

Objectは組み込みライブラリの一種で、ArrayStringといった組み込みライブラリも、Objectを継承している。

p Array.superclass # Object
p String.superclass # Object

ちなみに、ObjectのスーパークラスはBasicObjectで、これが継承関係の頂点にある。
BasicObject.superclassnilを返す。

p Object.superclass # BasicObject
p BasicObject.superclass # nil

インスタンスのクラスを調べる

インスタンスに対してclassメソッドを呼び出すと、クラスを返す。
instance_of?(クラス)で、そのクラスのインスタンスかどうかを真偽値で返す。
is_a?は継承関係にあるかを真偽値で返す。

class User
end

user = User.new

p user.class # User

p user.instance_of?(User) # true
p user.instance_of?(Object) # false

p user.is_a?(User) # true
p user.is_a?(Object) # true
p user.is_a?(Array) # false

Object 以外のクラスを継承する

以下の構文を使う。

class サブクラス < スーパークラス
end

以下の例では、Arrayクラスを継承したMyArrayクラスを定義している。

class MyArray < Array
end

array = MyArray.new

p array.class # MyArray
p array.is_a?(MyArray) # true
p array.is_a?(Array) # true

オーバーライドと super

サブクラスがスーパークラスと同名のメソッドを定義すると、オーバーライドする。
インスタンスメソッドもクラスメソッドも同じ挙動。

class Parent
  def foo
    p 'super'
  end

  def bar
    p 'bar'
  end
end

class Child < Parent
  def foo
    p 'sub'
  end
end

child = Child.new
child.foo #  "sub"
child.bar # "bar"
class Parent
  def self.foo
    p 'super'
  end

  def self.bar
    p 'bar'
  end
end

class Child < Parent
  def self.foo
    p 'sub'
  end
end

Child.foo # "sub"
Child.bar # "bar"

メソッド内でsuperを実行すると、スーパークラスの同名のメソッドを呼び出す。
これも、インスタンスメソッドとクラスメソッドで同じ挙動。

class Parent
  def foo
    'super'
  end
end

class Child < Parent
  def foo
    p "#{super}, sub"
  end
end

child = Child.new
child.foo # "super, sub"
class Parent
  def self.foo
    'super'
  end
end

class Child < Parent
  def self.foo
    p "#{super}, sub"
  end
end

Child.foo # "super, sub"

メソッドだけでなく、インスタンス変数もオーバーライドされる。

class Parent
  def initialize
    @name = 'Alice'
  end

  def greeting
    p "Hello, I am #{@name}."
  end
end

class Child < Parent
  def initialize
    @name = 'Bob'
  end
end

parent = Parent.new
parent.greeting # "Hello, I am Alice."
child = Child.new
child.greeting # "Hello, I am Bob."

public, private, protected

Rubyのメソッドは、公開レベルに応じて3つに分類できる。

デフォルトではpublicメソッドになるが、initializeメソッドだけはデフォルトでprivateメソッドになる。

publicメソッドは、外部から自由に呼び出せる。
privateメソッドは、レシーバを指定して呼び出すことが出来ない。

クラスの定義内でprivateキーワードを書くと、そこから下で定義したインスタンスメソッドはprivateメソッドになる。

下記の例では、nameprivateメソッドなので、レシーバを指定して呼び出そうとするとエラーになる。

class User
  def hello
    "Hello, #{self.name}."
  end

  def bye
    "Bye, #{name}."
  end

  private

  def name
    'Alice'
  end
end

u = User.new
p u.name # private method `name' called for #<User:0x00007fa6e81c9d78> (NoMethodError)
p u.hello # private method `name' called for #<User:0x00007fbb358bdc18> (NoMethodError)
p u.bye # "Bye, Alice."

レシーバを指定しない、というルールさえ守れば、サブクラスでスーパークラスのprivateメソッドを呼び出すことも出来る。

class Parent
  private

  def name
    'Alice'
  end
end

class Child < Parent
  def print_name
    p name
  end
end

child = Child.new
child.print_name # "Alice"

クラスメソッドはprivateキーワードの下に定義してもprivateにならず、以下のいずれかの書き方で設定する。

class User
  class << self
    private

    def hello
      'Hello!'
    end
  end

  p "#{hello} This is private." # "Hello! This is private."
end

User.hello # private method `hello' called for User:Class (NoMethodError)
class User
  def self.hello
    'Hello!'
  end

  private_class_method :hello

  p "#{hello} This is private." # "Hello! This is private."
end

User.hello # private method `hello' called for User:Class (NoMethodError)

protectedメソッドは、そのクラス自身とサブクラスのインスタンスからのみ、レシーバを指定して呼び出せる。

class User
  def hello
    "Hello, #{self.name}."
  end

  def bye
    "Bye, #{name}."
  end

  protected

  def name
    'Alice'
  end
end

u = User.new
p u.name # protected method `name' called for #<User:0x00007fb73d0edcc8> (NoMethodError)
p u.hello # "Hello, Alice." private メソッドと違い、これはエラーにならない
p u.bye # "Bye, Alice."

respond_to?

respond_to?は、指定したレシーバが、引数に渡されたメソッドを持つかを返す。

p 1.respond_to?(:to_s) # true
p 1.respond_to?(:foo) # false

特異メソッド

Rubyでは、オブジェクト単位でメソッドを定義することが出来る。

str1 = 'abc'
str2 = 'xyz'

def str1.foo
  "#{self} has foo."
end

p str1.respond_to?(:foo) # true
p str2.respond_to?(:foo) # false

p str1.foo # "abc has foo."

このように、特定のオブジェクトに紐付いたメソッドを特異メソッドと呼ぶ。

これまでクラスメソッドと呼んでいたものも、特異メソッドの一種である。
クラスもまたオブジェクトなので特異メソッドを定義することができ、それがいわゆるクラスメソッド。

class User
  p self # User
  p self.class # Class
  p self.instance_of?(Class) # true

  def self.foo # Class インスタンスである User オブジェクトに、特異メソッドを定義している
    'This is foo.'
  end
end

p User.foo # "This is foo."

なお、数値やシンボルに対しては特異メソッドを定義することは出来ない。

sym = :sym
def sym.foo # can't define singleton (TypeError)
end

i = 1
def i.foo # can't define singleton (TypeError)
end

参考資料

プログラミング言語を使って対象を記述していく

「よりよいプログラミング」を考える上で示唆に富む記事を読んだので、自分なりにまとめておく。
以下の記事を読むことで、プログラミングに対して大きなヒントを得られた。

設計やアーキテクチャの話ではなく、プログラミングというものに対する発想や認識の話。
プログラミングを「必要な動きを実装するもの」と捉えるのではなく、「対象を定義するもの」と捉える。
そうすることで、プログラミングにおいて最も重要な原則の一つである「正しい名前をつける」ということを実現できるようになる。

「正しい名前」という羅針盤

よいコードとは何か。
様々な議論や視点があるが、中核的な要素の一つが、命名。
関数や変数に適切な名前をつけることで、コードの質が高まる。

より正確に言えば「名前が正しい状態を維持し続ける努力」を弛まず行うことで、コードの質が高まっていく。
名前が正しい状態を維持するために、コードにも手を入れることになるから。

「正しい名前」とは、「それは何?」を一言で表現するもの。
それぞれの関数や変数が何であるのかを、過不足なく説明するもの。

「名前が「それは何?」を的確に表現している状態」を維持しようと心がけることで、名前とコードが相互作用を及ぼし、双方が洗練されていく。
関数に名前をつけようとしたとき、その関数の役割や意味が曖昧だと、正しい名前をつけることなど出来ない。そのため、まずはその関数の役割や意味を整理することになり、コードの書き換えや分割を行うことになる。それから、意味や役割に見合った名前をつける。
既存の関数に手を加えようとしたときも、名前が適切な状態を崩さないように気をつける。そのため、その関数の役割から外れた処理が紛れ込むことがなくなる。あるいは、そもそも既存の名前が適切ではなかったことに気付くかもしれない。場合によっては、より適切な粒度に関数を分割するべきなのかもしれない。
例として関数を出したが、変数やクラスなど、名前をつけるもの全てに同じことが言える。例えば変数については、どんな役割を期待され、何のために存在する変数なのかを考え抜いて名前をつけ、その名前に見合わないような値が入り込んだり、適切でない箇所で参照されたりすることを、防がなければならない。

その名前は「それは何?」を上手く説明できているのか、そのコードは名前の正しさを崩していないか、ということを絶えず意識してプログラミングしていく。

このような名前とコードの相互作用を繰り返すことで、コード全体の質が高まっていく。
可読性が高まるのはもちろんのこと、単機能で短い関数が適切な粒度で作成され、それぞれが疎結合になる。
一つ一つの変数や関数の意味が明確だから、間違った使い方をしたり間違った処理を加えたりしてしまう可能性も低くなる。
そもそもの設計が間違っていた場合、正しい名前をつけることやコードを整理することがどんどん苦しくなっていくので、名前の正しさにこだわり続けることで設計の間違いに気付き、より妥当な設計が見えてくる。

コードを改善していくための指針や道標が「正しい名前」であると言える。
「正しい名前」と「名前に見合ったコード」が、コード改善のための羅針盤となる。

「必要な動きを実装する」という考え方

しかしほとんどの現場では「正しい名前をつける」ということは軽視されている。
代わりに何が重視されているかというと、「動くこと」が最優先になっている。
もちろん動くことは大前提であり動かないコードに価値はないのだから、間違ってはいない。
しかし現実には、「動くことは大前提」ではなく、「動きさえすればよい」となってしまっている。

その結果、「正しい名前」ということについては軽視されるようになる。
「正しい名前をつけること」は努力義務のようになってしまい、それが出来るに越したことはないが、取り敢えず動いてさえいれば及第点とされてしまう。

なぜそうなってしまうのかと言えば、「プログラミングとは必要な動きを実装すること」だと思っているから。プログラミングを、そういうものだと捉えている。
確かにこの定義に照らし合わせれば「動けばよい」となる。動きさえすれば、立派にプログラミングをしていることになる。
中身の処理がどうなっていようと、表示させたいボタンが表示されており、それを押したときに意図した動作が行われていれば、それでよいということになる。

ここに「正しい名前」という観点は一切ない。「正しい名前が大切」ということを否定するわけではないが、特にそれを重視するわけでもない。
そしてこういう世界では、「正しい名前をつけよう」という努力は、どんどんおざなりになっていく。

優先順位が下だから。
命名が上手くいけば儲けものだが、そんなことよりも、とにかく求められた機能を実装しなければならない。報告のあったバグを直し、意図した動作が行われるようにしなければならない。
そしてそれさえ出来れば、目出度くリリースされる。名前とコードの不一致など、大して気に留めない。

取り敢えず動くコードを書くことに比べて、正しい名前を徹底することには大きな労力がかかるので、そのこともこの傾向に拍車をかける。

「対象を定義する」という考え方

このように、プログラミングを「必要な動きを実装するもの」だと捉えている限り、「正しい名前をつける」という努力を徹底することは非常に難しい。
なので、プログラミングに対する認識を変える。

プログラミングを、「実現しようとしている対象を定義する行為」と捉える。そのように認識を変える。

「実現しようとしている対象」とは、作ろうとしているソフトウェアであり、それによって実現されるビジネスなりサービスなりユーザー体験なりである。開発現場でいうところの「要件」とも言えるかもしれない。
それについてプログラミング言語を使って記述していき定義することが、プログラミングである。それによって生まれた完成物、つまり対象について定義したものが、プログラム。

その過程で、対象について記述するための語彙を増やしていくことになる。
プログラミング言語が予め用意している語彙だけでは表現力に限界がある。
なので、語彙を組み合わせて、新しい語彙を定義する。その対象に特有の概念や表現についても、語彙として定義してしまったほうが効率がよい。

このように語彙を作ったり語彙を駆使したりして、プログラム全体を定義していく。
そして過不足なく対象について定義できたとき、プログラムは完成となる。そしてここに到るまでの一連の作業こそがプログラミングである。

そしてこの世界においては、語彙の意味が明確で一貫性を持っているのが「よいプログラム」であり、語彙の意味が曖昧で一貫性がないことが「汚いプログラム」であるといえる。

語彙の定義や意味が曖昧だと、使い物にならない。厳密で一貫性のある記述はできず、対象を定義していくことに困難が生じる。的確に記述できない。
取り敢えずは記述できるかもしれないが、語彙が示しているものが不明瞭だから、書き換えや追記がしづらい。

そして、語彙の定義が間違っていたり、語彙の使い方を間違っていると、それはバグとなって現れる。

プログラミングに対するこのような捉え方は、「正しい名前をつける」という態度と相性がよい。というより、両者は同じものを指している。
「語彙の定義」とは「名前をつける」ということであり、正しく定義された語彙(=変数や関数)を駆使して、プログラム全体を表現していく。

だから、「プログラミング言語を使って対象を定義していく」という認識でプログラミングに取り組むことで、自然と「名前」に対する意識を保つことが出来る。
そして既に書いたように「正しい名前」を追求しながらプログラミングすることで、コードの質は高まっていく。

見える景色が変わる

長々と書いてきたが、ただ発想を変えただけで、人気のライブラリや最新のアーキテクチャを入れたわけではない。 プログラミングに対する認識を変えただけの話。
しかし発想の転換というのはとても重要で、難問だと思っていたことも切り口を変えてみることであっさり解決する、というのはよくある。 それはプログラミングにも言えて、考え方が変わるだけで、書き方も変わる。
はてなブックマークからの引用だが、『動きに名前をつけるのでなく名前の定義を書く』『「どう作る?」ではまって、「それ何?」を見直したら周辺の課題も巻き込んで解決』とあるように、意識を変えるだけで、プログラミングのやり方が大きく変わる可能性がある。

自分は業務でSPAを開発することが多いが、それについても、「SPAという対象を過不足なく記述していく作業」と捉えることが出来る。
JavaScriptというプログラミング言語に用意された語彙を使っていくことになるが、それだけでは効率が悪く冗長になる。汎用的で基礎的な語彙しか用意されていないから表現力に限界があるし、同じ記述を何度も繰り返したりすることになる。
だから、自分で新しく語彙を定義して、それを使っていく。
しかしそれだけでは大変なので、ライブラリも使う。ライブラリは語彙集と言える。
SPAの記述に役立つ語彙集として、ReactVueなどがあり、ライブラリ毎に語彙の種類や特徴は異なる。
このように、たくさんの語彙を定義し、それを駆使して、表示、レイアウト、機能、などを定義していき、SPAという全体像について書き上げる。

まとめ

「正しい名前をつける」のが大切であり、それを実践するための手法として「プログラミングとは、プログラミング言語を使って対象を定義することである」という認識に切り替える。単純な話ではあるが、実践するのは難しい。
だがこれを習慣や態度として身に着けることが出来れば、プログラマとしての地力がかなり上がると思っている。