Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

スプラウトクラスを使用した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つの問題があります:

  1. Appointment 多くの異なる責任があり(単一責任原則の違反)、これらの責任の1つは*取引の記録です。 Appointmentにトランザクション関連のコードを追加する クラス、**コードを少し悪化させています *。

  2. 新しい統合テストを作成して、メールが届いたことを確認することもできますが、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_テストできるようになりました 、およびインスタンス変数を使用するのではなく引数を受け入れるように各関数を変更したため、各関数を個別にテストすることもできます。ですから、私たちは今、始めたときよりもはるかに良い場所にいます。


  1. TracePointを使用してRubyでの複雑な例外動作を調査する

    特に大規模なアプリでは、例外を除いて何が起こっているのかを理解するのが非常に難しい場合があります。既存のプロジェクト内のコードで作業していると想像してください。例外を発生させると、何か奇妙なことが起こります。たぶん例外は飲み込まれます。たぶん、環境変数が変更されます。例外が別の例外にラップされる可能性があります。 TracePointsを使用して、例外が飲み込まれた場合でも、アプリの例外に関するもう少し情報を取得する簡単な方法を紹介します。 便利な例 Railsのコントローラーとビューの境界は、例外がロジックに反しているように見える1つの場所です。自分で見るのは簡単です。ビューで例外を発生

  2. Rubyでの静的分析

    ソースコードを解析して、すべてのメソッド、それらが定義されている場所、およびそれらが取る引数を見つけたいとします。 どうすればこれができますか? あなたの最初のアイデアはそれのために正規表現を書くことかもしれません… しかし、もっと良い方法はありますか? はい! 静的分析 は、ソースコード自体から情報を抽出する必要がある場合に使用できる手法です。 これは、ソースコードをトークンに変換する(解析する)ことによって行われます。 さっそく始めましょう! パーサージェムの使用 Rubyには標準ライブラリで利用可能なパーサーがあります。名前はRipperです。出力を操作するのは難し