মূল বিষয়বস্তুতে যান
Clean Code Mastery

Data Class: নিয়মহীন রেজিস্টার — যে কেউ যা খুশি লিখে যায়

Data Class smell শেখো একটা society register-এর গল্পের মাধ্যমে। দেখো কেন behavior ছাড়া data encapsulation ভেঙে পড়ে, আর কখন DTO আর record একদম ঠিকঠাক।

20 মিনিট আপডেট: June 11, 2026beginner
code-smellsdispensablesdata-classanemic-domain-modelencapsulationrefactoringtypescriptcsharp

নিয়মহীন রেজিস্টার — যে কেউ যা খুশি লিখে যায়

ধরো ঢাকার মিরপুরে একটা আবাসিক ভবন আছে — "সাগর অ্যাপার্টমেন্ট"। গেটে একটা visitor register রাখা আছে। সাধারণ একটা খাতা, বলপেন দিয়ে কলাম টানা: নাম, ফ্ল্যাট নম্বর, ঢোকার সময়, বের হওয়ার সময়। কোনো নিয়ম নেই। দারোয়ান সালাম ভাই সাধারণত কাউকে পার্কিং করতে সাহায্য করছেন। কলম দড়িতে ঝুলছে, যে আসে সে যা খুশি লেখে।

ভবনের সেক্রেটারি জামাল সাহেব এই রেজিস্টার নিয়ে বেশ গর্বিত। "যে ঢোকে তার পূর্ণ রেকর্ড আছে," তিনি কমিটিকে বলেন। "পুরো security।"

এক মাস পর খাতা খুলে দেখো:

  • এক visitor নাম লিখেছে "guest"। শুধু "guest"।
  • কেউ ফ্ল্যাট নম্বর লিখেছে 1403 — ভবনে মাত্র ৮টা floor
  • এক delivery boy সময় লিখেছে 25:70
  • একটা পুরো row ফাঁকা, শুধু একটা বিড়ালের আঁকিবুকি।
  • রুবেল নামের এক ছেলে চুপিচুপি ফিরে এসে তার বন্ধুর বের হওয়ার সময় রাত ১১টা থেকে বদলে ৯টা করে দিয়েছে — যাতে কেউ না জানে তারা রাতে ক্রিকেট খেলতে গিয়ে দেরিতে ফিরেছে।

তারপর একদিন মঙ্গলবার পার্কিং থেকে একটা সাইকেল চুরি হয়। জামাল সাহেব দৌড়ে গেটে গেলেন, রেজিস্টার খুললেন সেই সন্ধ্যায় কে এসেছিল দেখতে — আর পুরোটাই অকেজো মনে হলো। "guest" এসেছিল ফ্ল্যাট 1403-এ, সময় 25:70। data-ই আবর্জনা, কারণ রেজিস্টারে কোনো নিয়ম ছিল না। সে যে কোনো কিছু, যে কারো কাছ থেকে, যে কোনো সময়ে আনন্দের সাথে নিত — আর পরে যে কেউ যা খুশি বদলে দিতে পারত। খাতাটা data রাখত; data রক্ষা করত না।

চিত্র ১: নিয়মহীন রেজিস্টারে এক মাসের ঘটনা

এবার পাশের ব্যাংকের কথা ভাবো। সেখানে form-এ নাম লিখতে গেলে "guest" লিখলে কী হবে? 11 digit-এর account নম্বরের জায়গায় 1403 দিলে কী হবে? কাউন্টারেই clerk থামিয়ে দেবে — ভুল data খাতায় ঢোকার আগেই। ব্যাংকের "রেজিস্টার"-এ একজন guardian আছে যে লেখার মুহূর্তেই নিয়ম enforce করে — তাই stored data-কে সবসময় বিশ্বাস করা যায়। দুটো খাতার পার্থক্য এটুকুই: কাগজ না, guardian

Code-এ, একটা class যেখানে শুধু field আর getter-setter আছে — কোনো নিয়ম নেই, কোনো behavior নেই, কোনো guardian নেই — সেটাই হলো সেই visitor-এর খাতা। এটাই Data Class smell।

এই smell টা আসলে কী?

