blog

紫陽花

Rails ActiveRecordで値が変更する前後の値を取得する

ActiveRecordで値が変更する前後の値を取得することがあったのでメモ

Rails 2.1あたりからActiveRecordの変更前後の値を取得する機能が備わっている。

method 意味
changed? 変更されているかどうか
changed 変更されているattribute名の配列
changeds 変更されているattribute名と値のハッシュ。値は変更前後の値を配列で。
{attr}_changed? {attr}が変更されているかどうかの判定
{attr}_was {attr}の変更前の値。変更されていない場合は元の値
{attr}_change {attr}の変更前後の値の配列。変更されていなかったらnil
{attr}_will_change! {attr}を変更することを明示。

attrには属性名が入ります。nameの場合 name_wasで変更前の値を取得することができる

使用例

Railsのコールバック処理で複数の配列データを1つに統一したい場合に利用する

コールバック処理

  • before_save
  • before_update
  • after_update

etc...

before_update処理でmerge_tagメソッドを実行する

merge_tagメソッドでは既に登録されていた既存の値(tag)と、

変更が加えられた新規の値tag_wasを組み合わせる(merge)処理が行われ、

self.tagに代入される

before_update :merge_tag

def merge_tag
  # tag_wasには元々のtagの値が入り、
  # mergeの引数のtagは新たなtagの値になる
  self.tag = tag_was.merge(tag)
end

もっと簡単な例で動きを説明すると

tag = Tag.find(1)
#=> #<Tag:0x007fc35ee6fc98 id: 1, name: "ドラえもん", created_at: Wed, 11 Apr 2016 20:21:02 JST +09:00>
tag.name
#=> "ドラえもん"
tag.changed?
#=> false
tag.name = "のび太"
#=> "のび太"
tag.changed?
#=> true
tag.name_was
#=> "ドラえもん"

のように、tag.name_wasで変更前の値を取得することができています。

ActiveRecordのちょっとした機能ですが、応用的な処理を実装する時には便利に使えるかなと思います。

Singletonパターン Ruby

シングルトンパターンとは

  • 作成したクラスのインスタンスが唯一であることが保証される
  • 唯一であるがゆえに、生成した1つだけのインスタンスにアクセスすることができる。

実装

Rubyにはsingletonと呼ばれるモジュールが定義されているためこれをrequireし、 クラスにincludeすることでシングルトンクラスを定義することができる。

require 'singleton'

#  シングルトン
class SingletonObject# instanceメソッドが定義される
  include Singleton
  attr_accessor :counter
  
  def initialize
    @counter = 0
  end
end

obj1 = SingletonObject.instance
obj1.counter += 1
puts(obj1.counter)
# 1

obj2 = SingletonObject.instance
obj2.counter += 1
puts(obj2.counter)
# 2
# 前回の+1が引き継がれている

obj3 = SingletonObject.new
# private method `new` called for SingletonObject:Class
# newメソッドはプライベートメソッドとして定義される

acts_as_listとjQueryで並べ替えを実装する

jQueryのsortableを用いて並べ替えを実装する Rails

acts_as_list

モデルの並び順を簡単に操作できるようにするGem

昔のRails1系あたりでは標準で搭載されていたらしいが、 途中でGemとして切り離されて現在は独立したGemとして存在する。

Rails 1.x の時代には、Rails 本体に組み込まれていたけれど、その後プラグインとして分離されました。

引用:acts_as_list: gem か plugin か

本家がこちら。 https://github.com/swanandp/acts_as_list

導入の仕方などはドキュメントにわかりやすく書かれているのでそこを参考すると良さそう。 acts_as_listでは positionカラム がデータのソート基準となるよう決まっている。 なのでacts_as_listを使いたい場合は、対象とするテーブルのカラムにposition絡むを追加しましょう。

# has_manyでtodo_itemsを持つ親モデル
class TodoList < ActiveRecord::Base
  has_many :todo_items, -> { order(position: :asc) }
end

# belongs_toでtodo_listに紐づく子モデル
class TodoItem < ActiveRecord::Base
  belongs_to :todo_list
  # scopeで定義する
  acts_as_list scope: :todo_list
end

todo_list = TodoList.find(...)
# move_to_bottomで値を操作1,2,3とあるならば1が3になり2,3が繰り上がる
todo_list.todo_items.first.move_to_bottom
# move_higherで1,2,3,とあるならば2,1,3と変更される
todo_list.todo_items.last.move_higher

