Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Decoupling Ruby:การมอบหมายและการพึ่งพาการฉีด

ใน การเขียนโปรแกรมเชิงวัตถุ วัตถุหนึ่งมักจะขึ้นอยู่กับวัตถุอื่นเพื่อให้ทำงาน

ตัวอย่างเช่น หากฉันสร้างคลาสง่าย ๆ เพื่อเรียกใช้รายงานทางการเงิน:

class FinanceReport
  def net_income
    FinanceApi.gross_income - FinanceApi.total_costs
  end
end

เราสามารถพูดได้ว่า FinanceReport ขึ้นอยู่กับ FinanceApi ซึ่งใช้เพื่อดึงข้อมูลจากผู้ประมวลผลการชำระเงินภายนอก

แต่ถ้าเราต้องการเข้าถึง API อื่นในบางจุดล่ะ หรือมีแนวโน้มมากขึ้นว่าถ้าเราต้องการทดสอบคลาสนี้โดยไม่กระทบกับทรัพยากรภายนอกล่ะ คำตอบที่พบบ่อยที่สุดคือการใช้ Dependency Injection

ด้วย Dependency Injection เราไม่ได้อ้างถึง FinanceApi . อย่างชัดเจน ภายใน FinanceReport . แต่เราส่งผ่านเป็นข้อโต้แย้ง เรา ฉีด มัน.

การใช้ Dependency Injection ทำให้ชั้นเรียนของเรากลายเป็น:

class FinanceReport
  def net_income(financials)
    financials.gross_income - financials.total_costs
  end
end

ตอนนี้ชั้นเรียนของเราไม่มีความรู้ว่า FinanceApi วัตถุยังมีอยู่! เราสามารถส่ง สิ่งของใดๆ . ได้ ตราบเท่าที่มันใช้ gross_income และ total_costs .

มีประโยชน์หลายประการ:

  • ตอนนี้โค้ดของเรา "จับคู่" กับ FinanceApi .
  • เราถูกบังคับให้ใช้ FinanceApi ผ่านอินเทอร์เฟซสาธารณะ
  • ตอนนี้เราสามารถส่งผ่านวัตถุจำลองหรือต้นขั้วในการทดสอบของเราได้ เพื่อที่เราจะไม่ต้องเข้าถึง API จริง

นักพัฒนาส่วนใหญ่พิจารณา Dependency Injection เป็นสิ่งที่ดีโดยทั่วไป (ฉันด้วย!) อย่างไรก็ตาม เช่นเดียวกับเทคนิคทั้งหมด มีข้อแลกเปลี่ยน

รหัสของเรามีความทึบขึ้นเล็กน้อยในขณะนี้ เมื่อเราใช้ FinanceApi . อย่างชัดเจน เป็นที่ชัดเจนว่าค่านิยมของเรามาจากไหน มันไม่ชัดเจนเท่าในโค้ดที่รวม Dependency Injection

หากการโทรนั้นจะไปที่ self เราจึงได้ทำให้โค้ดมีความละเอียดมากขึ้น แทนที่จะใช้กระบวนทัศน์ "ส่งข้อความไปยังวัตถุและปล่อยให้มันแสดง" เชิงวัตถุ เราพบว่าเรากำลังย้ายไปยังกระบวนทัศน์ "อินพุต -> เอาต์พุต" ที่ใช้งานได้ดีกว่า

เป็นกรณีสุดท้ายนี้ (การเปลี่ยนเส้นทางการโทรที่จะไปที่ self ) ที่อยากดูวันนี้ ฉันต้องการนำเสนอทางเลือกที่เป็นไปได้สำหรับ Dependency Injection สำหรับสถานการณ์เหล่านี้:เปลี่ยนคลาสพื้นฐานแบบไดนามิก (นิดนึง)

ปัญหาที่ต้องแก้ไข

กลับมาอีกครั้งและเริ่มต้นด้วยปัญหาที่นำฉันไปสู่เส้นทางนี้เพื่อเริ่มต้นด้วย:รายงาน PDF

ลูกค้าของฉันร้องขอความสามารถในการสร้างรายงาน PDF ที่พิมพ์ได้หลายฉบับ — รายงานฉบับหนึ่งระบุค่าใช้จ่ายทั้งหมดสำหรับบัญชีหนึ่งราย รายได้อื่นในรายการ อีกฉบับคือกำไรที่คาดการณ์ไว้สำหรับปีต่อๆ ไป ฯลฯ

