スプラウトクラスを使用したRubyのリファクタリング
一部のレガシーアプリケーションを使用する際の最も厄介な課題の1つは、コードがテスト可能になるように記述されていないことです。したがって、意味のあるテストを書くことは困難または不可能です 。
これは鶏が先か卵が先かという問題です。レガシーアプリケーションのテストを作成するには、コードを変更する必要がありますが、最初にテストを作成しないと、自信を持ってコードを変更することはできません。
このパラドックスにどのように対処しますか?
これは、MichaelFeathersの優れたレガシーコードで効果的に作業するで取り上げられている多くの主題の1つです。 。今日は、スプラウトクラスという本の特定のテクニックにズームインします。 。
レガシーコードの準備をしてください!
Appointment
と呼ばれるこの古いActiveRecordクラスを見てみましょう。 。かなり長く、実際には数百行長くなっています。
class Appointment < ActiveRecord::Base
has_many :appointment_services, :dependent => :destroy
has_many :services, :through => :appointment_services
has_many :appointment_products, :dependent => :destroy
has_many :products, :through => :appointment_products
has_many :payments, :dependent => :destroy
has_many :transaction_items
belongs_to :client
belongs_to :stylist
belongs_to :time_block_type
def record_transactions
transaction_items.destroy_all
if paid_for?
save_service_transaction_items
save_product_transaction_items
save_tip_transaction_item
end
end
def save_service_transaction_items
appointment_services.reload.each { |s| s.save_transaction_item(self.id) }
end
def save_product_transaction_items
appointment_products.reload.each { |p| p.save_transaction_item(self.id) }
end
def save_tip_transaction_item
TransactionItem.create!(
:appointment_id => self.id,
:stylist_id => self.stylist_id,
:label => "Tip",
:price => self.tip,
:transaction_item_type_id => TransactionItemType.find_or_create_by_code("TIP").id
)
end
end
トランザクションレポート領域にいくつかの新しい機能を追加するように求められたが、Appointment
クラスの依存関係が多すぎるため、多くのリファクタリングなしでテストできません。どうすればよいですか?
1つのオプションは、変更を加えることです:
def record_transactions
transaction_items.destroy_all
if paid_for?
save_service_transaction_items
save_product_transaction_items
save_tip_transaction_item
send_thank_you_email_to_client # New code
end
end
def send_thank_you_email_to_client
ThankYouMailer.thank_you_email(self).deliver
end
上記のコードには2つの問題があります:
-
Appointment
多くの異なる責任があり(単一責任原則の違反)、これらの責任の1つは*取引の記録です。 。Appointment
にトランザクション関連のコードを追加する クラス、**コードを少し悪化させています *。 -
新しい統合テストを作成して、メールが届いたことを確認することもできますが、
Appointment
がないためです。 テスト可能な状態のクラスでは、単体テストを追加できませんでした。 テストされていないコードをさらに追加します 、もちろん悪いです。 (実際、MichaelFeathersはレガシーコードを定義しています 「テストなしのコード」として、レガシーコードに_more_レガシーコードを追加します。)
新しいコードを単にインラインで追加するよりも優れた解決策は、トランザクション記録の動作を独自のクラスに抽出することです。これをTransactionRecorder
と呼びます。 :
class TransactionRecorder
def initialize(options)
@appointment_id = options[:appointment_id]
@appointment_services = options[:appointment_services]
@appointment_products = options[:appointment_products]
@stylist_id = options[:stylist_id]
@tip = options[:tip]
end
def run
save_service_transaction_items(@appointment_services)
save_product_transaction_items(@appointment_products)
save_tip_transaction_item(@appointment_id, @stylist_id, @tip_amount)
end
def save_service_transaction_items(appointment_services)
appointment_services.each { |s| s.save_transaction_item(appointment_id) }
end
def save_product_transaction_items(appointment_products)
appointment_products.each { |p| p.save_transaction_item(appointment_id) }
end
def save_tip_transaction_item(appointment_id, stylist_id, tip)
TransactionItem.create!(
appointment_id: appointment_id,
stylist_id: stylist_id,
label: "Tip",
price: tip,
transaction_item_type_id: TransactionItemType.find_or_create_by_code("TIP").id
)
end
end
次に、Appointment
これだけに絞ることができます:
class Appointment < ActiveRecord::Base
has_many :appointment_services, :dependent => :destroy
has_many :services, :through => :appointment_services
has_many :appointment_products, :dependent => :destroy
has_many :products, :through => :appointment_products
has_many :payments, :dependent => :destroy
has_many :transaction_items
belongs_to :client
belongs_to :stylist
belongs_to :time_block_type
def record_transactions
transaction_items.destroy_all
if paid_for?
TransactionRecorder.new(
appointment_id: id,
appointment_services: appointment_services,
appointment_products: appointment_products,
stylist_id: stylist_id,
tip: tip
).run
end
end
end
Appointment
のコードはまだ変更中です 、これはテストできませんが、TransactionRecorder
のすべてをwe_can_テストできるようになりました 、およびインスタンス変数を使用するのではなく引数を受け入れるように各関数を変更したため、各関数を個別にテストすることもできます。ですから、私たちは今、始めたときよりもはるかに良い場所にいます。
-
TracePointを使用してRubyでの複雑な例外動作を調査する
特に大規模なアプリでは、例外を除いて何が起こっているのかを理解するのが非常に難しい場合があります。既存のプロジェクト内のコードで作業していると想像してください。例外を発生させると、何か奇妙なことが起こります。たぶん例外は飲み込まれます。たぶん、環境変数が変更されます。例外が別の例外にラップされる可能性があります。 TracePointsを使用して、例外が飲み込まれた場合でも、アプリの例外に関するもう少し情報を取得する簡単な方法を紹介します。 便利な例 Railsのコントローラーとビューの境界は、例外がロジックに反しているように見える1つの場所です。自分で見るのは簡単です。ビューで例外を発生
-
Rubyでの静的分析
ソースコードを解析して、すべてのメソッド、それらが定義されている場所、およびそれらが取る引数を見つけたいとします。 どうすればこれができますか? あなたの最初のアイデアはそれのために正規表現を書くことかもしれません… しかし、もっと良い方法はありますか? はい! 静的分析 は、ソースコード自体から情報を抽出する必要がある場合に使用できる手法です。 これは、ソースコードをトークンに変換する(解析する)ことによって行われます。 さっそく始めましょう! パーサージェムの使用 Rubyには標準ライブラリで利用可能なパーサーがあります。名前はRipperです。出力を操作するのは難し