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

রুবিতে সেফ মনকিপ্যাচিং: মেইনটেইনেবল কোডের জন্য বেস্ট প্র্যাকটিস

যখন আমি ২০১১ সালে প্রফেশনালি রুবি কোড লিখতে শুরু করলাম, তখন ভাষাটি সম্পর্কে আমাকে সবচেয়ে বেশি চমকদার লেগেছিল এর ফ্লেক্সিবিলিটি। লেগেছিল যেন রুবিতে সব কিছু সম্ভব। C# এবং Java-এর মতো কঠোর ভাষাগুলোর তুলনায়, রুবি প্রোগ্রাম ঠিক যেন জীবন্ত।

রুবি প্রোগ্রামে আপনি কত চমৎকার জিনিস করতে পারেন সেটি চিন্তা করুন। আপনি ইচ্ছেমতো মেথড ডিফাইন ও ডিলিট করতে পারেন। আপনি অস্তিত্বহীন মেথড কল করতে পারেন। আপনি হাওয়া থেকে পুরো নেমলেস ক্লাস তৈরি করতে পারেন। এটি অবিশ্বাস্য।

কিন্তু গল্প এখানেই শেষ নয়। আপনি এই টেকনিকগুলো নিজের কোডের ভিতরে ব্যবহার করার পাশাপাশি, রুবি আপনাকে ভার্চুয়াল মেশিনে লোড করা যেকোনো কোডের ওপর এগুলো অ্যাপ্লাই করার অনুমতি দেয়। অর্থাৎ, আপনি অন্যদের কোডের সাথে নিজের কোডের মতো সহজে "খেলতে" পারেন।

মনকিপ্যাচ কী?

প্রবেশ করুন মনকিপ্যাচ-এ।

সংক্ষেপে, মনকিপ্যাচ "monkey with" করে বিদ্যমান কোডের সাথে। বিদ্যমান কোডটি প্রায়শই वो কোড যা আপনার ডায়রেক্ট এক্সেস নেই, যেমন জেম বা রুবি স্ট্যান্ডার্ড লাইব্রেরি থেকে কোড। প্যাচগুলো সাধারণত মূল কোডের বিহেভিয়ার পরিবর্তন করার জন্য ডিজাইন করা হয় বাগ ফিক্স করতে, পারফরম্যান্স উন্নত করতে, ইত্যাদি।

সবচেয়ে অসফিস্টিকেটেড মনকিপ্যাচ রুবি ক্লাসগুলো রিওপেন করে এবং মেথড যোগ বা ওভাররাইড করে বিহেভিয়ার মডিফাই করে।

এই রিওপেনিং ধারণা রুবির অবজেক্ট মডেলের কোর। Java-এ ক্লাস শুধু একবার ডিফাইন করা যায়, কিন্তু রুবি ক্লাস (এবং মডিউলও) একাধিকবার ডিফাইন করা যায়। যখন আমরা একই ক্লাস দ্বিতীয়, তৃতীয়, চতুর্থ বার ডিফাইন করি, আমরা বলি আমরা এটি রিওপেন করছি। আমরা যেকোনো নতুন মেথড ডিফাইন করি সেগুলো বিদ্যমান ক্লাস ডেফিনিশনে যোগ হয়ে যায় এবং সেই ক্লাসের ইনস্ট্যানসে কল করা যায়।

এই সংক্ষিপ্ত উদাহরণ ক্লাস রিওপেনিং কনসেপ্ট ইলাস্ট্রেট করে:

class Sounds
  def honk
    "Honk!"
  end
end

class Sounds
  def squeak
    "Squeak!"
  end
end

sounds = Sounds.new
sounds.honk # => "Honk!"
sounds.squeak # => "Squeak!"

লক্ষ্য করুন #honk এবং #squeak উভয় মেথডই Sounds ক্লাসে উপলব্ধ রিওপেনিংর ম্যাজিক দিয়ে।

সহজ কথায়, মনকিপ্যাচিং হলো থার্ড-পার্টি কোডে ক্লাস রিওপেন করার কাজ।

কি মনকিপ্যাচিং ডেঞ্জারাস?