เราใช้ prawn gem เพื่อสร้าง PDF เหล่านี้ โดยแต่ละรายงานเป็นวัตถุ Ruby ที่แยกย่อยจาก Prawn::Document .

บางสิ่งเช่นนี้:

class CostReport < Prawn::Document
  def initialize(...)
    ...
  end

  def render
    text "Cost Report"
    move_down 20
    ...
  end

จนถึงตอนนี้ดีมาก แต่ข้อเสียคือ ลูกค้าต้องการรายงาน "ภาพรวม" ที่รวมบางส่วนจากรายงานอื่นๆ เหล่านี้ทั้งหมด .

โซลูชันที่ 1:การพึ่งพาการฉีด

ดังที่ได้กล่าวไว้ก่อนหน้านี้ วิธีแก้ปัญหาทั่วไปอย่างหนึ่งสำหรับปัญหาประเภทนี้คือการจัดโครงสร้างโค้ดใหม่เพื่อใช้ Dependency Injection นั่นคือ แทนที่จะให้รายงานเหล่านี้เรียกเมธอดใน self เราจะส่งต่อในเอกสาร PDF ของเราเป็นอาร์กิวเมนต์แทน

สิ่งนี้จะทำให้เรามีบางสิ่งเช่น:

class CostReport < Prawn::Document
...
  def title(pdf = self)
    pdf.text "Cost Report"
    pdf.move_down 20
    ...
  end
end

ใช้งานได้ แต่มีค่าใช้จ่ายบางอย่างที่นี่ ประการหนึ่ง วิธีการวาดทุกวิธีในตอนนี้ต้องใช้ pdf อาร์กิวเมนต์ และทุกๆ การเรียก prawn ตอนนี้ต้องผ่าน pdf . นี้ อาร์กิวเมนต์

การพึ่งพาอาศัยกันมีประโยชน์บางประการ:มันผลักดันเราไปสู่องค์ประกอบที่แยกจากกันในระบบของเรา และช่วยให้เราผ่านการจำลองหรือสตับเพื่อทำให้การทดสอบหน่วยง่ายขึ้น

อย่างไรก็ตาม เราไม่ได้เก็บเกี่ยวผลประโยชน์จากผลประโยชน์เหล่านี้ในสถานการณ์ของเรา เราอย่างแรง .แล้ว คู่กับ prawn API ดังนั้นการเปลี่ยนไปใช้ไลบรารี PDF อื่นแทบจะต้องมีการเขียนโค้ดใหม่ทั้งหมด

การทดสอบก็ไม่ใช่เรื่องใหญ่เช่นกัน เพราะในกรณีของเรา การทดสอบสร้างรายงาน PDF ด้วยการทดสอบอัตโนมัตินั้นยุ่งยากเกินกว่าจะคุ้มค่า

ดังนั้น Dependency Injection จึงให้พฤติกรรมที่เราต้องการ แต่ยังเพิ่มค่าใช้จ่ายเพิ่มเติมโดยมีประโยชน์น้อยที่สุดสำหรับเรา มาดูตัวเลือกอื่นกัน

โซลูชัน 2:การมอบหมาย

ไลบรารีมาตรฐานของ Ruby มี SimpleDelegator . ให้เรา เป็นวิธีที่ง่ายในการใช้ลวดลายมัณฑนากร คุณส่งผ่านอ็อบเจ็กต์ของคุณไปยังคอนสตรัคเตอร์ จากนั้นเมธอดใดๆ ที่เรียกไปยัง delegator จะถูกส่งต่อไปยังอ็อบเจ็กต์ของคุณ

การใช้ SimpleDelegator เราสามารถสร้างคลาสรายงานพื้นฐานที่ล้อมรอบ prawn .

class PrawnWrapper < SimpleDelegator
  def initialize(document: nil)
    document ||= Prawn::Document.new(...)
    super(document)
  end
end

จากนั้น เราสามารถอัปเดตรายงานของเราเพื่อรับช่วงต่อจากคลาสนี้ และรายงานจะยังคงทำงานเหมือนเดิม โดยใช้เอกสารเริ่มต้นที่สร้างขึ้นในตัวเริ่มต้นของเรา ความมหัศจรรย์เกิดขึ้นเมื่อเราใช้สิ่งนี้ใน ภาพรวม รายงาน:

class OverviewReport < PrawnWrapper
  ...
  def render
    sales = SaleReport.new(..., document: self)
    sales.sales_table
    costs = CostReport.new(..., document: self)
    costs.costs_pie_chart
    ...
  end
