Large Class smell: অনেক কাজ করা class ভাগ করুন

যে class data রাখে, লেখা সাজায় ও বার্তা পাঠায় সে তিনটি কাজ করছে। Large Class নিরাপদে ভাগ করা শিখুন, পরীক্ষিত আগে-পরের code-সহ।

একটি বেড়ে যাওয়া class ভাগ হয়ে মনোযোগী class

Large Class-এ অনেক field, অনেক method ও বদলানোর অনেক কারণ থাকে। প্রায়ই ছোট থেকে শুরু হয়। কেউ "আরেকটি method" যোগ করে, তারপর আরও কয়েকটি, আর class শেষে data রাখে, report সাজায় ও বার্তা পাঠায়। নিচের code ও output প্রকাশের আগে Python 3-এ চালানো।

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

  • নাম অস্পষ্ট (Manager, Helper, Utils, Data) বা class বাড়তেই থাকে।
  • কিছু method-গোষ্ঠী আলাদা field ব্যবহার করে। অর্ধেক field অর্ধেক method ব্যবহার করে।
  • email-এর লেখা বদলাতে গেলে ব্যালেন্সের নিয়ম থাকা ফাইলই বদলাতে হয়।
  • এর test-এ অনেক অসম্পর্কিত প্রস্তুতি লাগে।
  • বর্ণনায় নিজেকে "এবং" বলতে দেখেন: "ব্যালেন্স রাখে এবং statement ছাপে এবং alert পাঠায়"।

এটিই single responsibility principle-এর ধারণা: একটি class-এর বদলানোর কারণ একটিই।

সমাধান: Extract Class

যে field ও method-এর গোষ্ঠী একসঙ্গে থাকার, সেটি নতুন class-এ সরান এবং মূলটি সেটি ব্যবহার করুক (বা caller দুটিই)। উদাহরণে account শুধু টাকার নিয়ম রাখে। সাজানো ও বার্তা সরে গেছে।

# ---- BEFORE: one class that holds money, formats text and sends messages ----
class AccountBefore:
    def __init__(self, owner):
        self.owner, self.balance, self.history = owner, 0, []
    def deposit(self, amount):
        self.balance += amount; self.history.append(("deposit", amount))
    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("insufficient funds")
        self.balance -= amount; self.history.append(("withdraw", amount))
    def statement(self):
        lines = [f"Statement for {self.owner}"] + [f"{kind:<9}{amt:>8}" for kind, amt in self.history]
        return "\n".join(lines + [f"balance  {self.balance:>8}"])
    def to_csv(self):
        return "\n".join(f"{kind},{amt}" for kind, amt in self.history)
    def alert(self, outbox):
        outbox.append(f"To {self.owner}: balance is {self.balance}")

# ---- AFTER: the account keeps money; formatting and messaging live elsewhere ----
class Account:
    def __init__(self, owner):
        self.owner, self.balance, self.history = owner, 0, []
    def deposit(self, amount):
        self.balance += amount; self.history.append(("deposit", amount))
    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("insufficient funds")
        self.balance -= amount; self.history.append(("withdraw", amount))

class StatementFormatter:
    def text(self, acct):
        lines = [f"Statement for {acct.owner}"] + [f"{kind:<9}{amt:>8}" for kind, amt in acct.history]
        return "\n".join(lines + [f"balance  {acct.balance:>8}"])
    def csv(self, acct):
        return "\n".join(f"{kind},{amt}" for kind, amt in acct.history)

class Notifier:
    def __init__(self, outbox): self.outbox = outbox
    def balance_alert(self, acct):
        self.outbox.append(f"To {acct.owner}: balance is {acct.balance}")

Account এখন ছোট ও ভাবা সহজ। StatementFormatter টাকার নিয়ম না ছুঁয়ে নতুন ফরম্যাট (HTML, PDF) পেতে পারে, আর Notifier test-এ fake দিয়ে বদলানো যায়।

একই আচরণ প্রমাণ করুন

old, new = AccountBefore("Amina"), Account("Amina")
for a in (old, new):
    a.deposit(500); a.withdraw(120)
box_old, box_new = [], []
f = StatementFormatter()
print(f.text(new))
old.alert(box_old); Notifier(box_new).balance_alert(new)
assert old.statement() == f.text(new) and old.to_csv() == f.csv(new) and box_old == box_new
print(box_new[0])
print("same results, three small classes instead of one big one")
Statement for Amina
deposit       500
withdraw      120
balance       380
To Amina: balance is 380
same results, three small classes instead of one big one

পুরনো text statement, CSV ও alert নতুনগুলোর সঙ্গে হুবহু এক। আচরণ বদলায়নি, কিন্তু প্রতিটি কাজ এখন নিজস্ব class-এ।

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

  1. আগে বর্তমান public আচরণের চারপাশে test লিখুন।
  2. যে field-গোষ্ঠী সবসময় কিছু method-এর সঙ্গে থাকে সেগুলো খুঁজুন।
  3. নতুন class বানিয়ে একবারে একটি method সরান, প্রতিটি সরানোর পর test চালান।
  4. অনেক caller নির্ভর করলে কিছুদিন পুরনো method পাতলা মোড়ক হিসেবে রাখুন, তারপর সরান।
  5. class-কে কাজ দিয়ে নাম দিন (StatementFormatter), অস্পষ্ট শব্দে নয় (AccountHelper)।

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

  • একই data ব্যবহার করা অনেক সহজ method-এর class-ও সংহত হতে পারে।
  • ভাগ করলে একটি কাজ বুঝতে ছয়টি ফাইলে লাফাতে হলে ভাগ বেশি সূক্ষ্ম।

সাধারণ ভুল

  • দায়িত্ব নয়, শুধু প্রযুক্তিগত স্তর ধরে ভাগ ("সব getter এখানে")।
  • নতুন Utils class বানানো যা পরের large class হয়ে ওঠে।
  • method সরিয়ে তার ব্যবহৃত field না সরানো, ফলে নতুন class পুরনোটির দিকেই হাত বাড়ায়।
  • code সরানোর সময় আচরণ বদলে ফেলা। একবারে একটি কাজ করুন।

মূল কথা: class-এ আলাদা field ব্যবহার করা method-গোষ্ঠী থাকলে প্রতিটি গোষ্ঠী নিজস্ব class হতে চায়। একটি একটি method করে extract করুন, test সবুজ রাখুন, আর প্রতিটি class-কে তার একক কাজ দিয়ে নাম দিন।


Code smell ও refactoring সিরিজ, Bloaters: 2 নম্বর (মোট 5)। আগের লেখা: Long Method smell: চিনুন ও Extract Method দিয়ে সারান.

আরও পড়ুন

Primitive Obsession: খোলা string ও float-এর বদলে value object

খোলা string ও float ভুল মান যেকোনো জায়গায় যেতে দেয়। টাকা ও email-এর ছোট value object কীভাবে নিয়ম যোগ করে ও এক শ্রেণির bug দূর করে শিখুন।

Code smell•৩ মিনিটে পড়া

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

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

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