Data Class হলো এমন একটা class যেখানে শুধু field আর accessor আছে কিন্তু কোনো behavior নেই। সে নিজেকে validate করতে পারে না, নিজের সম্পর্কে কিছু calculate করতে পারে না, নিজেকে রক্ষাও করতে পারে না। তার data নিয়ে সব চিন্তাভাবনা করে অন্য class-রা, codebase জুড়ে ছড়িয়ে ছিটিয়ে — বাকি সবাইকে বাধ্য করে নিয়ম মনে রাখতে আর বারবার repeat করতে।

Martin Fowler তার bliki-তে এই smell-এর system-wide একটা version বর্ণনা করেছেন: Anemic Domain Model। object-গুলো দেখতে real domain model-এর মতো — Order, Customer, Invoice — কিন্তু ভেতরে রক্তশূন্য। শুধু getter-setter-এর থলে। সব logic বসে আছে বড় বড় procedural "service" class-এ, যারা থলে থেকে data বের করে, ভাবে, আর আবার ঢুকিয়ে দেয়। Fowler বলেন এটা একটা anti-pattern কারণ object-orientation-এর সব cost দিয়ে কোনো benefit পাওয়া যাচ্ছে না।

এখানে যে principle ভাঙছে সেটার একটা মনে রাখার মতো নাম আছে: Tell, Don't Ask। healthy object design বলে: object-কে বলো তুমি কী চাও (order.total()), সে নিজের data দিয়ে কাজ করুক। Anemic design-এ বরং raw data চেয়ে নেওয়া হয় (order.lines, order.discountRate) আর হিসাব করা হয় বাইরে — পাঁচটা আলাদা জায়গায়, পাঁচটা সামান্য আলাদা উপায়ে।

💡

একটা class হলো তার নিজের data-র স্বাভাবিক guardian। যখন data আর data সম্পর্কিত নিয়ম একই class-এ থাকে, নিয়মগুলো ঠিক একটাই জায়গায় enforce হয় আর bypass করার কোনো উপায় থাকে না। যখন আলাদা থাকে, প্রতিটা caller নিজে নিয়ম মনে রাখার দায়িত্ব পায় — আর কেউ না কেউ সবসময় ভুলে যায়।

একটা কথা এখনই বলে রাখি, কারণ এটা জরুরি: behavior-মুক্ত সব class-ই smell না। DTO, record, আর view model আসলেই plain data হওয়ার জন্য তৈরি — সেটা নিয়ে পরে পুরো সৎ আলোচনা করব। smell টা specifically তখনই হয় যখন একটা domain object-এর behavior থাকার কথা কিন্তু নেই।

একটু গভীরে যাও: এখানকার deep idea হলো invariant — এমন একটা condition যেটা object-এর পুরো জীবনকাল ধরে সত্য থাকতে হবে ("discount হবে 0 থেকে 1-এর মধ্যে", "বের হওয়ার সময় ঢোকার সময়ের পরে হবে")। Encapsulation-এর আসল মানে "field private করে getter যোগ করা" না — মানে হলো বাইরে থেকে invariant ভাঙা অসম্ভব করে দেওয়া। একটা data class-এ private field আছে কিন্তু zero invariant, তাই এটা syntax-এ encapsulated কিন্তু substance-এ না। Domain-Driven Design-এ এটাই হয় aggregate pattern: একটা aggregate root (যেমন Order) হলো সেই একমাত্র entry point যে সবকিছুর invariant রক্ষা করে। clerk সহ visitor register হলো aggregate; দড়িতে ঝোলানো খাতা নয়।

এই পুরো বিষয়টা একটা map-এ দেখো:

চিত্র ২: Data Class smell-এর পূর্ণ মানচিত্র

কীভাবে চিনবে

Code review-এর জন্য checklist:

  • এমন একটা class যেখানে শুধু public field, অথবা private field-এ mechanical getter-setter যে কোনো IDE generate করে দিতে পারে।
  • class-এর data নিয়ে যে logic কাজ করে সেটা class-এর ভেতরে ছাড়া সব জায়গায় আছে।
  • এই class-এর field-এ একই validation বা calculation কয়েকটা caller-এ বারবার লেখা আছে।
  • collection-গুলো raw getter দিয়ে বের করে দেওয়া হচ্ছে, তাই বাইরে থেকে class-এর অজান্তে item যোগ-বিয়োগ করা যাচ্ছে।
  • class টা পুরো codebase ঘুরে বেড়ায়, কিন্তু শুধু field পড়া বা পোকানোর জন্যই।
