T_Y_CODE

プログラミング学習記録

学習記録 12日目

12日目の学習記録をまとめていきます。

学習計画

学習内容

オブジェクト指向設計実践ガイド 6章

class Bicycle
  attr_reader :size, :tape_color

  def initialize(args)
    @size = args[:size]
    @tape_color = args[:tape_color]
  end

  def spares
    { chain: '10-speed',
      tare_size: '23',
      tape_color: tape_color }
  end
  #...
end

bike = Bicycle.new(size: 'M', tape_color: 'red')
bike.size # => 'M'
bike.spares
# => { chain: "10-speed",
#      tare_size: "23",
#      tape_color: "red" }
  • ロードバイクのクラスを元にマウンテンバイクのタイプも追加してみる。
class Bicycle
  attr_reader :style, :size, :tape_color,
              :front_shock, :rear_shock

  def initialize(args)
    @style = args[:style]
    @size = args[:size]
    @tape_color = args[:tape_color]
    @front_shock = args[:front__shock]
    @rear_shock = args[:rear_shock]
  end

  def spares
    if style == :road
      { chain: '10-speed',
        tare_size: '23', # milimeters
        tape_color: tape_color }
    else
      { chain: '10-speed',
        tare_size: '2.1', # inches
        rear_shock: rear_shock }
  end
  #...
end

bike = Bicycle.new(style: :mountain, size: 'S', front_shock: 'Manitou', rear_shock: 'Fox')
bike.size # => 'S'
bike.spares
# => { chain: "10-speed",
#      tare_size: "2.1",
#      rear_shock: "Fox" }
  • sparesメソッドは有害な影響を保有している。まずstyleが増えた際if文を増やさなくてはならない。また想定外のstyleが指定された場合elseの設定になってしまう。
  • if文で評価の対象としているstyle変数は実質的にクラスを2種類の別のものに分けている。大部分は似ている2つのものを1つのクラスにまとめてしまっている。
  • 上記問題は継承を使用することで解決する。BicycleクラスをMountainBikeクラスのスーパークラスと定義する。

f:id:t_y_code:20201017133150p:plain

  • 以下はMountainBikeクラスを作る際の起こしがちなミスの例である。
class MountainBike < Bicycle
  attr_reader :front_shock, :rear_shock

  def initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
    super(args)
  end

  def spares
    super.merge(rear_shock: rear_shock)
  end
end
mountain_bike = MountainBikeike.new(size: 'S', front_shock: 'Manitou', rear_shock: 'Fox')
mountain_bike.size # = > 'S'
mountain_bike.spares
# => { chain: "10-speed",
#      tare_size: "23", <- 間違い
#      tape_color: nil, <- 不適切
#      rear_shock: "Fox" }

f:id:t_y_code:20201017135725p:plain

  • Bicycleクラスを抽象的なクラスにしてBicycleクラスを継承したロードバイククラスを作るべきである。Bicycleクラスのような抽象的なクラスのことを抽象クラスと呼ぶ。抽象クラスはサブクラスがつくられるためだけに存在している。

f:id:t_y_code:20201017135902p:plain

  • 本書ではBicycleクラスをそのままRoadBikeクラスに変更し新しく空のBicycleクラスを作成、抽象的な変数やメソッドをBicycleクラスに移していく順序をとっている。一度サブクラスに降ろしてからスーパークラスに徐々に移動していく方法はリファクタリングにおいて重要な作業になる。継承で起こる多くの失敗は具象から抽象を厳密に分ける作業の難しさから起こるものである。この作業は横着しない方が良い。
  • まずはsizeゲッターを使用出来るようにする。
class Bicycle
  attr_reader :size

  def initialize(args={})
    @size = args[:size]
  end
end

class RoadBike < Bicycle
  attr_reader :tape_color

  def initialize(args)
    @tape_color = args[:tape_color]
  end
  #...
end
road_bike = RoadBike.new(size: 'M', tape_color: 'red')
road_bike.size # => "M"

mountain_bike = MountainBike.new(size: 'S', front_shock: 'Manitou', rear_shock: 'Fox')
mountain_bike.size # =>  "S"
  • sparesメソッドは自転車の種類ごとに異なりそのままBicycleクラスの昇格することは出来ない。
  • 一方sparesメソッド内で定義されているchainとtire_sizeは自転車に共通するものでありsize同様ゲッターセッターを定義すべきである。
