কম্পিউটার টিউটোরিয়াল

Ruby কোড ডিকাপলিং: ডেলিগেশন বনাম ডিপেন্ডেন্সি ইনজেকশন

অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং-এ, একটি অবজেক্ট প্রায়শে অন্য অবজেক্টের ওপর নির্ভর করে যাতে সেটি কাজ করতে পারে।

উদাহরণস্বরূপ, যদি আমি একটি সিম্পল ক্লাস বানাই ফাইন্যান্স রিপোর্ট চালানোর জন্য:

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

আমরা বলতে পারি FinanceReport নির্ভর করে FinanceApi-র ওপর, যা একটি এক্সটার্নাল পেমেন্ট প্রসেসর থেকে ইনফরমেশন পুল করতে ব্যবহৃত হয়।

কিন্তু যদি আমরা ভবিষ্যতে অন্য একটি API হিট করতে চাই? অথবা আরও সম্ভবত, আমরা এই ক্লাসটি এক্সটার্নাল রিসোর্স হিট না করে টেস্ট করতে চাই? সবচেয়ে সাধারণ উত্তর হলো Dependency Injection ব্যবহার করা।

Dependency Injection-এ, আমরা FinanceReport-এর ভিতরে এক্সপ্লিসিটলি FinanceApi রেফার না করি। বরং, আমরা এটি আর্গুমেন্ট হিসেবে পাস করি। আমরা এটি ইনজেক্ট করি।

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-কে সাধারণত ভালো জিনিস मानেন (আমিও!)। তবে সকল টেকনিকের মতো, এখানেও ট্রেড-অফ আছে।

আমাদের কোড একটু বেশি অপেক (opaque) হয়ে গেছে। যখন আমরা এক্সপ্লিসিটলি FinanceApi ব্যবহার করতাম, তখন স্পষ্ট ছিল আমাদের ভ্যালুগুলো কোথা থেকে আসছে। Dependency Injection অন্তর্ভুক্ত কোডে সেটি একদম স্পষ্ট নয়।

যদি কলগুলো অন্যথায় self-এ যেত, তবে আমরা কোডকে আরও ভার্বোজ (verbose) করে ফেললাম। অবজেক্ট-ওরিয়েন্টেড "একটি অবজেক্টকে মেসেজ পাঠাও এবং সেটি একশন নেওয়া দাও" প্যারাডাইম ব্যবহার করার বদলে, আমরা আরও ফাংশনাল "ইনপুট → আউটপুট" প্যারাডাইমের দিকে যাচ্ছি।

এই শেষ কেস (যে কলগুলো self-এ যেত) সেটিই আজ আমি দেখতে চাই। আমি এই পরিস্থিতিতে Dependency Injection-এর একটি সম্ভাব্য বিকল্প উপস্থাপন করতে চাই: বেস ক্লাস ডাইনামিক্যালি চেঞ্জ করা (প্রায়)।

সমাধান করার সমস্যা

একটু পিছিয়ে যাই এবং যে সমস্যাটি আমাকে এই পথে আনলো সেটি থেকে শুরু করি: PDF রিপোর্ট।

আমার ক্লায়েন্ট বিভিন্ন প্রিন্টযোগ্য PDF রিপোর্ট জেনারেট করার ক্ষমতা চেয়েছিল — একটি রিপোর্ট যেটি একাউন্টের সকল খরচ লিস্ট করবে, অন্যটি রেভেনিউ, আরেকটি ভবিষ্যতের বছরের প্রজেক্টেড প্রফিট, ইত্যাদি।

আমরা veteran prawn gem ব্যবহার করছি এই PDF গুলো তৈরি করতে, প্রতিটি রিপোর্ট হচ্ছে একটি Ruby অবজেক্ট যা Prawn::Document-এর সাবক্লাস।

এরকম কিছু:

class CostReport < Prawn::Document
  def initialize(...)
    ...
  end
  def render
    text "Cost Report"
    move_down 20
    ...
  end
end 

এ পর্যন্ত ভালো। কিন্তু রাব: ক্লায়েন্ট চায় একটি "Overview" রিপোর্ট যেটি এই সব রিপোর্টের অংশগুলোকে ইনক্লুড করবে।

সমাধান ১: Dependency Injection

আগে বলা অনুযায়ী, এই ধরণের সমস্যার একটি সাধারণ সমাধান হলো কোডকে 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 আর্গুমেন্ট দিয়ে যেতে হবে।

Dependency injection-এর কিছু সুবিধা আছে: এটি আমাদের সিস্টেমে ডিকাপলড কম্পোনেন্টের দিকে পুশ করে এবং মক বা স্টাব পাস করার সুবিধা দিয়ে ইউনিট টেস্টিং সহজ করে।