প্রশ্নSmelly উত্তরHealthy উত্তর
order total কে calculate করে?প্রতিটা caller, আলাদা আলাদাভাবেorder.total(), একবার
discount 0 থেকে 1-এর মধ্যে আছে কিনা কে দেখে?আশা করি কেউ না কেউ কোথাওapplyDiscount method, সবসময়
বাইরে থেকে কি lines list খালি করা যাবে?হ্যাঁ — order.lines আসল listনা — read-only view আর addLine()
object কি কখনো আজেবাজে value ধরতে পারে?হ্যাঁ, যেকোনো field-এ যেকোনো valueনা — প্রতিটা entry point-এ guard আছে
নিয়ম জানতে কোথায় পড়ব?codebase জুড়ে প্রতিটা callerclass নিজেই, একটা file

একটা কাজের sorting tool: data ধরে রাখে এমন যেকোনো class-কে এই chart-এ রাখো। বিপদের জায়গা হলো "guard করার মতো real rules আছে" কিন্তু "কিছুই guard করছে না"।

চিত্র ৩: কোন data-holding class আসলে smell?

কেন এটা সমস্যা

সমস্যা ১: Encapsulation ভেঙে পড়ে। যে কেউ যেকোনো field-এ যেকোনো value দিতে পারলে class নিজের correctness রক্ষা করতে পারে না। -৫০০ টাকার price, flat 1403, সময় 25:70 — সব চুপচাপ accept হয়ে যায়। Correctness এখন নির্ভর করছে প্রতিটা caller সবসময় সব নিয়ম মনে রাখবে কিনা তার উপর।

সমস্যা ২: নিয়ম duplicate হয়। "discount 0 থেকে 1 হতে হবে" — এই নিয়ম লেখা হয় order screen-এ, admin screen-এ, আর import job-এ। তিনটা copy। নিয়ম যদি "সর্বোচ্চ 0.5" হয়ে যায়, তিনটাই খুঁজে বের করতে হবে — এটাই Duplicate Code smell, Data Class smell থেকেই জন্ম নিচ্ছে।

সমস্যা ৩: data-র কোনো একক ব্যাখ্যা নেই। discountRate আসলে কী মানে আর কীভাবে বদলানো legal — সেটা জানতে সব জায়গা পড়তে হবে যেখানে এটা touch করা হয়েছে। Behavior-ওয়ালা class-এ একটাই file পড়লেই হয়।

সমস্যা ৪: Leaked internal দিয়ে পিছন থেকে ছুরি মারা যায়। যে getter আসল internal list return করে, সেটা দিয়ে যেকোনো caller order.lines.clear() করতে পারে যেকোনো জায়গা থেকে। Order নষ্ট হয়ে যায়, কিন্তু stack trace দোষীকে ধরতে পারে না। রুবেল বন্ধুর বের হওয়ার সময় বদলে দেওয়া — এটাই ঠিক এই ব্যাপারটা: internal-এ write access, কোনো audit নেই, কোনো guard নেই।

সমস্যা ৫: Feature Envy-র জন্ম দেয়। data class-এ যে behavior থাকার কথা সেটা তো কোথাও না কোথাও থাকতে হবে — তাই সে service আর helper-এ গিয়ে আশ্রয় নেয়, সারাদিন data class-এর field হিংসা করে পোকায়। Data Class আর Feature Envy একই মুদ্রার দুই পিঠ।

দেখো garbage কীভাবে ঢোকে, slow motion-এ। লক্ষ্য করো object কখনো আপত্তি করছে না:

চিত্র ৪: Anemic object সব কিছু accept করে; bug পাওয়া যায় অনেক পরে, অন্য জায়গায়

ক্ষতি বাড়ে যত বেশি জায়গা data touch করে। প্রতিটা নতুন caller মানে নিয়ম ভুলে যাওয়ার আরেকটা সুযোগ:

চিত্র ৫: বেশি caller মানে বেশি rule copy আর বেশি drift

Rich class-এর সাথে এই line সবসময় 1-এ flat থাকে — যত caller-ই আসুক। এই flat line-টাই এই refactoring-এর পুরো argument।

চিত্র ৬: Anemic order — প্রতিটা caller নিয়ম বহন করে; class কিছুই বহন করে না

Code-এ বাস্তব উদাহরণ

ধরো society অবশেষে তাদের visitor register digitize করল। প্রথম version খাতাটাকেই হুবহু copy করল — তার নিয়মহীনতা সহ।

