Long Method smell: চিনুন ও Extract Method দিয়ে সারান

সব কাজ করা একটি function পড়া, test ও বদলানো কঠিন। Long Method চিনে Extract Method দিয়ে ভাগ করুন, পরীক্ষিত আগে-পরের code-সহ।

একটি লম্বা code ব্লক ছোট নামওয়ালা অংশে ভাগ করা

Code smell হলো এমন প্যাটার্ন যা design-এর সমস্যার ইঙ্গিত দেয়। এটি bug নয়: code চলে। কিন্তু code পড়া, test ও বদলানো কঠিন হয়। Long Method সবচেয়ে চেনা smell, আর এটি "Bloaters"-এর প্রথম, মানে যা সামলানোর চেয়ে বড় হয়ে গেছে। নিচের code ও output প্রকাশের আগে Python 3-এ চালানো।

কীভাবে চিনবেন

  • function পড়তে scroll করতে হয়।
  • ভেতরে অংশ চিহ্নিত করতে # calculate tax-এর মতো comment লিখতে হয়।
  • এক body-তে validation, গণনা ও লেখা সাজানো হয়।
  • একটি নিয়মের test-এ বাকি সবকিছুও সাজাতে হয়।
  • variable ব্যবহারের জায়গা থেকে অনেক দূরে ঘোষণা করা।

কোনো জাদুকরী লাইন সংখ্যা নেই। ভালো পরীক্ষা: "এবং" ছাড়া এক ছোট বাক্যে বলতে পারেন function-টি কী করে?

সমাধান: Extract Method

পরিষ্কার উদ্দেশ্যের একটি অংশ নিন, উদ্দেশ্য বলা নামে একটি function-এ সরান এবং সেটি ডাকুন। মূল function রূপরেখার মতো পড়া না যাওয়া পর্যন্ত বারবার করুন। নিচে receipt_before সব করে, আর receipt_after পাঁচটি ছোট অংশে গড়া।

# ---- BEFORE: one long function does everything ----
def receipt_before(items, country):
    if not items:
        raise ValueError("no items")
    for name, price, qty in items:
        if price < 0 or qty < 1:
            raise ValueError(f"bad line: {name}")
    subtotal = 0
    for name, price, qty in items:
        subtotal += price * qty
    rate = 0.15 if country == "BD" else 0.2
    tax = round(subtotal * rate, 2)
    total = round(subtotal + tax, 2)
    lines = []
    for name, price, qty in items:
        lines.append(f"{name:<10} {qty} x {price:>6.2f}")
    lines.append(f"{'subtotal':<10} {subtotal:>10.2f}")
    lines.append(f"{'tax':<10} {tax:>10.2f}")
    lines.append(f"{'total':<10} {total:>10.2f}")
    return "\n".join(lines)

# ---- AFTER: small functions, each with one job and a name ----
def validate(items):
    if not items:
        raise ValueError("no items")
    for name, price, qty in items:
        if price < 0 or qty < 1:
            raise ValueError(f"bad line: {name}")

def subtotal_of(items):
    return sum(price * qty for _, price, qty in items)

def tax_rate(country):
    return 0.15 if country == "BD" else 0.2

def format_lines(items, subtotal, tax, total):
    lines = [f"{name:<10} {qty} x {price:>6.2f}" for name, price, qty in items]
    lines += [f"{'subtotal':<10} {subtotal:>10.2f}", f"{'tax':<10} {tax:>10.2f}", f"{'total':<10} {total:>10.2f}"]
    return "\n".join(lines)

def receipt_after(items, country):
    validate(items)
    subtotal = subtotal_of(items)
    tax = round(subtotal * tax_rate(country), 2)
    return format_lines(items, subtotal, tax, round(subtotal + tax, 2))

নতুন receipt_after ছোট এবং ওপর থেকে নিচে পড়া যায়: যাচাই, যোগ, কর হিসাব, সাজানো। প্রতিটি helper এখন আলাদা test, পুনরায় ব্যবহার, বা অন্যদের না ছুঁয়ে বদলানো যায়।

আচরণ বদলায়নি প্রমাণ করুন

refactoring গঠন বদলায়, ফল কখনও নয়। কাজের সময় পুরনো function রাখুন এবং তুলনা করুন:

items = [("notebook", 3.5, 4), ("pen", 0.75, 10)]
print(receipt_after(items, "BD"))
assert receipt_before(items, "BD") == receipt_after(items, "BD")
assert receipt_before(items, "DE") == receipt_after(items, "DE")
for bad in ([], [("x", -1, 1)]):
    for f in (receipt_before, receipt_after):
        try: f(bad, "BD"); raise SystemExit("should have failed")
        except ValueError: pass
print("before and after behave the same")
notebook   4 x   3.50
pen        10 x   0.75
subtotal        21.50
tax              3.23
total           24.73
before and after behave the same

assertion দুটি দেশের জন্য পুরনো ও নতুন output মেলায় এবং দেখে দুটি সংস্করণই খালি ও ঋণাত্মক input বাদ দেয়। test পাস করলে পুরনো সংস্করণ মুছতে পারেন।

নিরাপদে refactor করবেন যেভাবে

  1. আগে একটি test লিখুন, বা বর্তমান output-কে প্রত্যাশিত ধরে রাখুন।
  2. একবারে একটি অংশ extract করুন এবং প্রতিটি ধাপের পর test চালান।
  3. নতুন function-কে ধাপ নয়, উদ্দেশ্য বর্ণনার নাম দিন (if_country_bd নয়, tax_rate)।
  4. যা লাগে তা পাঠান, যা তৈরি হয় তা ফেরান। লুকানো ভাগ করা অবস্থা এড়ান।
  5. মূল function ছোট রূপরেখার মতো পড়া গেলে থামুন।

কখন ছেড়ে দেবেন

  • একই ধরনের ধাপের লম্বা তালিকা (configuration table বা সহজ statement-এর ক্রম) ছড়ানোর চেয়ে এক জায়গায় প্রায়ই পরিষ্কার।
  • শুধু লাইন গোনা কমাতে এক লাইনের অংশকে function করবেন না। নাম কিছু না যোগ করলে এড়ান।

সাধারণ ভুল

  • test ছাড়া extract করে ভুল করে আচরণ বদলে ফেলা।
  • খারাপ নাম (do_part2) যা সমাধানের বদলে সমস্যা লুকায়।
  • নতুন function-এ আটটি argument পাঠানো। সেটি পরের smell, Long Parameter List।
  • code আরেকটি সমান লম্বা function-এ সরানো।

মূল কথা: অংশ চিহ্নিত করতে comment লাগলে ওই অংশগুলো function হতে চায়। একটি একটি করে extract করুন, test চালু রাখুন, আর প্রতিটি অংশকে উদ্দেশ্য দিয়ে নাম দিন।


Code smell ও refactoring সিরিজ, Bloaters: 1 নম্বর (মোট 5)।

আরও পড়ুন

Python-এ SOLID নীতি ও design pattern: test করা যায় এমন code-সহ

SOLID পাঁচটি অভ্যাস যা code বদলানো ও test করা সহজ করে। ছোট একটি Python checkout উদাহরণে দেখুন, আর কখন pattern বেশি হয়ে যায়।

প্রোগ্রামিং•৩ মিনিটে পড়া