class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size = args[:size]
    @chain =  args[:chain]
    @tire_size = args[:tire_size]
  end
end
  • 上記より全てのサブクラスがsize, chain, tire_sizeを理解するようになった。またサブクラスは各変数に固有の値を設定出来るようになった。
  • テンプレートメソッドパターンを使用することで変数に初期値を設定することが出来る。メソッドとして初期値を設定する理由はサブクラスがメソッドをオーバーライドすれば初期値を変更出来るからである。
class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size = args[:size]
    @chain =  args[:chain] || default_chain
    @tire_size = args[:tire_size] || default_tire_size
  end

  def default_chain
    '10-speed'
  end
end

class RoadBike < Bicycle
  #...
  def default_tire_size
    '23'
  end
end

class MountainBike < Bicycle
  #...
  def default_tire_size
    '2.1'
  end
end
road_bike = RoadBike.new(size: 'M', tape_color: 'red')
road_bike.tire_size # => "23"
road_bike.chain # => "10-speed"

mountain_bike = MountainBike.new(size: 'S', front_shock: 'Manitou', rear_shock: 'Fox')
mountain_bike.tire_size # =>  "2.1"
mountain_bike.chain # => "10-speed"
  • Bicycleクラスはサブクラスにdefault_tire_sizeメソッドの実装を必要としています。現状default_tire_sizeメソッドを作成しないと以下のエラーが発生します。
NameError: undefined local variable or method
'default_tire_size'
  • もう少し明示的なエラーを出力させてあげるべきである。NotImplementedErrrorは定義はされているが中身が実装されていない関数を呼び出した時に発生する例外。
class Bicycle
  #...
  def default_tire_size
    raise NotImplementedError, "This #{self.class} cannt respond to: "
  end
end
  • 上記コードを記載することでエラーメッセージが分かりやすくなる。
NotImplementedError:
This NewBike cannot respond to: 
'default_tire_size'
  • sparesメソッドを作成します。サブクラスで固有に定義されるものはmergeメソッドを用いて実装する。定義した全コードを以下に示す。
class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size = args[:size]
    @chain =  args[:chain] || default_chain
    @tire_size = args[:tire_size] || default_tire_size
  end

  def spares
    { tire_size: tire_size, chain: chain }
  end

  def default_chain
    '10-speed'
  end

  def default_tire_size
    raise NotImplementedError, "This #{self.class} cannt respond to: "
  end
end

class RoadBike < Bicycle
  attr_reader :tape_color

  def initialize(args)
    @tape_color = args[:tape_color]
    super(args)
  end

  def spares
    super.merge({ tape_color: tape_color })
  end

  def default_tire_size
    '23'
  end
end

class MountainBike < Bicycle
  attr_reader :front_shock, :rear_shock

  def initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
    super(args)
  end

  def spares
    super.merge({ rear_shock: rear_shock })
  end

  def default_tire_size
    '2.1'
  end
end
  • 上記コードのままでも動くがまだ依存がある。サブクラスがsuperを各所で書くことが強要されており新たなサブクラスを作成する際superの記載を忘れると気づきにくいバグになる。
  • この問題はフックメッセージを使用することで解決出来る。superを送るように求めるのではなくスーパークラスが代わりに「フック」メッセージを送るようにすることが出来る。
class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size = args[:size]
    @chain =  args[:chain] || default_chain
    @tire_size = args[:tire_size] || default_tire_size

    post_initialize(args)
  end
  
  def post_initialize(args)
    nil
  end

  #...
end

class RoadBike < Bicycle
  attr_reader :tape_color

  def post_initialize(args)
    @tape_color = args[:tape_color]
  end

  #...
end
  • サブクラスのinitializeを削除することでスーパークラスのinitializeが呼び出される。スーパークラスのinitializeはpost_initializeメソッドを呼び出している。このメソッドでサブクラス固有の変数を定義すれば先程の問題は解決する。
  • この変更によりRoadBikeはBicycleに対する知識(size, chain, tire_size)が減った。つまりオブジェクト間の結合度が減ったことになる。
  • sparesメソッドもまたフックメッセージを使用出来る。