// Smelly version: the digital notebook with no rules
class VisitorEntry {
  name = "";
  flatNumber = 0;
  inTimeMinutes = 0;   // minutes since midnight
  outTimeMinutes = 0;
}
 
class VisitorRegister {
  entries: VisitorEntry[] = [];
}
 
// gate screen, somewhere:
const e = new VisitorEntry();
e.name = prompt("Name?") ?? "";
e.flatNumber = Number(prompt("Flat?"));
e.inTimeMinutes = nowInMinutes();
register.entries.push(e);
 
// security report, in another file:
function visitDuration(e: VisitorEntry): number {
  return e.outTimeMinutes - e.inTimeMinutes;  // negative if out < in!
}
 
// admin panel, in a third file:
function isStillInside(e: VisitorEntry): boolean {
  return e.outTimeMinutes === 0;   // "0 means not exited"... says who?
}
 
// and a prank, from anywhere at all:
register.entries.length = 0;        // entire register wiped, silently

খাতার প্রতিটা বিপর্যয় এখন code-এ সম্ভব:

  1. e.name = "" — "guest"/blank-row সমস্যা। কেউ দেখছে না।
  2. e.flatNumber = 1403 — flat হয় 101 থেকে 804 পর্যন্ত, কিন্তু class যেকোনো কিছু নেয়।
  3. visitDuration negative হতে পারে; isStillInside একটা গোপন নিয়ম আবিষ্কার করে ("0 মানে বের হয়নি") যেটা শুধু একটা caller-এর মাথায় আছে।
  4. register.entries আসল array, তাই যে কেউ সেটা মুছে দিতে পারে — রুবেলের বের হওয়ার সময় বদলানো, এখন এক line code-এ।
  5. প্রতিটা caller নিজের মতো নিয়ম বোঝে। এরই মধ্যে মতভেদ শুরু হয়ে গেছে।

ধাপে ধাপে ঠিক করো

একজন guardian নিয়োগ দিচ্ছি। ধাপে ধাপে খাতাটা ব্যাংক রেজিস্টার হয়ে উঠবে।

ধাপ ১: Encapsulate Field. Field-গুলো private করো আর সব লেখা method-এর মধ্য দিয়ে নিয়ে যাও যেটা নিয়ম চেক করবে। Constructor নিজেই garbage reject করবে।

ধাপ ২: Move Method. visitDuration আর isStillInside শুধু VisitorEntry-র data ব্যবহার করে — এটাই classic Feature Envy। এদের বাড়ি ফিরিয়ে দাও, class-এর ভেতরে।

ধাপ ৩: Encapsulate Collection. Register একটা read-only view দেবে আর checkIn/checkOut method দেবে। আসল array অস্পৃশ্য হয়ে যাবে।

// Clean version: the register now has a guardian
class VisitorEntry {
  private outTime: number | null = null;
 
  constructor(
    private readonly name: string,
    private readonly flatNumber: number,
    private readonly inTime: number,
  ) {
    if (name.trim().length < 2) throw new Error("Real name required");
    if (!isValidFlat(flatNumber)) throw new Error(`No such flat: ${flatNumber}`);
    if (inTime < 0 || inTime >= 1440) throw new Error("Invalid time");
  }
 
  checkOut(outTime: number): void {
    if (this.outTime !== null) throw new Error("Already checked out");
    if (outTime < this.inTime) throw new Error("Exit before entry? No.");
    this.outTime = outTime;
  }
 
  isStillInside(): boolean {
    return this.outTime === null;          // the rule, stated once, clearly
  }
 
  visitDurationMinutes(): number | null {
    return this.outTime === null ? null : this.outTime - this.inTime;
  }
}
 
class VisitorRegister {
  private readonly entries: VisitorEntry[] = [];
 
  get allEntries(): ReadonlyArray<VisitorEntry> {
    return this.entries;                   // a view, not the real thing
  }
 
  checkIn(name: string, flatNumber: number, inTime: number): VisitorEntry {
    const entry = new VisitorEntry(name, flatNumber, inTime);
    this.entries.push(entry);
    return entry;
  }
}

