ใน การเขียนโปรแกรมเชิงวัตถุ วัตถุหนึ่งมักจะขึ้นอยู่กับวัตถุอื่นเพื่อให้ทำงาน
ตัวอย่างเช่น หากฉันสร้างคลาสง่าย ๆ เพื่อเรียกใช้รายงานทางการเงิน:
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 ของคุณสำหรับสถานการณ์เหล่านี้