class Bicycle
  #...

  def spares
    { tire_size: tire_size,
      chain: chain }.merge(local_spares)
  end
  
  def local_spares
    nil
  end

  #...
end

class RoadBike < Bicycle
  #...

  def local_spares
    { tape_color: tape_color }
  end

  #...
end
  • 最終的な全コードは以下のようになる。サブクラスが簡潔になりより専門に特化したもののみ持つようになった。
class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size = args[:size]
    @chain =  args[:chain] || default_chain
    @tire_size = args[:tire_size] || default_tire_size

    post_initialize(args)
  end

  def spares
    { tire_size: tire_size,
      chain: chain }.merge(local_spares)
  end

  def post_initialize(args)
    nil
  end
  
  def local_spares
    nil
  end

  def default_chain
    '10-speed'
  end

  def default_tire_size
    raise NotImplementedError, "This #{self.class} cannt respond to: "
  end
end

class RoadBike < Bicycle
  attr_reader :tape_color

  def post_initialize(args)
    @tape_color = args[:tape_color]
  end

  def local_spares
    { tape_color: tape_color }
  end

  def default_tire_size
    '23'
  end
end

class MountainBike < Bicycle
  attr_reader :front_shock, :rear_shock

  def post_initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
  end

  def local_spares
    { rear_shock: rear_shock }
  end

  def default_tire_size
    '2.1'
  end
end

Ruby on Railsチュートリアル 2章

  • scaffoldジェネレータを使用してToyアプリケーションを作っていきます。
2.1 アプリケーションの計画
  • まずはRailsアプリケーションを新規作成します。
$ cd ~/environment
$ rails _6.0.3_ new toy_app
$ cd toy_app/
  • Gemfileの内容をチュートリアル指定のものへ変更します。
  • bundle installします。1章と同じエラーが出るので同様に対処していきます。
$ bundle install --without production
...
If you are updating multiple gems in your Gemfile at once,
try passing them all to `bundle update`
$ bundle config build.puma --with-cflags="-Wno-error=implicit-function-declaration"
$ bundle update --without production
$ bundle install --without production
$ git init
$ git add -A
$ git commit -m "Initialize repository"
$ git remote add origin https://github.com/***/***.git
$ git push -u origin master
  • 1.3.4同様にルートにHello, world!を表示するよう変更を加えます。
# /app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  render html: "Hello,world!!"
end
# /config/routes.rb
Rails.application.routes.draw do
  root "application#hello"
end
  • herokuへデプロイします。
$ heroku create
Creating app... done, ⬢ ***
https://***.herokuapp.com/ | https://git.heroku.com/***.git
$ git remote -v # リモートの設定を確認
$ git add -A
$ git commit -m "Add hello"
$ heroku create
$ git push && git push heroku master
  • ちゃんとデプロイ出来てるか確認します。実際のサイトは以下のURLです。

https://sheltered-dusk-99982.herokuapp.com

2.2 Usersリソース
  • scaffoldコマンドを用いてCRUDの機能が付いたUsersリソースを作成します。
$ rails generate scaffold User name:string email:string
Running via Spring preloader in process 39425
      invoke  active_record
      create    db/migrate/20201017091657_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      create      test/system/users_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      create      app/views/users/_user.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss
  • ログを見るとマイグレーション作成からモデル作成、ルートの作成、コントローラ作成、ビュー作成まで自動でやってくれていますね。scaffold便利。
  • モデルデータを作成するためにDBのマイグレーションを行います。
$ rails db:migrate
  • rails routesでルーティングを確認してみます。しっかり作成されてますね。

f:id:t_y_code:20201017182712p:plain

f:id:t_y_code:20201017182549p:plain

演習

1. CSSを知っている読者へ: 新しいユーザーを作成し、ブラウザのHTMLインスペクター機能を使って「User was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?

  • リロードするとpタグの中身が消える。
/* 作成時 */
<p id="notice">User was successfully created.</p>
/* リロード後 */
<p id="notice"></p>

2. emailを入力せず、名前だけを入力しようとした場合、どうなるでしょうか?

  • 作成出来る。バリデーションしてないから。