দেখো কী বদলে গেছে:

  • ফাঁকা নাম বা flat 1403 দিয়ে VisitorEntry তৈরিই করা যাবে না। Constructor হলো ব্যাংকের কাউন্টারের clerk।
  • "ভেতরে আছে" বোঝার একটাই সংজ্ঞা — outTime === null — একবার লেখা, class-এর ভেতরে, কোনো caller-এর মাথার গোপন 0 convention নয়।
  • Negative duration অসম্ভব; entry-র আগে exit হলে দরজাতেই reject।
  • register.entries.length = 0 আর compile-ই হবে না। দুষ্টুমি বন্ধ।
  • Caller-রা এখন Tell, Don't Ask মানছে: entry.visitDurationMinutes() জিজ্ঞেস করছে, field টেনে নিজে calculate করছে না।

Operation-এর পরের structure:

চিত্র ৭: Refactor-এর পরে — data আর তার নিয়ম অবশেষে একই class-এ

Entry-কে একটা ছোট্ট machine হিসেবে দেখার সুন্দর উপায়ও আছে। Rich class illegal jump অসম্ভব করে দেয়:

চিত্র ৮: Visitor entry একটা state machine হিসেবে — rich class শুধু legal move-ই allow করে

Anemic version-এ, সেই "rejected" arrow-গুলোর প্রতিটাই ছিল খোলা দরজা।

C#-এ একই smell

Classic anemic order, হাজার হাজার real codebase-এ ঠিক এভাবেই থাকে:

// Before: anemic data holder; callers do its thinking
public class Order
{
    public List<OrderLine> Lines { get; set; }
    public decimal DiscountRate { get; set; }
}
 
// far away, in some service:
decimal total = 0;
foreach (var line in order.Lines)
    total += line.UnitPrice * line.Quantity;
total -= total * order.DiscountRate;   // duplicated wherever a total is needed

Behavior-কে বাড়ি নিয়ে যাও আর দরজা বন্ধ করো:

// After: the class owns the rules about its own data
public class Order
{
    private readonly List<OrderLine> _lines = new();
 
    public IReadOnlyList<OrderLine> Lines => _lines;     // no outside mutation
    public decimal DiscountRate { get; private set; }
 
    public void AddLine(OrderLine line) => _lines.Add(line);
 
    public void ApplyDiscount(decimal rate)
    {
        if (rate is < 0 or > 1)
            throw new ArgumentOutOfRangeException(nameof(rate));
        DiscountRate = rate;
    }
 
    public decimal Total()
    {
        var subtotal = _lines.Sum(l => l.UnitPrice * l.Quantity);
        return subtotal - subtotal * DiscountRate;
    }
}
 
// every caller, everywhere:
decimal total = order.Total();

Total-এর নিয়ম একবারই লেখা। Illegal discount set করা যাবে না। Order-এর অজান্তে lines clear করার উপায় নেই। এই যাত্রা — anemic থেকে rich-এ — এটাই domain-driven design-এর মূল কথা।

আর Python-এ, যেখানে @dataclass plain data সহজ করে দেয় — boundary-তে অসাধারণ, domain-এ বিপজ্জনক:

# Fine as a boundary DTO: plain by design
from dataclasses import dataclass
 
@dataclass(frozen=True)
class VisitorSummaryDto:
    name: str
    flat_number: int
 
# Rich in the domain: the guardian pattern
class VisitorEntry:
    def __init__(self, name: str, flat_number: int, in_time: int):
        if len(name.strip()) < 2:
            raise ValueError("Real name required")
        if not is_valid_flat(flat_number):
            raise ValueError(f"No such flat: {flat_number}")
        self._name = name
        self._flat = flat_number
        self._in_time = in_time
        self._out_time: int | None = None
 
    def check_out(self, out_time: int) -> None:
        if self._out_time is not None:
            raise ValueError("Already checked out")
        if out_time < self._in_time:
            raise ValueError("Exit before entry? No.")
        self._out_time = out_time

একটু গভীরে যাও: ওই Python snippet-এ একটা architectural pattern লুকিয়ে আছে: boundary-তে plain, core-এ smart। Hexagonal/clean architecture-এর ভাষায়, DTO থাকে adapter layer-এ (JSON, database row, message format মেলায়), আর invariant-guarding entity থাকে domain layer-এ। CQRS এটা আরো এগিয়ে নেয়: write-side model rich হয় (change-এর সময় invariant রক্ষা করতে হয়), read-side projection ইচ্ছাকৃতভাবে anemic হয় (শুধু display বা reporting-এর জন্য flat data)। তাই "data class কি smell?" প্রশ্নের একটা precise architectural উত্তর আছে: নির্ভর করছে তুমি কোন layer-এ দাঁড়িয়ে আছো। adapter-এ যেটা সঠিক, domain-এ সেটাই রোগ।