また、最近の紹介記事ですと、Qiitaに上がっているjnchitoさんの記事が素晴らしくわかりやすかったです。 実装の際に参考にさせてもらいました。 Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)

動画は見ていませんが初心者にもわかりやすいように筆者が参考にした記事や、 動画を用いての実装方法まで紹介していてさすがですね。

今回自分は ranked_model を使った実装を行っていないので機会があればそちらも使ってみたいなと思います。 どうやらranked-modleの方がパフォーマンスが良いようです。(検証はしてません)

Railsで順番を管理するgemとして、ranked-model gemを使います。 acts_as_listも有名ですが、ranked-modelの方がパフォーマンス的に優れています。

ドラッグ&ドロップで並べ替えを行う実装

jQueryのsortableを用いて、ドラッグ&ドロップで操作を行う実装を紹介します。 上記でacts_as_listを紹介しましたが、今回の実装では用いません。 acts_as_listではなくjQueryのsortableを用いることでも並べ替えを良い感じにできるんだぜってことを紹介したいと思います。

# Railsで良い感じに実装するためのGem達
gem 'jquery-ui-rails'
gem 'jquery-turbolinks'
// application.jsで読み込みます
//= require jquery-ui/effect-highlight
//= require jquery-ui/sortable

D&Dを行った際の処理 $('.table-sortable').sortable でsortableを適用させる。 D&Dをトリガーにしてajax処理を行いDB側のposition値を変更する使用

// table_sort.js
$(function() {
  return $('.table-sortable').sortable({
    axis: 'y',
    items: '.sentence',
    update: function() {
    // ajax処理で並べ替え後の値をサーバー側に渡す
      $.ajax({
        type: 'POST',
        url: '/sort',
        dataType: 'json',
        // $('.table-sortable').sortable('serialize') で変更後の値を取得
        data: $('.table-sortable').sortable('serialize')
      });
    },
    // ここはおまけで変更完了エフェクトを付け足す
    stop: function(e, ui) {
      return ui.item.children('td').effect('highlight');
    }
  });
});

controller側の実装

def sort
  Content.all.each do |sentence|
    content.position = params[:content].index(content.position.to_s) + 1
    content.save
  end
  render nothing: true
end

model側での実装 値を追加した際にpositionカラムに値の最大値をセットするために before_createにset_positionメソッドを定義します

  before_create :set_position

  def set_position
    if max =Content.where(hoge_id: hoge_id).maximum(:position)
      self.position = max + 1
    end
  end

slimの実装 id="position_#{content.position}"でデフォルト値をセット

table.table.table-striped.table-sortable
  thead
    tr
      th id
      th name
      th description
  tbody
    - @contents.each do |content|
      tr.sontent id="position_#{content.position}"
        td= content.id
        td= content.name
        td= content.description

簡単にイケてる実装ができて便利!

参考

acts_as_list: gem か plugin か

Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)

Ruby/Ruby on Rails/acts_as_list

[Rails]acts_as_listでモデルオブジェクトの並び替え

Compositeパターン Ruby

Compositeパターン

コンポジットパターンとは

  • あるものが同じような下位のもので作られているという考え方
  • 大きなオブジェクトが小さな子オブジェクトから構成されていて、その子オブジェクトもさらに小さな孫オブジェクトでできていたりする
  • 階層構造やツリー構造のオブジェクトを作りたい時に利用出来る

構成

再帰」とは、ある処理の中で再びその手続きを呼び出すこと。

用いるメリット

  • ファイルシステムなどの木構造を伴う再起的なデータ構造を表現できる
  • 階層構造で表現されるオブジェクトの取扱いを楽にする
# FileEntry, DirEntryクラスの共通メソッドを規定(Component)
class Entry
  # ファイル/ディレクトリの名称を返す
  def get_name; end

  # ファイル/ディレクトリのパスを返す
  def ls_entry(prefix) end

  # ファイル/ディレクトリの削除を行う
  def remove; end
end



# Leaf (中身)
class FileEntry < Entry
  def initialize(name)
    @name = name
  end

  # ファイルの名称を返す
  def get_name
    @name
  end

  # ファイルのパスを返す
  def ls_entry(prefix)
    puts(prefix + "/" + get_name)
  end

  # ファイルの削除を行う
  def remove
    puts @name + "を削除しました"
  end