যদি আগের বাক্যটি আপনাকে ভয় পায়, সেটি সম্ভবত ভালো জিনিস। মনকিপ্যাচিং, বিশেষ করে وقتی Безachtet ভাবে করা হয়, রিয়েল কওয়াস সৃষ্টি করতে পারে।

এক মুহূর্ত চিন্তা করুন কী হবে যদি আমরা Array#<< রিডিফাইন করি:

class Array
  def <<(*args)
    # do nothing 😈
  end
end

এই চার লাইন কোড দিয়ে, পুরো প্রোগ্রামের সকল অ্যারে ইনস্ট্যান্স এখন ব্রোকেন।

আরও, #<<-এর মূল ইমপ্লিমেন্টেশন হারিয়ে গেছে। রুবি প্রসেস রিস্টার্ট ছাড়া সেটি ফিরে পাওয়ার কোনো উপায় নেই।

যখন মনকিপ্যাচিং ভয়ানকভাবে ভুল হয়

২০১১ সালে, আমি একটি বিখ্যাত সোশ্যাল নেটওয়ার্কিং কোম্পানিতে কাজ করতাম। সেখানে, কোডবেস ছিল একটি বড় রেইলস মনোলিথ রুবি ১.৮.৭-এ রান হচ্ছিল। সैकड़ো ইঞ্জিনিয়ার প্রতিদিন কোডবেসে কন্ট্রিবিউট করত, এবং ডেভেলপমেন্ট পেস খুব দ্রুত।

এক সময়, আমার টিম সিদ্ধান্ত নিল String#% মনকিপ্যাচ করতে ইন্টারন্যাশনালাইজেশনের জন্য প্লুরাল লেখা সহজ করতে। আমাদের প্যাচ কী করতে পারতো তার একটি উদাহরণ:

replacements = { horse_count: 3, horses: { one: "is 1 horse", other: "are %{horse_count} horses" } }
# "there are 3 horses in the barn"
"there %{horse_count:horses} in the barn" % replacements

আমরা প্যাচটি লিখে শেষে প্রোডাকশনে ডিপ্লয় করলাম... শুধু খুঁজে পেলাম এটি কাজ করছে না। আমাদের ইউজাররা লিটারাল %{...} ক্যারেক্টার দেখছিল সুন্দর প্লুরালাইজড টেক্সটের বদলে। এটা মেনে যেতো না। প্যাচটি আমার ল্যাপটপের ডেভেলপমেন্ট এনভায়রনমেন্টে পারফেক্টলি কাজ করেছিল। প্রোডাকশনে কেন কাজ করছে না?

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

কয়েক দিন মাথা ঘামালে, একটি কো-ওয়ার্কার একটি রেইলস ইнициালাইজার ট্র্যাক ডাউন করতে পারলেন যেটি String#%-এর অন্য একটি ইমপ্লিমেন্টেশন যোগ করেছিল যেটি আমাদের কেউ দেখিনি। জটিল করতে, এই পুরনো ইমপ্লিমেন্টেশনটিও একটি বাগ ধারণ করত, তাই প্রোডাকশন কনসোলে আমরা যা দেখলাম সেটি রুবির অফিসিয়াল ডকুমেন্টেশন থেকে আলাদা।

গল্প এখানেই শেষ নয়। পুরনো মনকিপ্যাচ ট্র্যাক ডাউন করার ক्रमে, আমরা আরও কমপক্ষে তিনটি খুঁজে পেলাম, সকল একই মেথড প্যাচ করছিল। আমরা একে অপরকে হতভय দৃষ্টিতে তাকালাম। এটি কীভাবে কাজ করতো??

আমরা অবশেষে এটিকে রেইলসের ইগার লোডিং-র ওপর চাকলাম দিলাম। ডেভেলপমেন্টে, রেইলস রুবি ফাইলগুলো লেজি লোড করে, অর্থাৎ শুধু যখন require করা হয় তখন লোড করে। প্রোডাকশনে, যাইহোক, রেইলস অ্যাপের সকল রুবি ফাইল ইনিশিয়ালাইজেশনে লোড করে। এটি মনকিপ্যাচিং-এ বড় একটি মन्कি রেঞ্চ ফেলে দেয়।