Real project-এ এই smell কোথায় লুকায়

  • অতিরিক্ত "layered enterprise" architecture। "entity মানে শুধু data; সব logic যাবে service layer-এ" — এই culture mass-produce করে anemic model। Service layer ফুলে ফেঁপে হাজার line-এর procedural script হয়ে যায় আর entity রক্তশূন্যই থাকে।
  • ORM entity-কে domain model হিসেবে ব্যবহার। Database mapping tool-রা আগে সব কিছুতে public getter-setter চাইত, এভাবে একটা প্রজন্মের developer শিখল entity hollow করতে আর পিছনে তাকাতে নেই।
  • IDE-generated accessor reflexes। Field বানাও, generate-getters-setters shortcut চাপো, শেষ। Class জন্ম নেয় anemic হয়ে, আর behavior লেখা হয় developer যেখানে দাঁড়িয়ে সেখানেই।
  • Exposed collection। public List<Student> Students { get; set; } — যেকোনো consumer add, remove, clear, বা পুরো list replace করতে পারে। "একটা section-এ সর্বোচ্চ ৪০ জন student" — এই invariant enforce করা অসম্ভব হয়ে যায়।
  • Validation শুধু UI-তে। Form নিয়ম চেক করে, কিন্তু domain object যেকোনো কিছু accept করে। তারপর একটা import job, message consumer, বা দ্বিতীয় UI সরাসরি লেখে — আর garbage পাশের দরজা দিয়ে ঢুকে, ঠিক চিত্র ৪-এর import job-এর মতো।

Team-গুলো যখন audit করে তাদের anemic class কোথা থেকে এলো, blame সাধারণত এভাবে ভাগ হয়:

চিত্র ৯: Real team-এ domain class কেন anemic হয়

কখন ignore করা যাবে

এই পোস্টের সবচেয়ে গুরুত্বপূর্ণ সৎ section এটা। Plain data class কখনো কখনো ঠিক সেটাই যা দরকার। প্রতিটাকে smell বলাটা beginner-এর ভুল।

Class-এর ধরনSmell?কেন ঠিক আছে (বা নেই)
Boundary পেরোনো DTO (API payload, queue message)নাতার কাজই হলো JSON বা wire format-এ map হওয়া একটা transparent shape
C# record / Java record / Python @dataclassনাLanguage-এর নিজস্ব immutable value bundle; বাড়তি ceremony যোগ করলে language-এর বিরুদ্ধে লড়াই করা হয়
Read model / view model / CQRS projectionনাDisplay বা reporting-এর জন্য ইচ্ছাকৃতভাবে flat, query-shaped data
Configuration objectনাSettings স্বভাবতই data
Functional-programming style record আর pure functionনাFP-তে immutable data আর আলাদা function-ই intended design
নিয়ম আছে এমন domain entity যেটাকে getter/setter বানানো হয়েছেহ্যাঁতার invariant guard করা আর calculation করার কথা, কিন্তু পারছে না
এমন "domain" object যার নিয়ম caller-দের মধ্যে copy হয়ে আছেহ্যাঁDuplication আর drift প্রমাণ করছে behavior-টা ভেতরেই থাকার কথা

Healthy DTO আর sick domain object চেনার উপায়? জিজ্ঞেস করো: এই data-র কি এমন নিয়ম আর invariant আছে যেটা সবসময় সত্য থাকতে হবে?

  • JSON হিসেবে বের হওয়া OrderResponseDto-র কোনো নিয়ম রক্ষা করার নেই — এটা data-র একটা ছবি, frozen আর outbound। Plain হওয়াই perfect।
  • তোমার domain-এর ভেতরের Order-এর নিয়ম আছে — "discount 0 থেকে 1", "total এভাবে calculate হয়", "বাইরে থেকে lines mutate করা যাবে না"। সেটা যদি নিজেকে রক্ষা করতে না পারে, সে anemic।
⚠️

তোমার DTO-তে business logic ঢুকিয়ে "ঠিক করতে" যেও না। Behavior সহ DTO নিজেই এক ঝামেলা — এখন তোমার wire format আর business rule একসাথে বদলাবে। অনেক system-এর সঠিক shape হলো: মাঝখানে rich domain object, edge-এ thin DTO, আর দুটোর মধ্যে mapping। Boundary-তে plain, core-এ smart।