end

ที่นี่ SaleReport#sales_table และ CostReport#costs_pie_chart ยังคงไม่เปลี่ยนแปลง แต่เรียก prawn (เช่น text(...) , move_down 20 ฯลฯ) กำลังส่งต่อไปยัง OverviewReport ผ่าน SimpleDelegator เราสร้าง

ในแง่ของพฤติกรรม เราได้ทำให้มันเหมือนกับว่า SalesReport ตอนนี้เป็นคลาสย่อยของ OverviewReport . ในกรณีของเรา นี่หมายถึงการเรียก prawn . ทั้งหมด API ของตอนนี้ไป SalesReport -> OverviewReport -> Prawn::Document .

วิธีการทำงานของ SimpleDelegator

ทาง SimpleDelegator การทำงานภายใต้ประทุนนั้นโดยทั่วไปจะใช้ method_missing ของ Ruby ฟังก์ชันเพื่อส่งต่อการเรียกเมธอดไปยังอ็อบเจกต์อื่น

ดังนั้น SimpleDelegator (หรือคลาสย่อยของมัน) ได้รับการเรียกเมธอด ถ้ามันใช้วิธีนั้นก็เยี่ยมมาก มันจะดำเนินการเช่นเดียวกับวัตถุอื่น ๆ อย่างไรก็ตาม หากไม่มีเมธอดที่กำหนดไว้ ก็จะกด method_missing . method_missing จะพยายาม call วิธีการนั้นบนวัตถุที่มอบให้กับตัวสร้าง

ตัวอย่างง่ายๆ:

require 'simple_delegator'
class Thing
  def one
    'one'
  end
  def two
    'two'
  end
end

class ThingDecorator < SimpleDelegator
  def two
    'three!'
  end
end

ThingDecorator.new(Thing.new).one #=> "one"
ThingDecorator.new(Thing.new).two #=> "three!"

โดยการแบ่งคลาสย่อย SimpleDelegator ด้วย ThingDecorator . ของเราเอง คลาสที่นี่ เราสามารถเขียนทับเมธอดบางอย่าง และปล่อยให้คนอื่นตกเป็นค่าเริ่มต้น Thing วัตถุ

ตัวอย่างเล็กน้อยข้างต้นไม่ได้ทำ SimpleDelegator ความยุติธรรมแม้ว่า คุณอาจดูโค้ดนี้แล้วบอกกับฉันว่า "ไม่จัดคลาสย่อย Thing ให้ผลลัพธ์แบบเดียวกันกับฉันไหม”

ใช่ใช่มันไม่ แต่ข้อแตกต่างที่สำคัญคือ SimpleDelegator นำวัตถุที่จะมอบหมายให้เป็นอาร์กิวเมนต์ในตัวสร้าง ซึ่งหมายความว่า เราสามารถส่งผ่านวัตถุต่างๆ ได้ในขณะใช้งานจริง .

นี่คือสิ่งที่อนุญาตให้ใช้ในการเปลี่ยนเส้นทางการโทรไปยัง prawn วัตถุในโซลูชันที่ 2 ด้านบน ถ้าเราเรียกรายงานตัวเดียวว่า prawn การโทรไปที่เอกสารใหม่ที่สร้างขึ้นในตัวสร้าง อย่างไรก็ตาม รายงานภาพรวมสามารถเปลี่ยนแปลงสิ่งนี้เพื่อให้เรียก prawn จะถูกส่งต่อไปยัง มัน เอกสาร

บทสรุป

Dependency Injection น่าจะเป็นทางออกที่ดีที่สุดสำหรับ ส่วนใหญ่ ปัญหาการแยกส่วน ส่วนใหญ่ ของเวลานั้น

อย่างไรก็ตาม เช่นเดียวกับเทคนิคทั้งหมด มีข้อแลกเปลี่ยน ในกรณีของฉัน ฉันไม่คิดว่าค่าโสหุ้ยที่ DI นำเสนอนั้นคุ้มกับประโยชน์ที่ได้รับ ดังนั้นฉันจึงมองหาโซลูชันอื่น

เช่นเดียวกับทุกสิ่งใน Ruby มีทางอื่นเสมอ . ฉันจะไม่เข้าถึงวิธีแก้ปัญหานี้บ่อยๆ แต่แน่นอนว่าเป็นส่วนเสริมที่ดีสำหรับ Ruby toolbelt ของคุณสำหรับสถานการณ์เหล่านี้