Rubyのデザインパターン。プログラミングにおけるストラテジーパターンの活用
今日、あなたのリーダーはあなたを見つけると興奮し、会議に急いでいるので、あなたに小さな頼みごとをしたいと言っています。どんな頼みごとなのでしょうか?気になりますね。
彼は、あなたのプロジェクトのデータベースには、現在、非常に多くのユーザーに関するデータを保持するユーザー情報テーブルがあり、今、あなたはユーザー情報を選択的に照会する関数を完成させる必要があると教えてくれました。彼は、多くのユーザー名の配列が渡され、それらのユーザー名に基づいて対応するデータを検索する必要があると言っています。
簡単な機能なので、快諾しましたね。あなたのプロジェクトはMySQLデータベースを使用しているので、すぐに次のコードを書きました。
require 'mysql'
class QueryUtil
def find_user_info usernames
@db = Mysql.real_connect("localhost","root","123456","test",3306);
sql = "select * from user_info where "
usernames.each do |user|
sql << "username = '"
sql << user
sql << "' or "
end
puts sql
result = @db.query(sql);
result.each_hash do |row|
# process data read from the database
end
# The read data should be assembled into an object and returned later, omitted here
ensure
@db.close
end
end
ここでは、渡されたユーザー名の配列に基づいて SQL 文を作成し、 データベースにアクセスして対応する行を検索しています。デバッグのために、組み立てた SQL 文を出力することもあります。
そして、このメソッドをテストするために、次のようなコードを書きました。
qUtil = QueryUtil.new
qUtil.find_user_info ["Tom", "Jim", "Anna"]
さて、テストコードを実行すると、プログラムがエラーを起こしていることがわかります。そこで、すぐに出力されたSQL文をチェックしてみると、案の定、問題が見つかりました。
select * from user_info where username = 'Tom' or username = 'Jim' or username = 'Anna' or
綴られたSQL文は、最後に余分なorキーワードがある! なぜなら、forループは最後のデータにorを追加してはいけないのに、コードは愚かにも最後のデータにorキーワードを追加してしまい、結果としてSQL文の構文エラーになってしまったからです。
これはどうしたらいいのでしょうか?
あったあった! ということで、解決策を思いつきました。SQL文の組み立てが終わるのを待って、最後のorの前にインターセプトすればいいのです。そこで、コードを次のように変更します。
require 'mysql'
class QueryUtil
def find_user_info usernames
@db = Mysql.real_connect("localhost","root","123456","test",3306);
sql = "select * from user_info where "
usernames.each do |user|
sql << "username = '"
sql << user
sql << "' or "
end
sql = sql[0 . -" or ".length]
puts sql
result = @db.query(sql);
result.each_hash do |row|
# process the data read from the database
end
# The read data should be assembled into an object and returned later, omitted here
ensure
@db.close
end
end
Stringのintercept substringメソッドを使うと、最後のorの前の部分だけが取り出されるので、テストコードを再度実行すると、すべてうまくいき、印刷されるSQL文は以下のようになります。
select * from user_info where username = 'Tom' or username = 'Jim' or username = 'Anna'
よし、完了! 自信があるんだね。
ミーティングを終えたリーダーがやってきて、あなたの成果を見ています。しかし、あなたが使っているSQL文の組み立てアルゴリズムに何か違和感があるようで、何が問題なのかはっきりしません。そこで彼は、SQL文を組み立てるための別のアルゴリズムをあなたに教え、それをコードに追加するように頼みましたが、前のアルゴリズムは削除しないで、とりあえず残しておいてください、と言うと、また忙しくなったかのように逃げて行きました。それで、あなたは今教えてもらったアルゴリズムを追加すると、コードは次のようになります。
require 'mysql'
class QueryUtil
def find_user_info(usernames, strategy)
@db = Mysql.real_connect("localhost","root","123456","test",3306);
sql = "select * from user_info where "
if strategy == 1
usernames.each do |user|
sql << "username = '"
sql << user
sql << "' or "
end
sql = sql[0 . -" or ".length]
elsif strategy == 2
need_or = false
usernames.each do |user|
sql << " or " if need_or
sql << "username = '"
sql << user
sql << "'"
need_or = true
end
end
puts sql
result = @db.query(sql);
result.each_hash do |row|
# process the data read from the database
end
# The read data should be assembled into an object and returned later, omitted here
ensure
@db.close
end
end
このように、リーダーから教わったアセンブリアルゴリズムでは、boolean変数を使用して、orキーワードを追加する必要があるかどうかを制御していることがわかります。forループを最初に実行するときは、boolean値がfalseなのでorを追加せず、ループの最後でboolean値をtrueに代入し、ループが毎回headにorキーワードを追加するようにします。SQL文の最後にorをつけるのは、headメソッドを使っているので、もう気にする必要はないのです。そして、両方のアルゴリズムを維持するために、find_user_infoメソッドにパラメータを追加し、最初のアルゴリズムを使用する場合のストラテジー値を1、2番目のアルゴリズムを使用する場合のストラテジー値を2としています。
このようにすると、テストコードも以下のように変更する必要があります。
qUtil = QueryUtil.new
qUtil.find_user_info(["Tom", "Jim", "Anna"], 2)
ここでは、2番目のアルゴリズムを使ってSQL文をまとめるようにパラメータで指定していますが、印刷される結果は、1番目のアルゴリズムを使った場合とまったく同じです。
あなたは早速、忙しい合間を縫ってリーダーに今の結果を確認してもらったが、彼は相変わらずうるさそうにしていた。
"このように書くと、find_user_infoメソッドのロジックは複雑すぎて非常に読みづらく、将来の拡張につながらないし、第3、第4のアルゴリズムを追加したい場合、このメソッドはまだ良く見えるでしょうか? "リーダーはこの状況を解決するのに、strategyパターンを使うように指摘しましたが、strategyパターンのコアとなる考えは、アルゴリズムを別のオブジェクトに抽出することです。
正しい方向を示すために、彼は忙しい中、strategy patternを使った最適化の方法を教え始める。
まず、get_sql メソッドを含む親クラスを定義します。これは単に例外を投げるだけのものです。
class Strategy
def get_sql usernames
raise "You should override this method in subclass."
end
end
次に、上記の親クラスを継承した2つのサブクラスを定義し、SQL文をまとめるための2つのアルゴリズムを2つのサブクラスそれぞれに追加します。
class Strategy1
def get_sql usernames
sql = "select * from user_info where "
usernames.each do |user|
sql << "username = '"
sql << user
sql << "' or "
end
sql = sql[0 . -" or ".length]
end
end
class Strategy2
def get_sql usernames
sql = "select * from user_info where "
need_or = false
usernames.each do |user|
sql << " or " if need_or
sql << "username = '"
sql << user
sql << "'"
need_or = true
end
end
end
そして、組み立てられたSQL文は、以下のコードのように、QueryUtilのfind_user_infoメソッドの中でStrategyのget_sqlメソッドを呼び出すことで取得することが可能です。
require 'mysql'
class QueryUtil
def find_user_info(usernames, strategy)
@db = Mysql.real_connect("localhost","root","123456","test",3306);
sql = strategy.get_sql(usernames)
puts sql
result = @db.query(sql);
result.each_hash do |row|
# process the data read from the database
end
# The read data should be assembled into an object and returned later, omitted here
ensure
@db.close
end
end
最後に、テストコードでは、find_user_info メソッドを呼び出すときに、どのポリシーオブジェクトを使用する必要があるのかを表示するように指示するだけです。
qUtil = QueryUtil.new
qUtil.find_user_info(["Tom", "Jim", "Anna"], Strategy1.new)
qUtil.find_user_info(["Jac", "Joe", "Rose"], Strategy2.new)
プリントアウトされるSQL文は、以下のように全く予想外のものではありません。
select * from user_info where username = 'Tom' or username = 'Jim' or username = 'Anna'
select * from user_info where username = 'Jac' or username = 'Joe' or username = 'Rose'
ポリシーパターンで修正すると、コードがより読みやすくなり、拡張性も高くなるので、後から新しいアルゴリズムを追加する必要があっても安心です!
strategyパターンとsimple factoryパターンを組み合わせた例
要求事項
ショッピングモールのレジのソフトウェア、単価と顧客が購入したアイテムの数量に基づいて、コストを計算し、そこにキャンペーン、20%オフ、フル300オフまたはそのような何かであろう。
1. ファクトリーモデルを使用する
# -*- encoding: utf-8 -*-
# Cash charge abstract class
class CashSuper
def accept_cash(money)
end
end
#normal charge subclass
class CashNormal < CashSuper
def accept_cash(money)
money
end
end
#Discounted charges subclass
class CashRebate < CashSuper
attr_accessor :mony_rebate
def initialize(mony_rebate)
@mony_rebate = mony_rebate
end
def accept_cash(money)
money * mony_rebate
end
end
# rebate fee subclass
class CashReturn < CashSuper
attr_accessor :mony_condition, :mony_return
def initialize(mony_condition, mony_return)
@mony_condition = mony_condition
@mony_return = mony_return
end
def accept_cash(money)
if money > mony_condition
money - (money/mony_condition) * mony_return
end
end
end
# Cash charge factory class
class CashFactory
def self.create_cash_accept(type)
case type
when 'normal_charge'
CashNormal.new()
when '20% discount'
CashRebate.new(0.8)
when '100 off over 300'
CashReturn.new(300,100)
end
end
end
cash0 = CashFactory.create_cash_accept('normal charge')
p cash0.accept_cash(700)
cash1 = CashFactory.create_cash_accept('20% off')
p cash1.accept_cash(700)
cash2 = CashFactory.create_cash_accept('100 off over 300')
p cash2.accept_cash(700)
カスタム割引のパーセンテージと完全な割引の回数を指定します。
問題点
アクティブなカテゴリ、50%オフ、500以上の200オフなどを追加する場合、ファクトリークラスにブランチ構造を追加する必要があります。
アクティビティは多彩で、100点+10点とポイントがアクティビティ賞として用意されていなければならないポイントアクティビティも追加可能で、その場合はサブクラスを追加する必要があります。
しかし、アクティビティを追加するたびにファクトリークラスを修正しに行くのはひどい処理方法です。アルゴリズムの変更に直面したときに、もっと良い方法があるはずです。
2. strategyパターン
CashSuperとサブクラスは変更せず、以下を追加します。
class CashContext
attr_accessor :cs
def initialize(c_super)
@cs = c_super
end
def result(money)
cs.accept_cash(money)
end
end
type = '20% off'
cs=case type
when 'normal charge'
CashContext.new(CashNormal.new())
when '20% off'
CashContext.new(CashRebate.new(0.8))
when '100 off over 300'
CashContext.new(CashReturn.new(300,100))
end
p cs.result(700)
CashContext クラスは異なる CashSuper サブクラスをラップし、対応する結果を返す。つまり、アルゴリズムがどのように変化しても、その結果を用いて得ることができるように、異なるアルゴリズムをラップするのである。結果はresultを使って得ることができる。
しかし、現状では、どのアルゴリズムを使うか、ユーザーが判断しに行く必要がある問題があります。これは、Simple Workshopクラスと組み合わせることができます。
3. ストラテジーとシンプルワークショップの融合
class CashContext
attr_accessor :cs
def initialize(type)
case type
when 'normal charges'
@cs = CashNormal.new()
when '20% off'
@cs = CashRebate.new(0.8)
when '100 off over 300'
@cs = CashReturn.new(300,100)
end
end
def result(money)
cs.accept_cash(money)
end
end
cs=CashContext.new('20% off')
p cs.result(700)
異なるサブクラスはCashContextでインスタンス化されます。(単純なファクトリー)
サブクラス選択のプロセスを内部に移し、アルゴリズムをカプセル化する(ポリシーパターン)。
呼び出し側は、パラメータ(アクティビティの種類、元の価格)を渡して最終結果を得るという、よりシンプルな使い方ができるようになりました。
ここでは、ユーザーは1つのクラス(CashContext)を知るだけでよいのに対して、SimpleWorksは2つのクラス(CashFactoryのaccept_cashメソッドとCashFactory)を知る必要があり、より徹底したカプセル化が行われていることが分かります。
関連
-
Rubyイテレータの知識まとめ
-
GitHubが提唱するRubyコードの書き方まとめ
-
RubyのHash構造体の基本操作のまとめ
-
Nokogiriパッケージを使ってXML形式のデータを操作するためのRubyチュートリアル
-
Rubyのデザインパターンプログラミングにおけるコマンドパターンの活用を徹底分析
-
Rubyのデザインパターン。プログラミングにおけるアピアランスパターンの応用
-
Ruby+Watirの自動テスト環境とWindowsでのデータ読み込みについて
-
RubyGnome2 ライブラリを用いた GTK 環境での Ruby GUI プログラミングの基本的な考え方
-
Rubyにおける継承とメッセージング
-
Rubyのモジュールに関する基礎知識
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン