পাঠ ৪৯ · ৫৮-এর মধ্যে · মডিউল ১১
Home / Courses / Software Engineering Principles & Git / কোড স্মেল

কোড স্মেল

Code smells
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • কোড স্মেল কী এবং এটি কীভাবে M4-M5-এর ডিজাইন নীতির সাথে সম্পর্কিত
  • পাঁচটি সাধারণ, নাম-করা কোড স্মেল ও তাদের বাস্তব লক্ষণ
  • একটি হিউরিস্টিক লং-মেথড ডিটেক্টর কীভাবে বানাতে হয়
  • কোনো external লাইব্রেরি ছাড়া একটি সরল লাইন-বাই-লাইন সিমিলারিটি-ভিত্তিক ডুপ্লিকেট-কোড ডিটেক্টর

১ · কোড স্মেল কী — পৃষ্ঠতলের লক্ষণ, গভীর সমস্যার ইঙ্গিত

কোড স্মেলCode Smellএকটি সারফেস-লেভেল ইঙ্গিত যা একটি গভীর ডিজাইন সমস্যার সম্ভাবনা দেখায় — প্রমাণ নয়, একটি সংকেত। হলো কোডে একটি সারফেস-লেভেল ইঙ্গিত যা একটি গভীর ডিজাইন সমস্যার সম্ভাবনা দেখায় — লক্ষ্য করুন "সম্ভাবনা" শব্দটি: একটি স্মেল প্রমাণ করে না কিছু ভুল, শুধু ইঙ্গিত দেয় "এখানে একটু ভালো করে দেখা দরকার"। একটি কোড স্মেল আসলে M4-M5-এ শেখা বিমূর্ত নীতিগুলো (SOLID, coupling/cohesion, DRY) বাস্তবে কেমন দেখায় তার একটি ডিটেক্টেবল, কনক্রিট রূপ — অর্থাৎ, ঠিক কোন নীতি লঙ্ঘন হয়েছে তা আনুষ্ঠানিকভাবে নির্ণয় করার আগেই, এই লক্ষণগুলো খালি চোখে (বা কোডে) ধরা যায়।

২ · পাঁচটি সাধারণ কোড স্মেল

Long Method
একটি ফাংশন/মেথড অনেক বেশি কাজ করছে — M4/L16-এর SRP লঙ্ঘনের সরাসরি লক্ষণ।
Large Class / "God Class"
একটি ক্লাসের অনেক বেশি দায়িত্ব — M4/L16-এর Employee গড-ক্লাস উদাহরণের সরাসরি পুনঃব্যবহার।
Duplicate Code
একই লজিক একাধিক জায়গায় কপি-পেস্ট করা — M4/L15-এর DRY লঙ্ঘনের সরাসরি লক্ষণ।
Long Parameter List
একটি ফাংশনের অনেক প্যারামিটার — প্রায়ই সম্পর্কিত প্যারামিটার একটি অবজেক্টে গ্রুপ করা উচিত (M5/L21-এর Builder-এর সম্ভাব্য সমাধান)।
Feature Envy
একটি মেথড অন্য ক্লাসের ডেটার প্রতি বেশি আগ্রহী — লজিকটি হয়তো সেই অন্য ক্লাসেই থাকা উচিত (M4/L14-এর cohesion লক্ষ্যের লঙ্ঘন)।
স্মেল থেকে নীতি পর্যন্ত — M4-M5-এর সাথে সংযোগ

প্রতিটি স্মেল আসলে কোনো না কোনো আগের পাঠের নীতির একটি "লক্ষণ" রূপ — Long Method দেখলে সন্দেহ করুন SRP লঙ্ঘন হয়েছে কিনা; Duplicate Code দেখলে সন্দেহ করুন DRY লঙ্ঘন হয়েছে কিনা। স্মেল শনাক্ত করা প্রথম, দ্রুত ধাপ — এরপর M11/L50-এর নির্দিষ্ট রিফ্যাক্টরিং টেকনিক দিয়ে সেই স্মেলের প্রকৃত কারণ ঠিক করা হয়।

স্মেল: Long Method নীতি লঙ্ঘন: SRP (M4/L16) ফিক্স: Extract Method স্মেল: Duplicate Code নীতি লঙ্ঘন: DRY (M4/L15) ফিক্স: শেয়ার্ড ফাংশনে একত্র করা
প্রতিটি কোড স্মেল একটি নির্দিষ্ট নীতি লঙ্ঘনের বাহ্যিক লক্ষণ — এবং প্রতিটি নীতি লঙ্ঘনের একটি নাম-করা, স্ট্যান্ডার্ড ফিক্স আছে (M11/L50)।

৩ · হিউরিস্টিক ডিটেক্টর বানানো

নিচের কোড সেলে দুটো হিউরিস্টিক ডিটেক্টর বানানো হয়েছে — detect_long_method, যা লাইন গুনে একটি থ্রেশহোল্ডের সাথে তুলনা করে, এবং detect_duplicate_code, যা কোনো external diff লাইব্রেরি ছাড়াই দুটো ফাংশনের লাইন-বাই-লাইন সিমিলারিটি রেশিও হিসাব করে। দুটোই একটি ইচ্ছাকৃতভাবে স্মেলি উদাহরণ এবং একটি ক্লিন উদাহরণের বিপরীতে চালিয়ে যাচাই করা হয়েছে — ডিটেক্টর দুটো যেন সঠিকভাবে স্মেলিটাকে ফ্ল্যাগ করে এবং ক্লিনটাকে পাস করে।

Python
def detect_long_method(function_lines, threshold=10):
    """লাইন গুনে থ্রেশহোল্ডের সাথে তুলনা করে -- একটি সরল, বাস্তব হিউরিস্টিক।"""
    line_count = len(function_lines)
    return {
        "line_count": line_count,
        "threshold": threshold,
        "is_long_method": line_count > threshold,
    }


def detect_duplicate_code(lines_a, lines_b, similarity_threshold=0.7):
    """কোনো external diff লাইব্রেরি ছাড়াই -- দুটো ফাংশনের লাইন-বাই-লাইন মিল-অনুপাত হিসাব করে।"""
    shorter_len = min(len(lines_a), len(lines_b))
    if shorter_len == 0:
        return {"similarity": 0.0, "is_duplicate": False}
    matching_lines = 0
    for i in range(shorter_len):
        if lines_a[i].strip() == lines_b[i].strip():
            matching_lines += 1
    similarity = matching_lines / shorter_len
    return {"similarity": round(similarity, 2), "is_duplicate": similarity >= similarity_threshold}


# ---- স্মেলি উদাহরণ: validation, calculation, formatting সব একসাথে একটি লম্বা ফাংশনে ----
smelly_process_order_lines = [
    "def process_order(order):",
    "    if order['customer'] is None:",
    "        raise ValueError('গ্রাহক নেই')",
    "    if not order['items']:",
    "        raise ValueError('আইটেম নেই')",
    "    if order['amount'] < 0:",
    "        raise ValueError('অ্যামাউন্ট ঋণাত্মক')",
    "    total = 0",
    "    for item in order['items']:",
    "        total += item['price'] * item['quantity']",
    "    if order['customer']['is_vip']:",
    "        total = total * 0.9",
    "    receipt = '--- রসিদ ---'",
    "    receipt += f\"গ্রাহক: {order['customer']['name']}\"",
    "    receipt += f\"মোট: {total}\"",
    "    print(receipt)",
    "    return total",
]

# ---- ক্লিন উদাহরণ: একটি কাজ, একটি সংক্ষিপ্ত ফাংশন ----
clean_calculate_total_lines = [
    "def calculate_total(items):",
    "    return sum(item['price'] * item['quantity'] for item in items)",
]

print("=== Long Method ডিটেক্টর ===")
result_smelly = detect_long_method(smelly_process_order_lines, threshold=10)
result_clean = detect_long_method(clean_calculate_total_lines, threshold=10)
print("স্মেলি process_order:", result_smelly)
print("ক্লিন calculate_total:", result_clean)
assert result_smelly["is_long_method"] is True
assert result_clean["is_long_method"] is False
print("চেক পাস: স্মেলি সঠিকভাবে ফ্ল্যাগড, ক্লিন সঠিকভাবে পাস")

# ---- ডুপ্লিকেট-কোড উদাহরণ: একই ডিসকাউন্ট লজিক দুই জায়গায় কপি-পেস্ট (M4/L15-এর DRY লঙ্ঘনের পুনঃব্যবহার) ----
discount_calc_checkout_lines = [
    "def apply_discount_checkout(price):",
    "    if price > 1000:",
    "        return price * 0.85",
    "    return price",
]
discount_calc_invoice_lines = [
    "def apply_discount_invoice(price):",
    "    if price > 1000:",
    "        return price * 0.85",
    "    return price",
]

# ---- সম্পূর্ণ অসম্পর্কিত, প্রকৃতপক্ষে ভিন্ন দুটো ফাংশন ----
unrelated_lines_a = clean_calculate_total_lines
unrelated_lines_b = [
    "def format_receipt(customer_name, total):",
    "    return f'ধন্যবাদ {customer_name}, মোট {total} টাকা'",
]

print("\n=== Duplicate Code ডিটেক্টর ===")
result_dup = detect_duplicate_code(discount_calc_checkout_lines, discount_calc_invoice_lines, similarity_threshold=0.7)
result_nodup = detect_duplicate_code(unrelated_lines_a, unrelated_lines_b, similarity_threshold=0.7)
print("checkout বনাম invoice ডিসকাউন্ট:", result_dup)
print("calculate_total বনাম format_receipt:", result_nodup)
assert result_dup["is_duplicate"] is True
assert result_nodup["is_duplicate"] is False
print("চেক পাস: কপি-পেস্ট করা জোড়া সঠিকভাবে ফ্ল্যাগড, অসম্পর্কিত জোড়া সঠিকভাবে পাস")

    
লক্ষ্য করুন detect_duplicate_code-এর সিমিলারিটি রেশিও শুধু একই পজিশনের লাইন তুলনা করে (index দিয়ে) — এটি একটি ইচ্ছাকৃতভাবে সরল হিউরিস্টিক, বাস্তব diff টুল (যেমন git diff, M7-M9-এ আসা) অনেক বেশি জটিল অ্যালগরিদম (লংগেস্ট কমন সাবসিকোয়েন্স) ব্যবহার করে লাইন শিফট/পুনর্বিন্যাসও ধরতে পারে। এখানে সরলতাই যথেষ্ট, কারণ আমাদের discount_calc জোড়ায় দুটো ফাংশনের লাইনগুলো ঠিক একই ক্রমে আছে — শুধু প্রথম লাইনের (ফাংশনের নাম) পার্থক্য বাদে।
মূল কথা · Key takeaway

কোড স্মেল একটি সতর্কীকরণ, রায় নয় — একটি স্মেল আপনাকে বলে "এখানে দেখো", ঠিক কী ভুল বা এটি ঠিক করা উচিত কিনা তা নয়। কিন্তু হিউরিস্টিক ডিটেক্টর (যেমন উপরের দুটো) এই "দেখো" সংকেতগুলো স্বয়ংক্রিয়ভাবে খুঁজে বের করতে পারে, যা M11/L52-এর কোড রিভিউয়ে মানুষের মনোযোগ substance-এ কেন্দ্রীভূত রাখতে সাহায্য করে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ একটি ১৫-লাইনের ফাংশন কি সবসময় Long Method স্মেলযুক্ত? উপরের ডিটেক্টর কীভাবে এই প্রশ্নের উত্তর দেয়?

না, নিশ্চিতভাবে না — উপরের ডিটেক্টর threshold প্যারামিটার নেয় বলেই, "লম্বা" আপেক্ষিক। ১৫ লাইনের একটি ফাংশন যদি একটিমাত্র, স্পষ্ট কাজ করে (যেমন একটি জটিল কিন্তু একক গাণিতিক সূত্র), তা হয়তো সম্পূর্ণ স্বাভাবিক। থ্রেশহোল্ড টিম-নির্ভর একটি নীতিগত সিদ্ধান্ত — মূল কথা হলো, লাইন-সংখ্যা নিজেই স্মেল নয়, এটি শুধু একটি প্রক্সি সংকেত যা "সম্ভবত একাধিক দায়িত্ব একসাথে আছে" এর দিকে ইঙ্গিত করে।