ক্লাস রিওপেন করার ফলাফল

এই কেসে, প্রতিটি মনকিপ্যাচ String ক্লাস রিওপেন করেছিল এবং কার্যকরভাবে #% মেথডের বিদ্যমান ভার্সনকে অন্য একটি দিয়ে রিপ্লেস করেছিল। এই পদ্ধতির বেশ কয়েকটি বড় পিটফল আছে:

  • সর্বশেষ অ্যাপ্লাই করা প্যাচ "জিতে", অর্থাৎ বিহেভিয়ার লোড অর্ডার ডিপেন্ডেন্ট
  • মূল ইমপ্লিমেন্টেশন অ্যাক্সেস করার কোনো উপায় নেই
  • প্যাচগুলো প্রায় কোনো অডিট ট্রেল ছাড়া ছেড়ে যায়, যা পরে সেগুলো খুঁজে বের করা খুব কঠিন করে দেয়

আশ্চর্য নয়, আমরা এই সবই মোকাবিলা করেছি।

প্রথমে, আমরা জানতাম না অন্য মনকিপ্যাচগুলো এক্সিস্ট করে। উইনিং মেথডের বাগের কারণে, লাগতো মূল ইমপ্লিমেন্টেশন ব্রোকেন। অন্য কম্পিটিং প্যাচগুলো আবিষ্কার করার পর, কোনটি জিতলো সেটি বোঝা অসম্ভব ছিল profuse puts স্টেটমেন্ট ছাড়া।

শেষে, যখন আমরা ডেভেলপমেন্টে কোন মেথড জিতলো সেটি আবিষ্কার করলাম, প্রোডাকশনে একটি ভিন্নটি জিতলো। এটি প্রোগ্রামেটিক্যালি বলা কঠিন ছিল কোন প্যাচটি সর্বশেষ অ্যাপ্লাই হয়েছে যেহেতু রুবি ১.৮-এ আজকের চমৎকার Method#source_location মেথডটি ছিল না।

আমি কমপক্ষে এক সপ্তাহ ব্যয় করলাম কী হচ্ছে সেটি বের করার চেষ্টায়, সময় যা আমি এসেনশিয়ালি ওয়েস্ট করলাম একটি সম্পূর্ণ এয়েডেবল সমস্যার পেছনে।

অবশেষে, আমরা সিদ্ধান্ত নিলাম LocalizedString র‍্যাপার ক্লাস আনার সাথে একটি #% মেথড। আমাদের String মনকিপ্যাচটি তখন শুধু হলো:

class String
  def localize
    LocalizedString.new(self)
  end
end

যখন মনকিপ্যাচিং ফেইল করে

আমার অভিজ্ঞত অনুযায়ী, মনকিপ্যাচ প্রায়শই দুটি কারণেই ফেইল করে:

  • প্যাচটি নিজে ব্রোকেন। উপরে উল্লিখিত কোডবেসে, একই মেথডের বেশ কয়েকটি কম্পিটিং ইমপ্লিমেন্টেশন ছিল, তবে "জিতা" মেথডটি কাজ করতো না।
  • অ্যাসাম্পশনগুলি ইনভ্যালিড। হোস্ট কোড আপডেট হয়েছে এবং প্যাচটি লেখা অনুযায়ী আর অ্যাপ্লাই হয় না।

দ্বিতীয় বুলেট পয়েন্টটি আরও ডিটেইলসে দেখি।

সেরা পরিকল্পনাও...

মনকিপ্যাচিং প্রায়শই একই কারণেই ফেইল করে যার জন্য আপনি প্রথমে এটিকে হাত নিতে — কারণ আপনার হোস্ট কোডে ডায়রেক্ট এক্সেস নেই। ঠিক সেই কারণে, মূল কোড আপনার নিচে থেকে পরিবর্তন হতে পারে।

একটি উদাহরণ বিবেচনা করুন একটি জেমে যা আপনার অ্যাপ ডিপেন্ড করে:

class Sale
  def initialize(amount, discount_pct, tax_rate = nil)
    @amount = amount
    @discount_pct = discount_pct
    @tax_rate = tax_rate
  end
  def total
    discounted_amount + sales_tax
  end
  private
  def discounted_amount
    @amount * (1 - @discount_pct)
  end
  def sales_tax
    if @tax_rate
      discounted_amount * @tax_rate
    else
      0
    end
  end
end

রুকুন, এটা ঠিক নয়। সেলস ট্যাক্স ফুল অমাউন্টের ওপর অ্যাপ্লাই হওয়া উচিত, ডিসকাউন্টেড অমাউন্টের ওপর নয়। আপনি প্রজেক্টে একটি পুল রিকোয়েস্ট সাবমিট করলাম। মেইনটেইনার মার্জ করার انتظار করে, আপনি আপনার অ্যাপে এই মনকিপ্যাচ যোগ করলাম:

class Sale
  private
  def sales_tax
    if @tax_rate
      @amount * @tax_rate
    else
      0
    end
  end
end

এটি পারফেক্টলি কাজ করল। আপনি চেক-ইন করলাম এবং ভুলে গেলাম।

খুব দিন সব ঠিকঠাক চলল। একদিন ফাইন্যান্স টিম আপনাকে ইমেইল করল কেন কোম্পানিটি এক মাস ধরে সেলস ট্যাক্স কালেক্ট করছে না।

বিব্রান্ত, আপনি ইস্যুতে খনন শুরু করলাম এবং সর্বশেষ লক্ষ্য করলাম আপনার একজন কো-ওয়ার্কার সম্প্রতি Sale ক্লাস ধারণকারী জেমটি আপডেট করলেন। আপডেটেড কোড:

class Sale
  def initialize(amount, discount_pct, sales_tax_rate = nil)
    @amount = amount
    @discount_pct = discount_pct
    @sales_tax_rate = sales_tax_rate
  end
  def total
    discounted_amount + sales_tax
  end
  private
  def discounted_amount
    @amount * (1 - @discount_pct)
  end
  def sales_tax
    if @sales_tax_rate
      discounted_amount * @sales_tax_rate
    else
      0
    end
  end
end

দেখুন, প্রজেক্ট মেইনটেইনাররা একটি @tax_rate ইনস্ট্যান্স ভেরিয়েবল রিনেম করল @sales_tax_rate হিসেবে। মনকিপ্যাচটি পুরনো @tax_rate ভেরিয়েবলের ভ্যালু চেক করত, যা সবসময় nil। কেউ লক্ষ্য করল না কারণ কোনো এরর কখনো রেইজ হয়নি। অ্যাপ্লিকেশন যেন কিছুই হয়নি সেভাবে চলতে থাকল।

কেন মনকিপ্যাচ করবেন?

এই উদাহরণগুলো দেখলে লাগতে পারে মনকিপ্যাচিং শুধু হেডেইচের লायक নয়। তবুও আমরা কেন করি? আমার মতামতে, তিনটি মেইন ইউজ-কেস আছে:

  • ব্রোকেন বা ইনকমপ্লিট থার্ড-পার্টি কোড ফিক্স করতে
  • ডেভেলপমেন্টে দ্রুত একটি চেঞ্জ বা একাধিক চেঞ্জ টেস্ট করতে
  • বিদ্যমান ফাংশনালিটি ইনস্ট্রুমেন্টেশন বা অ্যানোটেশন কোড দিয়ে র‍্যাপ করতে

কিছু কেসে, থার্ড-পার্টি কোডে বাগ বা পারফরম্যান্স ইস্যু সমাধান করার একমাত্র ভায়বল পথ হলো মনকিপ্যাচ অ্যাপ্লাই করা।

তবে বড় শক্তির সাথে বড় দায়িত্ব আসে।

দায়িত্ববোধ সহ মনকিপ্যাচিং