কোন refactoring দিয়ে ঠিক করবে

Symptomঠিক করার refactoringResult
এই data-র উপর behavior অন্য class-এMove MethodLogic চলে আসে data-র মালিক class-এ
খোলা public fieldEncapsulate Fieldলেখা যায় শুধু guarded method-এর মধ্য দিয়ে
Raw collection বের করে দেওয়াEncapsulate CollectionRead-only view আর add/remove method
Caller বারবার getter নিয়ে একই calculation করছেExtract Method + Move Methodসেই calculation class-এর একটা method হয়ে যায়
যে setter কখনো থাকাই উচিত নাRemove Setting MethodConstruction-এর পরে immutable

Fowler-এর catalog থেকে একটা কাজের hunting tactic: প্রতিটা getter-এর caller-দের দেখো। কয়েকটা caller যদি একই value নিয়ে একই calculation করে, সেই calculation class-এ method হতে চাইছে। Getter-গুলো follow করো; ওরাই missing behavior-এর কাছে নিয়ে যাবে।

চিত্র ১০: সমাধানের পথ — খাতা থেকে ব্যাংক রেজিস্টার

দ্রুত revision

+--------------------------------------------------------------+
|  DATA CLASS — QUICK REVISION                                 |
+--------------------------------------------------------------+
|  Story   : A visitor register with no rules — anyone         |
|            scribbles anything, so the data can't be trusted. |
|  Smell   : A DOMAIN class with fields + getters/setters      |
|            but no behavior; others do its thinking.          |
|  Why bad : Cannot guard its invariants; rules duplicated     |
|            across callers; internals leak; data goes bad.    |
|  Principle: Tell, Don't Ask — ask order.total(), don't       |
|            pull fields and compute outside.                  |
|  NOT smell: DTOs, records, view models, config objects —     |
|            plain-by-design data at boundaries is GOOD.       |
|  Cures   : Move Method, Encapsulate Field,                   |
|            Encapsulate Collection, Remove Setting Method.    |
|  Motto   : Plain at the boundary, smart at the core.         |
+--------------------------------------------------------------+

Practice exercise

একটা library management program-এর anemic heart আছে। এটা নিয়ে কাজ করো।

class LibraryBook {
  title = "";
  timesIssued = 0;
  isIssued = false;
  dueDateDay = 0;        // day of month; 0 means "no due date"
}
 
class Library {
  books: LibraryBook[] = [];
}
 
// in the issue-desk screen:
function issueBook(b: LibraryBook, today: number): void {
  b.isIssued = true;
  b.timesIssued = b.timesIssued + 1;
  b.dueDateDay = today + 14;          // can become 36 if today is 22!
}
 
// in the fine-counter screen:
function fineFor(b: LibraryBook, today: number): number {
  return (today - b.dueDateDay) * 2;  // negative fine if returned early!
}
 
// in the reports screen:
function isOverdue(b: LibraryBook, today: number): boolean {
  return b.isIssued && today > b.dueDateDay && b.dueDateDay !== 0;
}

তোমার কাজ:

  1. LibraryBook-এর বাইরে থাকা book-এর data সম্পর্কিত প্রতিটা নিয়ম list করো। (Hint: কমপক্ষে চারটা আছে, গোপন "0 মানে due date নেই" convention সহ।)
  2. Caller-দের মধ্যে ইতিমধ্যে থাকা দুটো real bug খুঁজে বের করো। (Due-date arithmetic আর fine calculation দেখো।)
  3. Refactor করো: issue, fineFor, আর isOverdue — এগুলো Move Method দিয়ে LibraryBook-এর ভেতরে নিয়ে যাও। Field-গুলো private করো। Move করার সময়ই দুটো bug ঠিক করো — class এখন নিজের correctness-এর দায়িত্বে।
  4. গোপন 0 convention-কে honest কিছু দিয়ে replace করো (dueDateDay: number | null)। লক্ষ্য করো class এখন এই detail caller-দের থেকে পুরোপুরি লুকাতে পারছে।
  5. Encapsulate Collection দিয়ে Library.books protect করো: একটা read-only view আর একটা addBook method।
  6. একটা book-এর state machine আঁকো (চিত্র ৮-এর মতো): available → issued → returned। চিহ্নিত করো কোন illegal jump তোমার নতুন class এখন reject করছে।
  7. শেষে, library-র public website API-এর জন্য title আর timesIssued সহ একটা BookSummaryDto design করো — পুরোপুরি behavior-মুক্ত। এক বাক্যে বলো এই plain class কেন Data Class smell নয়