end



# Composite
class DirEntry < Entry
  def initialize(name)
    @name = name
    @directory = Array.new
  end

  # ディレクトリの名称を返す
  def get_name
    @name
  end

  # ディレクトリにディレクトリ/ファイルを追加する
  def add(entry)
    @directory.push(entry)
  end

  # ファイル/ディレクトリのパスを返す
  def ls_entry(prefix)
    puts(prefix + "/" + get_name)
    @directory.each do |e|
      e.ls_entry(prefix + "/" + @name)
    end
  end

  # ファイル/ディレクトリの削除を行う
  def remove
    @directory.each do |i|
      i.remove
    end
    puts @name + "を削除しました"
  end
end


root = DirEntry.new("root")
tmp = DirEntry.new("tmp")
tmp.add(FileEntry.new("conf"))
tmp.add(FileEntry.new("data"))
root.add(tmp)

root.ls_entry("")
#/root
#/root/tmp
#/root/tmp/conf
#/root/tmp/data

root.remove
#confを削除しました
#dataを削除しました
#tmpを削除しました
#rootを削除しました

開発での小ネタ その2

開発での小ネタ その2

普段の開発で得た知見を書き留めておく

YouTubeへのリンクでクエリを付加するロジック

DBにURLが保存してありすでに存在すればそのまま。タグが付いていなければ付加する こちらを参考にしました。

def youtube_url
    uri = URI.parse("https://www.youtube.com/watch?v=WztpnvW4CHA")
    query_hash = {}
    query_hash.merge!(Hash[URI.decode_www_form(uri.query)]) if uri.query.present?
    query_hash.symbolize_keys!.merge!(tag: youtube.opening_bgm_id)
    uri.query = URI.encoded_www_form(query_hash.to_a)
    uri.to_s
end

URI.parseでURLをオブジェクトに変換し、クエリが存在するか判定する if query.present?

存在するならば、URI.decode_www_formを用いてクエリをkeyとvalueのhash形式にしquery_hashに格納する。

文字列(“hoge”)とシンボル(:hoge)が同一な存在とするため、symbolize_keys!を用いて文字列をシンボルに変換します。

.merge!(v: youtube.opening_bgm_id)で付加したいidをmergeする。

mergeメソッドはすでに同じkeyが存在する場合に上書きします。

URI.encoded_www_form(query_hash.to_a)でクエリをuriのqueryに格納して完了!

URI周りを厳密に処理を行いたい場合は以上のようなやり方で操作すると良さげです。

Railsでhttpリクエストをhttpsにリダイレクトさせる

gem "rack-ssl-enforcer"