আমি মনকিপ্যাচিং কনভারসেশনকে দায়িত্ববোধের চারপাশে ফ্রেম করা পছন্দ করি না সেটি ভালো বা খারাপ। নিশ্চিত, মনকিপ্যাচিং খারাপভাবে করলে কওয়াস সৃষ্টি করতে পারে। যাইহোক, কিছু যত্ন ও তত্পরতাসহ করলে, পরিস্থিতি वारেন্ট করলে এটিকে এড়াতে কোনো কারণ নেই।

আমি যে নিয়মগুলো ফলো করার চেষ্টা করি:

  1. প্যাচটি একটি স্পষ্ট নামের মডিউলে র‍্যাপ করুন এবং Module#prepend ব্যবহার করে অ্যাপ্লাই করুন
  2. নিশ্চিত করুন আপনি সঠিক জিনিস প্যাচ করছেন
  3. প্যাচের সারফেস এরিয়া লিমিট করুন
  4. নিজের জন্য এস্কেপ হ্যাচ রাখুন
  5. ওভার-কমিউনিকেট করুন

এই আর্টিকেলের বাকি অংশে, আমরা এই নিয়মগুলো ব্যবহার করে রেইলস DateTimeSelector-এর জন্য একটি মনকিপ্যাচ লিখব যাতে এটি অপশনালি ডিসকার্ডেড ফিল্ড রেন্ডার করা বন্ধ করতে পারে। এটি একটি চেঞ্জ যা আমি כמה বছর আগে রেইলসে করার চেষ্টা করেছিলাম। ডিটেইলস এখানে পাবেন।

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

চলুন শুরু করি!

Module#prepend ব্যবহার করুন

আমার সামনে আসা কোডবেসে, String#%-এর সকল ইমপ্লিমেন্টেশন String ক্লাস রিওপেন করে অ্যাপ্লাই করা হয়েছিল। আমি যা ড্রব্যাকস বলেছিলাম সেগুলোর একটি অগমেন্টেড লিস্ট:

  • এরর হোস্ট ক্লাস বা মডিউল থেকে উদ্ভূত হয়েছে যেন দেখাচ্ছে, প্যাচ কোড থেকে নয়
  • প্যাচে আপনি যে মেথড ডিফাইন করবেন সেগুলো একই নামের বিদ্যমান মেথড রিপ্লেস করে, অর্থাৎ, মূল ইমপ্লিমেন্টেশন ইনভোক করার কোনো উপায় নেই
  • কোনো উপায় নেই জানার কোন প্যাচগুলো অ্যাপ্লাই হয়েছে এবং তাই কোন মেথড "জিতল"
  • প্যাচগুলো প্রায় কোনো অডিট ট্রেল ছাড়া ছেড়ে যায়, যা সেগুলো খুঁজে বের করা খুব কঠিন করে দেয়

বদলে, আপনার প্যাচকে একটি মডিউলে র‍্যাপ করা এবং Module#prepend দিয়ে অ্যাপ্লাই করা অনেক বেটার। এটি আপনাকে মূল ইমপ্লিমেন্টেশন কল করার স্বাধীনতা দেয়, এবং একটি কুইক Module#ancestors কল প্যাচকে ইনহেরিট্যান্স হায়ারার্কিতে দেখাবে তাই কোনো সমস্যা হলে খুঁজে বের করা সহজ।

শেষে, একটি সিম্পল prepend স্টেটমেন্ট কমেন্ট আউট করা সহজ যদি কোনো কারণে প্যাচ ডিজএবল করতে হয়।

আমাদের রেইলস মনকিপ্যাচের জন্য একটি মডিউলের শুরু:

module RenderDiscardedMonkeypatch
end

ActionView::Helpers::DateTimeSelector.prepend(
  RenderDiscardedMonkeypatch
)

সঠিক জিনিস প্যাচ করুন

এই আর্টিকেল থেকে আপনি একটা জিনিসই নেন, সেটি হোক: কোনো মনকিপ্যাচ অ্যাপ্লাই না করুন যদি না আপনি জানেন আপনি সঠিক কোড প্যাচ করছেন। অধিকাংশ কেসে, প্রোগ্রামেটিক্যালি আপনার অ্যাসাম্পশন এখনও হোল্ড করছে কিনা ভেরিফাই করা সম্ভব (এটি রুবি畢竟)। একটি চেকলিস্ট:

  1. নিশ্চিত করুন আপনি যে ক্লাস বা মডিউল প্যাচ করার চেষ্টা করছেন সেটি এক্সিস্ট করে
  2. নিশ্চিত করুন মেথডগুলো এক্সিস্ট করে এবং সঠিক আরিটি আছে
  3. যদি কোডটি একটি জেমে থাকে, জেমের ভার্সন চেক করুন
  4. যদি অ্যাসাম্পশন হোল্ড না করে, হেল্পফুল এরর মেসেজ দিয়ে বেইল আউট করুন

সশুরু থেকে, আমাদের প্যাচ কোড একটি খুব গুরুত্বপূর্ণ অ্যাসাম্পশন করেছে। এটি অ্যাসাম করছে ActionView::Helpers::DateTimeSelector নামক একটি কনস্ট্যান্ট এক্সিস্ট করে এবং সেটি একটি ক্লাস বা মডিউল।

ক্লাস/মডিউল চেক

চলুন নিশ্চিত করি সেই কনস্ট্যান্ট এক্সিস্ট করে প্যাচ করার চেষ্টা করার আগে:

module RenderDiscardedMonkeypatch
  def self.apply_patch
    const = begin
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
    if const
      const.prepend(RenderDiscardedMonkeypatch)
    end
  end
end
RenderDiscardedMonkeypatch.apply_patch

ভালো, কিন্তু এখন আমরা একটি লোকাল ভেরিয়েবল (const) গ্লোবাল স্কোপে লিক করেছি। এটিকে ফিক্স করি:

module RenderDiscardedMonkeypatch
  def self.apply_patch
    const = begin
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
    if const
      const.prepend(self)
    end
  end
end
RenderDiscardedMonkeypatch.apply_patch

মেথড চেক

এখন, প্যাচ করা build_hidden মেথড আনার পালা। এছাড়া একটি চেক যোগ করি যাতে নিশ্চিত হয় সেটি এক্সিস্ট করে এবং সঠিক সংখ্যক আর্গুমেন্ট গ্রহণ করে (অর্থাৎ সঠিক আরিটি)। যদি সেই অ্যাসাম্পশন হোল্ড না করে, কিছু তো ভুল আছে:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
      if const && mtd && mtd.arity == 2
        const.prepend(self)
      end
    end
    private
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
  end
  def build_hidden(type, value)
    ''
  end
end
RenderDiscardedMonkeypatch.apply_patch

জেম ভার্সন চেক

শেষে, চলুন চেক করি আমরা রেইলসের সঠিক ভার্সন ব্যবহার করছি কিনা। যদি রেইলস আপগ্রেড হয়, তবে প্যাচটিও আপডেট করতে হয়তে পারে (বা পুরো বাদ দিতে হয়তে পারে)।

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
      if const && mtd && mtd.arity == 2 && rails_version_ok?
        const.prepend(self)
      end
    end
    private
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
  def build_hidden(type, value)
    ''
  end
end
RenderDiscardedMonkeypatch.apply_patch

হেল্পফুলি বেইল আউট

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

এখানে আমরা আমাদের রেইলস প্যাচ কীভাবে মডিফাই করতে পারি:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching "\
              "ActionView's date_select helper. Please investigate."
      end
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since "\
             "ActionView's date_select helper was monkeypatched in "\
             "#{__FILE__}. Please reevaluate the patch."
      end
      const.prepend(self)
    end
    private
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
  def build_hidden(type, value)
    ''
  end
end
RenderDiscardedMonkeypatch.apply_patch

সারফেস এরিয়া লিমিট করুন

হয়তো মনে হয় মোনকিপ্যাচে হেল্পার মেথড ডিফাইন করা হার্মলেস। কিন্তু মনে রাখবেন Module#prepend দিয়ে ডিফাইন করা যেকোনো মেথড ইনহেরিট্যান্সের ম্যাজিক দিয়ে বিদ্যমান মেথডকে ওভাররাইড করবে। যদিও লাগতে পারে হোস্ট ক্লাস বা মডিউল একটি নির্দিষ্ট মেথড ডিফাইন করে না, কিন্তু নিশ্চিত হওয়া কঠিন। এই কারণেই, আমি শুধু সেসব মেথড ডিফাইন করার চেষ্টা করি যেগুলো আমি প্যাচ করতে চাই।

নোট করুন এটি সিঙ্গলটন ক্লাসে ডিফাইন করা মেথডগুলোর জন্যও প্রযোজ্য, অর্থাৎ class << self-এ ডিফাইন করা মেথড।

এখানে আমাদের রেইলস প্যাচ কীভাবে মডিফাই করতে পারি শুধু একটা #build_hidden মেথড রিপ্লেস করতে:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching"\
              "ActionView's date_select helper. Please investigate."
      end
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since"\
             "ActionView's date_selet helper was monkeypatched in "\
             "#{__FILE__}. Please reevaluate the patch."
      end
      const.prepend(InstanceMethods)
    end
    private
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
  module InstanceMethods
    def build_hidden(type, value)
      ''
    end
  end
end
RenderDiscardedMonkeypatch.apply_patch

নিজের জন্য এস্কেপ হ্যাচ রাখুন

যখন সম্ভব, আমি আমার মনকিপ্যাচের ফাংশনালিটি অপ্ট-ইন রাখতে পছন্দ করি। এটি শুধু তখনই অপশন যখন আপনার প্যাচ করা কোড কোথায় ইনভোক হচ্ছে সেটির ওপর কন্ট্রোল থাকে। আমাদের রেইলস প্যাচের কেসে, এটি DateTimeSelector-এ @options হ্যাশ দিয়ে সম্ভব:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching"\
              "ActionView's date_select helper. Please investigate."
      end
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since"\
             "ActionView's date_selet helper was monkeypatched in "\
             "#{__FILE__}. Please reevaluate the patch."
      end
      const.prepend(InstanceMethods)
    end
    private
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
  module InstanceMethods
    def build_hidden(type, value)
      if @options.fetch(:render_discarded, true)
        super
      else
        ''
      end
    end
  end
end
RenderDiscardedMonkeypatch.apply_patch

বढ़িয়া! এখন কলাররা অপ্ট-ইন করতে পারেন নতুন অপশন দিয়ে date_select হেল্পার কল করে। অন্য কোনো কোডপাথ অ্যাফেক্টেড নয়:

date_select(@user, :date_of_birth, { order: [:month, :day], render_discarded: false })

ওভার-কমিউনিকেট করুন

আমার আপনার জন্য শেষ পরামর্শ হয়তো সবচেয়ে গুরুত্বপূর্ণ — আপনার প্যাচ কী করে এবং কখন সেটি রি-এগজামিন করার সময় সেটি কমিউনিকেট করা। মনকিপ্যাচের সাথে আপনার লক্ষ্য সবসময় হওয়া উচিত পুরোপুরি প্যাচটি রিমুভ করা। সেই লক্ষ্যে, একটি দায়িত্ববোধী মনকিপ্যাচে কমেন্ট থাকে যা:

  • বর্ণনা করে প্যাচটি কী করে
  • ব্যাখ্যা করে কেন প্যাচটি প্রয়োজনীয়
  • অ্যাসাম্পশনগুলো আউটলাইন করে যা প্যাচটি করে
  • ভবিষ্যতে একটি তারিখ স্পেসিফাই করে যখন আপনার টিমকে অল্টারনেটিভ সলিউশন রিকনসিডার করতে হবে, যেমন আপডেটেড জেম পুল ইন করা
  • রিলেভেন্ট পুল রিকোয়েস্ট, ব্লগ পোস্ট, StackOverflow আন্সারের লিংক ইনক্লুড করে

আপনি এমনকি একটি প্রিডিটারমাইনড ডেটে ওয়ার্নিং প্রিন্ট করতে পারেন বা একটি টেস্ট ফেইল করতে পারেন যাতে টিমকে প্যাচের অ্যাসাম্পশন রিকনফার্ম করতে এবং বিবেচনা করতে উত্সাহিত করা যায় সেটি এখনও প্রয়োজনীয় কিনা।