তবে আমাদের সিনেরিওতে আমরা এই সুবিধাগুলো পাচ্ছি না। আমরা ইতিমধ্যে prawn API-র সাথে দৃঢ়ভাবে কাপলড, তাই অন্য PDF লাইব্রেরিতে চেঞ্জ করলে gầnশুরে পুরো কোড রিরাইট লাগবে।

টেস্টিংও আমাদের কেসে বড় চিন্তা নয়, কারণ জেনারেটেড PDF রিপোর্ট অটোমেটেড টেস্ট দিয়ে টেস্ট করা খুব কামসรรมরদ্দ।

তাই Dependency Injection আমাদের আচরণ দিয়েছে কিন্তু অতিরিক্ত ওভারহেডও যোগ করেছে যেনামাত্র সুবিধা দিয়েছে। আরেকটি অপশন দেখি।

সমাধান ২: ডেলিগেশন (Delegation)

Ruby-এর স্ট্যান্ডার্ড লাইব্রেরি আমাদের SimpleDelegator দিয়েছে ডেকোরেটর প্যাটার্ন ইমপ্লিমেন্ট করার সহজ উপায় হিসেবে। আপনি আপনার অবজেক্টটি কনস্ট্রাক্টরে পাস করুন, এবং তারপর ডেলিগেটরে যেকোনো মেথড কল আপনার অবজেক্টে ফরওয়ার্ড হয়।

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, ইত্যাদি) এখন SimpleDelegator দিয়ে OverviewReport-এ ফরওয়ার্ড হচ্ছে।

বিহেভিয়ারের দৃষ্টিতে, আমরা এতolah কাছে এনেছি যেন SalesReport এখন OverviewReport-এর সাবক্লাস। আমাদের কেসে, এর মানে prawn-এর API-এ সকল কল এখন যাচ্ছে SalesReport → OverviewReport → Prawn::Document।

SimpleDelegator কীভাবে কাজ করে

SimpleDelegator हुडের নিচে মূলত Ruby-এর method_missing ফাংশনালিটি ব্যবহার করে মেথড কল অন্য অবজেক্টে ফরওয়ার্ড করে।

সেজন্য 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!" 

আমাদের নিজস্ব ThingDecorator ক্লাস দিয়ে SimpleDelegator-কে সাবক্লাস করে, আমরা কিছু মেথড ওভাররাইড করতে পারি এবং অন্যগুলো ডিফল্ট Thing অবজেক্টে ফল-থ্রু করতে দিতে পারি।

উপরোক্ত טרিভিয়াল উদাহরণ SimpleDelegator-কে বিচার করতে পারেন না। আপনি এই কোডে তাকিয়ে বলতে পারেন, "কি Thing-কে সাবক্লাস করলে একই ফলাফল না मिलবে?"

হ্যাঁ, হ্যাঁ মিলবে। কিন্তু এখানে কী ডিফারেন্স: SimpleDelegator কনস্ট্রাক্টরে আর্গুমেন্ট হিসেবে সেটি নেয় যেটিকে ডেলিগেট করবে। এর মানে রানটাইমে আমরা ভিন্ন অবজেক্ট পাস করতে পারি।

এটিই আমাদের সমাধান ২-এ prawn অবজেক্টে কল রিডাইরেক্ট করার অনুমতি দিয়েছে। একক রিপোর্ট কল করলে, prawn কলগুলো কনস্ট্রাক্টরে তৈরি নতুন ডকুমেন্টে যায়। কিন্তু ওভারভিউ রিপোর্ট এটি চেঞ্জ করতে পারে যাতে prawn কলগুলো তার নিজস্ব ডকুমেন্টে ফরওয়ার্ড হয়।

উপসংহার

Dependency Injection সম্ভবত অধিকাংশ ডিকাপলিং সমস্যার সেরা সমাধান অধিকাংশ সময়।

তবে সকল টেকনিকের মতো, এখানেও ট্রেড-অফ আছে। আমার কেসে, আমি মনে করলাম DI-এর অফার করা সুবিধাগুলোর তুলনায় ওভারহেড টি ওরথ নয়, তাই আমি অন্য সমাধান খুঁজতে লাগলাম।

Ruby-এ সব কিছুর মত, সদা আরেকটি পথ আছে। আমি এই সমাধানটা সচরাচর ব্যবহার করব না, কিন্তু নিশ্চিতভাবেই এটি আপনার Ruby টুলবেল্টে এই পরিস্থিতিগুলোর জন্য একটি চমৎকার অডিশন।

  1. রুবিতে বিটওয়াইজ অপারেশন মাস্টার করুন: টিপস অ্যান্ড ট্রিকস

  2. রুবিতে ল্যাম্বডা মাস্টার করুন: বেসিক থেকে অ্যাডভান্সড টেকনিক

  3. রুবি ইউনিকোড নর্মালাইজেশন আয়ত্ত করুন: অপ্রত্যাশিত স্ট্রিং আচরণ এড়ান

  4. রুবি ২.৬-এ ৯টি গুরুত্বপূর্ণ উন্নতির অন্বেষণ