3. 「@example.com」のような間違ったメールアドレスを入力して更新しようとした場合、どうなるでしょうか?

  • 作成出来る。こちらもバリデーションしてないから。

4. 上記の演習で作成したユーザーを削除してみてください。ユーザーを削除したとき、Railsはどんなメッセージを表示するでしょうか?

<p id="notice">User was successfully destroyed.</p>
Started DELETE "/users/5" for 127.0.0.1 at 2020-10-17 18:33:20 +0900
Processing by UsersController#destroy as HTML
  Parameters: {"authenticity_token"=>"haACzUla3pcJCjB38uj1z1FtCJt9Nl5cIm0vQV10VvFjVVJHg/pmciQv7jjsSJ+ugYAr10B98DKJ1FAtP7NHjQ==", "id"=>"5"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
  ↳ app/controllers/users_controller.rb:67:in `set_user'
   (0.0ms)  begin transaction
  ↳ app/controllers/users_controller.rb:57:in `destroy'
  User Destroy (0.4ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 5]]
  ↳ app/controllers/users_controller.rb:57:in `destroy'
   (3.8ms)  commit transaction
  ↳ app/controllers/users_controller.rb:57:in `destroy'
Redirected to http://127.0.0.1:3000/users
Completed 302 Found in 8ms (ActiveRecord: 4.4ms | Allocations: 2765)


Started GET "/users" for 127.0.0.1 at 2020-10-17 18:33:20 +0900
Processing by UsersController#index as HTML
  Rendering users/index.html.erb within layouts/application
  User Load (0.1ms)  SELECT "users".* FROM "users"
  ↳ app/views/users/index.html.erb:15
  Rendered users/index.html.erb within layouts/application (Duration: 1.6ms | Allocations: 1142)
Completed 200 OK in 9ms (Views: 8.8ms | ActiveRecord: 0.1ms | Allocations: 6866)
2.2.2 MVCの挙動
  • ルートURLで表示するアクションをUsersコントローラのindexアクションに変更します。
# /config/routes.rb
Rails.application.routes.draw do
  root "users#index"
end
  • Usersコントローラを確認すると自動でアクションが作成されている。このアクションはRailsの設計方針であるRESTを成立させるために作られている。
  • RESTとは、アプリケーションを構成するコンポーネント
    • RDBMSCRUD
    • HTTPRequestの各メソッド(GET, POST, PATCH, DELETE)
  • に対応させて、自由にCRUDできるものとして扱うアーキテクチャ

qiita.com

演習

1. 図 2.11を参考にしながら、/users/1/edit というURLにアクセスしたときの振る舞いについて図を書いてみてください。

  • editアクション内に何も記載が内がよく見るとbefore_actionでeditアクションを実行する前にset_userメソッドを実行している。set_userメソッドではデータベースからユーザ情報を取得している。
# /app/controllers/user_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]
  #...
  def edit
  end
  #...
  private
    def set_user
      @user = User.find(params[:id])
    end
  #...
end

f:id:t_y_code:20201017192506j:plain
2. 図示した振る舞いを見ながら、Scaffoldで生成されたコードの中でデータベースからユーザー情報を取得しているコードを探してみてください。Hint: set_userという特殊な場所の中にあります。

  • 1の回答で示した通りUsersコントローラ内のset_userメソッド内で取得している。

3. ユーザーの情報を編集するページのファイル名は何でしょうか?

  • 図に示した通りedit.html.erb。/app/views/users/ディレクトリ内にある。
2.3 Micropostsリソース
2.3.1 マイクロソフトを探検する
  • Usersリソース同様にscaffoldを使用してMicropostsリソースを作成する。
$ rails generate scaffold Micropost content:text user_id:integer
  • データベースの更新を行う。
$ rails db:migrate
  • ルーティングを見るとしっかりMicroposts関連が追加されてます。
$ rails routes

f:id:t_y_code:20201017193212p:plain

  • サーバを起動し動作確認を行う。

f:id:t_y_code:20201017193422p:plain

演習

1. CSSを知っている読者へ: 新しいマイクロポストを作成し、ブラウザのHTMLインスペクター機能を使って「Micropost was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?

  • リロードするとpタグの中身が消える。
/* 作成時 */
<p id="notice">Micropost was successfully created.</p>
/* リロード後 */
<p id="notice"></p>

2. マイクロポストの作成画面で、ContentもUserも空にして作成しようとするどうなるでしょうか?

  • 作成できる。

3. 141文字以上の文字列をContentに入力した状態で、マイクロポストを作成しようとするとどうなるでしょうか?(ヒント: WikipediaRubyの記事にある設計思想の引用文が140文字を超えているので、これをコピペしてみましょう)

  • 作成できる。

4. 上記の演習で作成したマイクロポストを削除してみましょう。

  • 削除しておきます。
2.3.2 マイクロポストをマイクロにする
  • Micropostモデルにバリデーションを追加して文字数を140字以内に制限します。
# /app/Models/micropost.rb
class Micropost < ApplicationRecord
  validates :content, length: { maximum: 140 }
end
演習

1. 先ほど2.3.1.1の演習でやったように、もう一度Contentに141文字以上を入力してみましょう。どのように振る舞いが変わったでしょうか?

  • 140文字以上の登録を使用とするとエラーが発生するようになりました。

f:id:t_y_code:20201017194200p:plain
2. CSSを知っている読者へ: ブラウザのHTMLインスペクター機能を使って、表示されたエラーメッセージを調べてみてください。

  • div#error_explanationタグ内に記載されている。
2.3.2 ユーザーはたくさんマイクロポストを持っている
  • MicropostモデルとUserモデルを関連付けさせる。
# /app/Models/user.rb
class User < ApplicationRecord
  has_many :microposts
end
# /app/Models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 }
end
演習

1. ユーザーのshowページを編集し、ユーザーの最初のマイクロポストを表示してみましょう。同ファイル内の他のコードから文法を推測してみてください(コラム 1.2で紹介した技術の出番です)。うまく表示できたかどうか、/users/1 にアクセスして確認してみましょう。

  • 以下のコードを追記する。
# /app/views/users/show.html.erb
/* ... */
<p>
  <strong>First Micropost:</strong>
  <%= @user.microposts.first.content %>
</p>
/* ... */
  • 表示できました。

f:id:t_y_code:20201017195241p:plain
2. リスト 2.18は、マイクロポストのContentが存在しているかどうかを検証するバリデーションです。マイクロポストが空でないことを検証できているかどうか、実際に試してみましょう(図 2.17のようになっていると成功です)。

  • 省略

3. リスト 2.19のFILL_INとなっている箇所を書き換えて、Userモデルのnameとemailが存在していることを検証してみてください(図 2.18)。

# リスト2.19
class User < ApplicationRecord
  has_many :microposts
  validates FILL_IN, presence: true
  validates FILL_IN, presence: true
end
  • 以下のように書き換えます。
# /app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
  validates :name, presence: true
  validates :email, presence: true
end
  • 動作確認します。

f:id:t_y_code:20201017195819p:plain

2.3.4 継承の階層
演習

1. Applicationコントローラのファイルを開き、ApplicationControllerがActionController::Baseを継承している部分のコードを探してみてください。

  • 以下のコードが該当箇所。
# /app/controllers/application_controller.rb
class ApplicationController < ActionController::Base # <-
  def hello
    render html: "Hello,world!!"
  end
end

2. ApplicationRecordがActiveRecord::Baseを継承しているコードはどこにあるでしょうか? 先ほどの演習を参考に、探してみてください。ヒント: コントローラと本質的には同じ仕組みなので、app/modelsディレクトリ内にあるファイルを調べてみると...?)

  • 以下のコードが該当箇所。
# /app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base # <-
  self.abstract_class = true
end
2.3.5 アプリケーションをデプロイする
$ git add -A
$ git commit -m "Finish toy app"
$ git push
  • herokuにデプロイします。
$ git push heroku
$ heroku run rails db:migrate
演習
  • 動作確認だけのため飛ばします。
  • 以上で2章終了です。

本日の総括

  • 今まで継承の使い方について曖昧な状態だったのでどういう時に継承を使うべきなのか、気をつけることは何なのか等学べた。フックメッセージなど新しいテクニックを学ぶのは楽しい。
  • Railsチュートリアルは3章からが本番だと思っているので明後日から気合入れて学習します。