rack-ssl-enforcerを用います。 Gemをbundle installし、config/environments/** に以下を記述します。

config.middleware.use Rack::SslEnforcer

これでSSL通信に限定ます。 ロードバランサーなどを用いておりヘルスチェックのhttpリクエストは通したいとい場合は以下の記述を追加。

# except: ["/ping"]を追加してsslの制限をexceptすることで解決
config.middleware.use Rack::SslEnforcer, except: ["/ping"]

enumerizeでscope: trueにするとscopeとして使える

class User < ApplicationRecord
  extend Enumerize
  enumerize :sex, :in => [:male, :female], scope: true
  enumerize :status, :in => { active: 1, blocked: 2 }, scope: :having_status
end

User.with_sex(:female)
# SELECT "users".* FROM "users" WHERE "users"."sex" IN ('female')

User.without_sex(:male)
# SELECT "users".* FROM "users" WHERE "users"."sex" NOT IN ('male')

User.having_status(:blocked).with_sex(:male, :female)
# SELECT "users".* FROM "users" WHERE "users"."status" IN (2) AND "users"."sex" IN ('male', 'female')

slackへのアクション通知はslack-notifierがおすすめ

gem "slack-notifier"
notifier = Slack::Notifier.new(Rails.application.config.slack_webhook_url)
notifier.ping(message)

上記で実行するとslack_webhook_urlに対してmessageが送られる。 リッチな通知を実現したい時には以下のようにattachementsを利用すると良い。

message = {
 title: 'タイトル',
 title_link: 'タイトルのリンク',
 text: 'テキスト文',
 image_url: '表示させるサムネイルのURL',
}
SlackNotifier.ping(attachments: [message])

こちらを参考にしました。

バッチなどで大量のデータを操作する際にはfind_eachと良い

分割してレコードを取得して処理をする。 デフォルトで1000件ずつ処理をする。

Railsには find_each というメソッドが用意されています。通常の each メソッドを使用すると、全データをまとめてメモリに展開してから処理を開始します。そのため、十分にメモリに載るデータ量であれば何も問題ないですが、数百万、数千万というデータ量になってくるとメモリに載りきらずに溢れてしまって大変なことになります。 find: 全データをメモリに展開してから処理 find_each: 少しずつデータをメモリに展開しつつ処理 そういうときには find_each メソッドを使いましょう。

参考にしました。 Railsで大量のデータをまとめて更新するならfind_each使うよね

Page.where(:category_id => 1).find_each do |page|
    # hogehoge
end

引数の管理にはハッシュとfetchを使おう

クラス設計で依存関係の管理を行う時のテクニックです。 引数が必要なメソッドを定義した時に値を渡す順番が元から決まっておりそれに沿った形で値を渡さなければいけない状況が多々あります。 もしくは、引数が必要がない場合とある場合を想定したメソッドがあったりします。そんな問題を解決する際には引数のHash化とfetchを用いると良い感じになります。

導入前

class Gear
  attr_reader :chainring, :cog, :wheel 
  def initialize(chainring, cog, wheel)
    @chainring = chainring
    @cog       = cog
    @wheel     = wheel
  end
end

Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches

導入後

class Gear
  attr_reader :chainring, :cog, :wheel 
  # fetchを使ってデフォルト値を指定している
  def initialize(args={})
    @chainring = args.fetch(:chainring, 40)
    @cog       = args.fetch(:cog, 18)
    @wheel     = args[:wheel]
  end
end

Gear.new(
    chainring: 52, 
    cog:       11,
    wheel:     Wheel.new(26, 1.5)).gear_inches

引数をHashで渡すことにより引数を渡す順番の依存がなくなり、 またfetchメソッドを用いることでデフォルト値を設定することも可能になるのでより柔軟性のあるコードになる。良い!!!

saveメソッドを用いて実行するvalidationを指定する

validatesメソッドのonオプションを用いることで走らせるメソッドを指定できる

validates :title, presence: true, on: :create

こうすれば、createメソッドの時にのみvalidationが走ることになる。 saveメソッドのcontextオプションと一緒に用いることで自由に実行するバリデーションを指定できるようになる。

validates :category, presence: true, on: :publish

@obj.attributes = { title: "hoge", content: "こんてんつ" }
@obj.save(context: :publish)

すごく良い

複数のvalitetionをかける場合は、

with_options on: :publish do |publish|
  publish.validates :title, presence: true
  publish.validates :content, presece: true
end

みたいな感じで定義できるっぽい。

状況によってsave時に実行するバリデーションを切り替える

こちらの記事を参考にしました。素晴らしい知見に感謝

メソッドキャッシュを用いて少しでも高速化を図る

class Hoge
  attr_accessor :obj
  def huga
    @obj ||= begin
      sleep 3
      "aaa"
    end
  end
end

一回目にhogeを実行する時よりも2回目の方が実行が早くなる。@objインスタンス変数に値が存在すればそれを使うという書き方をすることで不必要な処理を省く。 すごく細かいところだかこう小さな気遣いが後々効いてくる。なお、使いどころには注意すること

RSpecでスタブを定義しテストをする

スタブ

テストを行うためにメソッドにダミーの振る舞いを定義すること

  • allowでクラス名を指定
  • receiveでメソッドを指定
  • and_returnでメソッド実行の戻り値を指定
describe "" do
  before do
    allow_any_instance_of(Twitter).to receive(:post).and_return(true)
   end
end

テストを実行する前に 振る舞いを定義しておく

describe "" do
  before do
    allow_any_instance_of(Twitter).to receive(:post).and_raise(ArgumentError)
  end
end

and_raiseで例外エラーを発生させることも可能

コレクションクロージャメソッドのすゝめ

collect (map)、select (filter)、inject (reduce) メソッドを用いて処理を簡潔で分かりやすいものに書き換える

ループを取り除いてコレクションクロージャメソッドを使うと、コードをたどりやすくなる。コレクションクロージャメソッドは、コレクションの中を行き来したり、派生コレクションを作ったりするためのインフラストラクチャコードを隠し、ビジネスロジックに集中できるようにしてくれる。

引用: リファクタリング Ruby エディション

.select フィルタ処理

# before
managers = []
employees.each do |e|
  managers << e if e.manager?
end

# after
managers = employees.select{ |e| e.manager? }

.collect 収集処理

# before
offices = []
employees.each{ |e| offices << e.office }

# after
offices = employees.collect{ |e| e.office }

.select & .collect 複数の処理をまとめて

# before
managerOffices = []
employees.each do |e|
  managerOffices << e.office if e.manager?
end

# after
managerOffices = employees.select{ |e| e.manager? }
                          .collect{ |e| e.office }

gsubメソッド ブロックを用いる

Rubyのgsubメソッドは置換を行うメソッド

第1引数にマッチした値を第2引数の値に置き換える

values = "apple cherry banana"
values.gsub("apple", "pear")
#=> "pear cherry banana"

ブロックを使う方法があることを知った。

"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/) {"<p>#{$1}</p>"}
# => "<p>ruby</p> <p>python</p>"

$1から$9が特殊変数として定義されているそうな。

Ruby .mapの返り値は必ず配列が返ってくる

.mapというメソッドがあるがこれをハッシュに対して実行した際に、返り値がハッシュではなく配列が返ってきた。 ハッシュの返り値を期待していたもののmapメソッドは必ず配列が返る仕様になっているようで詰まった。 .mapには別名で.collectメソッドも存在する

each_slice each_with_object()

Rubyの配列操作ではメソッドが豊富に用意されている。

.each_sliceメソッドは引数に渡した数ごとに分割した値を返してくれる

.each_with_objectメソッドはinjectメソッドに似ている配列操作内で使用したいオブジェクトをブロック内で使えるようにする LINK

Observerパターン Ruby

Observerパターン

オブザーバーパターンとは

Observerとは「観測者」 あるオブジェクトの状態が変化した際に、そのオブジェクト自信が「観測者」に「通知」をする仕組み。 オブザーバーは3つのオブジェクトで構成される。

  • サブジェクト(subject):変化する側のオブジェクト
  • オブザーバ(Observer):変化を関連するオブジェクトに通知するインターフェース
  • 具象オブザーバ(ConcreateObserver):状態の変化に関連して具体的な処理を行う

状態とは、クラスが持つ変数の値のこと。

用いるメリット

  • オブジェクト間の依存度を下げることができる
  • 通知先の管理をオブザーバーが行うことで、サブジェクトは通知側を意識しなくていい

実装

  • Employee(サブジェクト):従業員を表す
  • Observable(オブザーバ):従業員のニュースを監視する仕組み(observer/Observable)
  • Payroll(具体オブザーバ1):給与の小切手の発行を行う
  • TaxMan(具体オブザーバ2):税金の請求書の発行を行う
require 'observer'
class Employee
  include Observable # Observerとして働く

  attr_reader :name, :title, :salary

  def initialize(name, title, salary)
    @name = name
    @title = title
    @salary = salary
    # 監視するインスタンスを追加
    add_observer(Payroll.new)
    add_observer(TaxMan.new)
  end

  # 給与をセットして、ConcreteObserverに通知する
  def salary=(new_salary)
    @salary = new_salary
    # changedで変化があったかを確認する
    changed
    # 変化があった場合に以下実行される
    notify_observers(self)
  end
end

#----------------具体オブザーバ---------------

# 給与の小切手の発行を行う
class Payroll
  # 変更通知用にupdateメソッドを持っている
  def update(changed_employee)
    puts "彼の給料は#{changed_employee.salary}になりました!#{changed_employee.title}のために新しい小切手を切ります。"
  end
end

# 税金の請求書の発行を行う
class TaxMan
  # 変更通知用にupdateメソッドを持っている
  def update(changed_employee)
    puts "#{changed_employee.name}に新しい税金の請求書を送ります"
  end
end

john = Employee.new('John', 'Senior Vice President', 5000)
john.salary = 6000
#=> 彼の給料は6000になりました!Senior Vice Presidentのために新しい小切手を切ります。
#=> Johnに新しい税金の請求書を送ります
john.salary = 7000
#=> 彼の給料は7000になりました!Senior Vice Presidentのために新しい小切手を切ります。
#=> Johnに新しい税金の請求書を送ります

ストラテジーとの違い

  • オブザーバー:発生しているオブジェクトに対してイベントを通知している
  • ストラテジー:何らかの処理を行うためにオブジェクトを取得している

Strategyパターン Ruby

Strategyパターン

ストラテジーパターンとは

抽象的な処理と具象的な処理を分離することで、 変化に強い構造を実現する。

委譲を用いてアルゴリズムを変更可能にする。

目的の部分としてはテンプレートメソッドパターンと似ているが、

問題に対する解決のアプローチが異なる。

ストラテジは「戦略」という意味。

条件によってアルゴリズムを切り替えるところは、まるで戦略を練っているようだからその名がついたそうな。

ストラテジーパターンの最終的な目的は、 アルゴリズムの交換」を行うこと。

テンプレートメソッドパターンが抱える問題

テンプレートメソッドパターンは 継承をベースにしているといこと。

これによりよろしくないつながりを生んでしまう。

良い部分を作り出すと同時にそこで生まれたつながりが複雑にしてしまう。

よろしくないつながりを生み出さずに変化しやすい形をとりたい。

そんなときに使うのがStrategyパターン

変化しやすいコードの塊を抽出し、 全く別のクラスに閉じ込める。

Strategyパターンでは、アルゴリズムの部分を他の部分と意識的に分離する。

そしてアルゴリズムとインターフェースを規定し、アルゴリズムの部分は委譲を用いて処理を他クラスの使って解決する。

これによりアルゴリズムは代替可能な状態になるため変更が容易になる。

アルゴリズムのクラス化」

構成

  • コンテキスト(Context):ストラテジの利用者
  • 抽象戦略(Strategy):同じ目的をもった一連のオブジェクトを抽象化したもの
  • 具象戦略(ConcreateStrategy):具体的なアルゴリズム

用いるメリット

  • 使用するアルゴリズムに多様性を持たせることができる
  • コンテキストと戦略を分離することでデータも分離することができる
  • 継承よりもストラテジを切り替える方が楽

実装

  • Report(コンテキスト):レポートを表す
  • Formatter(抽象戦略):レポート
  • HTMLFormatter(具象戦略):HTMLフォーマットでレポートを出力
  • PlaneTextFormatter(具象戦略):PlaneTextフォーマットでレポートを出力

f:id:Yamakichi:20161121000756p:plain

処理をstrategy(戦略)のclassとして定義する。 Formatterクラスを抽象戦略として定義しているが、これはインターフェースを定義するだけのクラスになるので、 Rubyらしくはありません。不要なら定義しなくても問題はない(ダックタイピングの哲学)

# レポートの出力を抽象化したクラス(抽象戦略)
class Formatter
  def output_report(title, text)
    raise 'Called abstract method !!'
  end
end

# HTML形式に整形して出力(具体戦略)
class HTMLFormatter < Formatter
  def output_report(report)
    puts "<html><head><title>#{report.title}</title></head><body>"
    report.text.each { |line| puts "<p>#{line}</p>" }
    puts '</body></html>'
  end
end

# PlaneText形式(*****で囲う)に整形して出力(具体戦略)
class PlaneTextFormatter < Formatter
  def output_report(report)
    puts "***** #{report.title} *****"
    report.text.each { |line| puts(line) }
  end
end

レポート側の実装

# レポートを表す(コンテキスト)
class Report
  attr_reader :title, :text
  attr_accessor :formatter

    # 引数にformatterオブジェクトを渡す(依存オブジェクトの注入)
  def initialize(formatter)
    @title = 'report title'
    @text = %w(text1 text2 text3)
    @formatter = formatter
  end

    # @formatterのoutput_reportに処理を委譲する
  def output_report
    @formatter.output_report(self)
  end
end

report = Report.new(HTMLFormatter.new)
report.output_report
#<html><head><title>report title</title></head><body>
#<p>text1</p>
#<p>text2</p>
#<p>text3</p>
#</body></html>

report.formatter = PlaneTextFormatter.new
report.output_report
#***** report title *****
#text1
#text2
#text3

注意すべきこと

  • コンテキストとストラテジ間のインターフェースがストラテジの種類の増加を妨げないようにすること
  • コンテキストの変更がストラテジに影響を与えないようにすること