প্র ০২ detect_duplicate_code যদি দুটো ফাংশনের লাইনগুলো একই কিন্তু ভিন্ন ক্রমে থাকে (যেমন দ্বিতীয় ফাংশনে if-check আগে, calculation পরে — উল্টো ক্রমে), তাহলে কি ধরতে পারবে?

না — এই সরল ডিটেক্টর শুধু একই ইনডেক্সের লাইন তুলনা করে (lines_a[i] বনাম lines_b[i]), তাই লাইনগুলো পুনর্বিন্যাস করা থাকলে ম্যাচ মিস হয়ে যাবে, যদিও লজিক্যালি এখনো "ডুপ্লিকেট"। এটাই একটি সৎভাবে স্বীকার করা সীমাবদ্ধতা — বাস্তব diff/duplicate-detection টুল (যেমন Git-এর diff অ্যালগরিদম) এই সীমাবদ্ধতা কাটিয়ে উঠতে অনেক বেশি জটিল অ্যালগরিদম ব্যবহার করে।

প্র ০৩ Feature Envy স্মেল কেন M4/L14-এর "cohesion" লক্ষ্যের সরাসরি লঙ্ঘন?

কারণ Feature Envy মানে একটি মেথড তার নিজের ক্লাসের ডেটার চেয়ে অন্য ক্লাসের ডেটার প্রতি বেশি আগ্রহী — এটি ঠিক বিপরীত যা high cohesion চায় (একটি ক্লাসের ভেতরের সব দায়িত্ব একে অপরের সাথে ঘনিষ্ঠভাবে সম্পর্কিত হওয়া)। একটি envious মেথড আসলে বলে দিচ্ছে তার লজিক ভুল ক্লাসে বসে আছে — এটি সরিয়ে সেই "অন্য" ক্লাসেই রাখলে cohesion উন্নত হয়।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোডে detect_long_method(smelly_process_order_lines, threshold=20) চালিয়ে দেখুন ফলাফল কীভাবে বদলায়, এবং কেন।

    smelly_process_order_lines-এ ১৭টি লাইন আছে। থ্রেশহোল্ড ২০-এ বাড়ালে is_long_method এখন False হবে, কারণ 17 > 20 মিথ্যা। এটাই দেখায় হিউরিস্টিক ডিটেক্টরের ফলাফল থ্রেশহোল্ডের উপর সম্পূর্ণ নির্ভরশীল — কোডটা নিজে বদলায়নি, শুধু "লম্বা"-র সংজ্ঞা বদলেছে, তাই একই কোডে ভিন্ন থ্রেশহোল্ডে ভিন্ন রায় আসতে পারে।

  2. চিন্তা করুন: discount_calc_invoice_lines-এর তৃতীয় লাইন "return price * 0.85"-কে "return price * 0.80"-এ পাল্টালে (একটি ভিন্ন ছাড়ের হার) সিমিলারিটি স্কোর কত হবে, এবং এখনো কি duplicate হিসেবে ফ্ল্যাগ হবে?

    ৪টি লাইনের মধ্যে ফাংশনের নামের লাইন (index 0) আগে থেকেই মেলে না, এখন ছাড়ের হারের লাইনও (index 2) মিলবে না — শুধু "if price > 1000:" (index 1) ও "return price" (index 3) মিলবে, অর্থাৎ 2/4 = 0.5 সিমিলারিটি — যা threshold 0.7-এর নিচে, তাই is_duplicate: False হয়ে যাবে। এটাই এই সরল হিউরিস্টিকের একটি সৎ সীমাবদ্ধতা — একটি ছোট মানের পরিবর্তনও (কনস্ট্যান্ট বদলানো) স্কোরকে থ্রেশহোল্ডের নিচে নামিয়ে দিতে পারে, যদিও কোডটি এখনো স্পষ্টতই কপি-পেস্ট-করা, একই স্ট্রাকচারের ডুপ্লিকেশন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

পূর্ববর্তী পাঠ
ম্যানুয়াল বনাম অটোমেটেড টেস্টিং ও রিগ্রেশন টেস্টিং