যখন তোমার LibraryBook আর impossible data রাখতে পারবে না — আর তোমার DTO গর্বের সাথে, সঠিকভাবে plain থাকবে — তখন বুঝবে এই lesson-এর দুটো অংশই তুমি আয়ত্ত করেছ, আর জামাল সাহেবের সাইকেল চোর ধরা পড়ে যেত।

সচরাচর জিজ্ঞাসা

Data Class smell জিনিসটা সহজ কথায় কী?
এটা এমন একটা class যেখানে শুধু field আর getter-setter আছে, কিন্তু নিজের কোনো behavior নেই। তার data নিয়ে সব চিন্তাভাবনা — validation, calculation, নিয়মকানুন — করে অন্য class-রা, দূরে দূরে ছড়িয়ে থেকে। data আর তার নিয়ম আলাদা জায়গায় বাস করে, অথচ সবসময় একসাথেই বদলায়।
DTO আর record কি Data Class smell?
না। DTO একটা boundary পেরিয়ে data বহন করে — যেমন API বা message queue — আর তার কাজই হলো একটা সহজ, স্বচ্ছ shape হওয়া। Record আর dataclass হলো language-এর নিজস্ব উপায়ে immutable value bundle মডেল করার। smell তখনই হয় যখন একটা DOMAIN object-এর নিজের নিয়ম থাকার কথা, কিন্তু সেটাকে getter-setter-এর থলেতে পরিণত করা হয়েছে।
Anemic domain model মানে কী?
এটা Martin Fowler-এর দেওয়া নাম — এমন একটা design যেখানে domain object দেখতে সত্যিকারের মনে হয় কিন্তু ভেতরে কোনো behavior নেই, শুধু data। আর সব logic বসে থাকে procedural service class-এ। দূর থেকে object-oriented মনে হয়, কিন্তু object-এর মূল সুবিধাটাই পাওয়া যায় না: data আর সেই data-র উপর কাজগুলো একসাথে রাখা।
Tell, Don't Ask মানে কী?
কোনো object-এর কাছ থেকে data চেয়ে নিজে calculation করার বদলে, object-কে সরাসরি বলো তুমি কী চাও — সে নিজেই ভাবুক। বাইরে থেকে lines আর discount টেনে এনে total হিসাব করার বদলে শুধু order.total() জিজ্ঞেস করো।
Data Class ঠিক করতে কোন refactoring ব্যবহার করবে?
Move Method দিয়ে সেই behavior-কে সেই class-এ নিয়ে যাও যে class data-র মালিক। Encapsulate Field দিয়ে খোলা public field-এ controlled access বসাও। Encapsulate Collection দিয়ে বাইরের লোককে internal list নষ্ট করা থেকে বিরত রাখো — read-only view আর add/remove method দিয়ে।

আরো দেখো

সম্পর্কিত পাঠ

Feature Envy: যে method সারাদিন অন্যের class-এ বসে থাকে

Feature Envy code smell শেখো একটা সহজ স্কুলের গল্পের মাধ্যমে। যখন একটা method নিজের class-এর চেয়ে অন্য class-এর data বেশি ব্যবহার করে, তখন সেটা আসলে ওই অন্য class-এই থাকার কথা। সারানোর উপায় হলো Move Method।

আরও পড়ুন

Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number

Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।

আরও পড়ুন

Lazy Class: যে চাকরির কাজ শুধু একটা বাটন চাপা

Lazy Class code smell শিখো একটা মজার গল্পের মাধ্যমে। কোন class-গুলো টিকে থাকার যোগ্যতা রাখে না সেটা বুঝতে পারবে, আর Inline Class দিয়ে সেগুলো ঠিক করতে পারবে।

আরও পড়ুন

Move Method: কাজটা সেই class-এ নিয়ে যাও যেখানে সে আসলে থাকে

একটা স্কুলের গল্পের মাধ্যমে Move Method রিফ্যাক্টরিং শেখো। যে class-এর data method-টা সবচেয়ে বেশি ব্যবহার করে, সেখানেই সরিয়ে নাও — যাতে behaviour আর data একসাথে থাকে।

আরও পড়ুন