এখানে আমাদের রেইলস date_select প্যাচের ফাইনাল ভার্সন, কমেন্ট এবং ডেট চেকসহ:

# ActionView's date_select helper provides the option to "discard" certain
# fields. Discarded fields are (confusingly) still rendered to the page
# using hidden inputs, i.e. . This patch adds an
# additional option to the date_select helper that allows the caller to
# skip rendering the chosen fields altogether. For example, to render all
# but the year field, you might have this in one of your views:
#
# date_select(:date_of_birth, order: [:month, :day])
#
# or, equivalently:
#
# date_select(:date_of_birth, discard_year: true)
#
# To avoid rendering the year field altogether, set :render_discarded to
# false:
#
# date_select(:date_of_birth, discard_year: true, render_discarded: false)
#
# This patch assumes the #build_hidden method exists on
# ActionView::Helpers::DateTimeSelector and accepts two arguments.
#
module RenderDiscardedMonkeypatch
  class << self
    EXPIRATION_DATE = Date.new(2021, 8, 15)
    def apply_patch
      if Date.today > EXPIRATION_DATE
        puts "WARNING: Please re-evaluate whether or not the ActionView "\
             "date_select patch present in #{__FILE__} is still necessary."
      end
      const = find_const
      mtd = find_method(const)
      # make sure the class we want to patch exists;
      # make sure the #build_hidden method exists and accepts exactly
      # two arguments
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching "\
              "ActionView's date_select helper. Please investigate."
      end
      # if rails has been upgraded, make sure this patch is still
      # necessary
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since "\
             "ActionView's date_select helper was monkeypatched in "\
             "#{__FILE__}. Please re-evaluate the patch."
      end
      # actually apply the patch
      const.prepend(InstanceMethods)
    end
    private
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
      # return nil if the constant doesn't exist
    end
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
      # return nil if the method doesn't exist
    end
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
  module InstanceMethods
    # :render_discarded is an additional option you can pass to the
    # date_select helper in your views. Use it to avoid rendering
    # "discarded" fields, i.e. fields marked as discarded or simply
    # not included in date_select's :order array. For example,
    # specifying order: [:day, :month] will cause the helper to
    # "discard" the :year field. Discarding a field renders it as a
    # hidden input. Set :render_discarded to false to avoid rendering
    # it altogether.
    def build_hidden(type, value)
      if @options.fetch(:render_discarded, true)
        super
      else
        ''
      end
    end
  end
end
RenderDiscardedMonkeypatch.apply_patch

উপসংহার

আমি পুরোপুরি বুঝতে পারছি উপরের কিছু সাজেশন হয়তো ওভারকিল মনে হয়। আমাদের রেইলস প্যাচে প্রকৃত প্যাচ কোডের চেয়ে অনেক বেশি ডিফেন্সিভ ভেরিফিকেশন কোড আছে!

সেটা আপনার ব্রডসোর্ডের একটি শিথ মনে করুন। এটি কাটা এড়াতে অনেক সহজ যদি সেটি একটি প্রোটেক্টিভ লেয়ারের মধ্যে आवৃত থাকে।

যে বিষয়টি আসলে মনে রাখা দরকার, সেটি হলো আমি দায়িত্ববোধী মনকিপ্যাচ প্রোডাকশনে ডিপ্লয় করতে আত্মবিশ্বস্ত মনে করি। দায়িত্বহীনগুলো শুধু টাইম বোমা যা আপনাকে বা আপনার কোম্পানির টাইম, মানি, এবং ডেভেলপার হেলথ খরচ করবে।

P.S. Ruby Magic পোস্টগুলো প্রেস থেকে বের होतेই পেতে চাইলে, আমাদের Ruby Magic নিউজলেটার সাবস্ক্রাইব করুন এবং কোনো পোস্ট মিস করবেন না!

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

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

  3. রুবি দিয়ে ইন্সারশন সর্টিং দক্ষতা অর্জন: ধাপে ধাপে গাইড

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