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

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

খোলা primitive মান এক value object-এ মোড়া

Primitive Obsession মানে অর্থ ও নিয়ম আছে এমন জিনিসের জন্য খোলা string, সংখ্যা ও boolean ব্যবহার: টাকা, email ঠিকানা, ফোন নম্বর, তারিখ, ডাক কোড। একটি float জানে না যে সে দাম, আর একটি string জানে না যে তাতে @ থাকতে হবে। নিচের code ও output প্রকাশের আগে Python 3-এ চালানো।

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

  • টাকা float-এ রাখা, মুদ্রা আলাদা variable-এ বা কোথাও নেই।
  • ছড়িয়ে থাকা অনেক if "@" in email যাচাই, বা একটিও নেই।
  • "pending" বা "BD"-এর মতো magic string অনেক ফাইলে তুলনা।
  • একসঙ্গে লাগে এমন দুটি মান (পরিমাণ ও মুদ্রা) আলাদা পাঠানো।
  • একই যাচাই কয়েকটি function-এ কপি করা।

কী ভুল হতে পারে

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

সমাধান: primitive-কে ছোট object দিয়ে বদলান

Value object একটি primitive মুড়ে তার নিয়ম বহন করে। একবার বানান, একবার যাচাই করুন, আর যে function এটি পায় সে ভরসা করতে পারে। frozen dataclass এটিকে অপরিবর্তনীয় করে ও বিনামূল্যে সমতা দেয়।

from dataclasses import dataclass

# ---- BEFORE: money as a float, an email as a bare string ----
print("float sum:", 0.1 + 0.2, "| equals 0.3?", 0.1 + 0.2 == 0.3)
print("USD + BDT as plain numbers:", 10.0 + 1200.0)   # nonsense, and nothing stops it

# ---- AFTER: small value objects that know their own rules ----
@dataclass(frozen=True)
class Money:
    cents: int
    currency: str
    @classmethod
    def of(cls, amount: str, currency: str):
        whole, _, frac = amount.partition(".")
        return cls(int(whole) * 100 + int((frac + "00")[:2]), currency)
    def __add__(self, other):
        if other.currency != self.currency:
            raise ValueError(f"cannot add {other.currency} to {self.currency}")
        return Money(self.cents + other.cents, self.currency)
    def __str__(self):
        return f"{self.cents // 100}.{self.cents % 100:02d} {self.currency}"

@dataclass(frozen=True)
class Email:
    value: str
    def __post_init__(self):
        if self.value.count("@") != 1 or " " in self.value or "." not in self.value.split("@")[1]:
            raise ValueError(f"not an email address: {self.value!r}")

Money পূর্ণসংখ্যার cent রাখে, আলাদা মুদ্রা যোগ করতে দেয় না এবং নিজেকে ঠিকভাবে ছাপে। Email ভুল ঠিকানায় তৈরিই হতে দেয় না, তাই Email নেওয়া যেকোনো function জানে সেটি ইতিমধ্যে বৈধ।

আসল output

float sum: 0.30000000000000004 | equals 0.3? False
USD + BDT as plain numbers: 1210.0
0.30 USD
True
error: cannot add BDT to USD
amina@example.com
error: not an email address: 'amina.example.com'

float যোগ 0.30000000000000004 ছাপে, আর Money 0.10 ও 0.20 যোগ করে ঠিক 0.30 USD দেয় এবং দুটি সমান। USD ও BDT মেশানো এখন নীরবে ভুল সংখ্যার বদলে error, আর ভুল email প্রোগ্রামের গভীরে নয়, প্রান্তেই ব্যর্থ হয়।

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

  1. নিয়ম বহনকারী একটি primitive বাছুন (টাকা বা email দিয়ে শুরু)।
  2. value object বানিয়ে আলাদা test করুন।
  3. আগে সীমানায় (যেখানে data ঢোকে) primitive বদলান, তারপর ভেতরে এগোন।
  4. type অপ্রয়োজনীয় করে দিলে ছড়ানো যাচাই মুছুন।
  5. status-এর মতো নির্দিষ্ট string-সেটে Enum ব্যবহার করুন।

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

  • যে সাধারণ int শুধু গণনা, তাতে মোড়ক লাগে না।
  • প্রতিটি string মুড়বেন না। নিয়ম বা একক আছে এমনগুলো মুড়ুন।
  • বাস্তব টাকার code-এ rounding mode বা অনেক মুদ্রা লাগলে হাতে লেখা class-এর বদলে Python-এর decimal.Decimal বা ভালো পরীক্ষিত library ভাবুন।

সাধারণ ভুল

  • যাচাইহীন মোড়ক, যা type যোগ করে কিন্তু নিরাপত্তা নয়।
  • পরিবর্তনযোগ্য value object যা পায়ের নিচে বদলায়। frozen করুন।
  • এক জায়গায় constructor-এ যাচাই করে আবার প্রতিটি function-এও করা।
  • "আপাতত" টাকা float-এ রাখা।

মূল কথা: কোনো মানের নিয়ম থাকলে তাকে type দিন। Money ও Email-এর মতো ছোট value object যাচাই এক জায়গায় আনে এবং ভুল মান ঘোরাফেরা অসম্ভব করে।


Code smell ও refactoring সিরিজ, Bloaters: 3 নম্বর (মোট 5)। আগের লেখা: Large Class smell: অনেক কাজ করা class ভাগ করুন.

আরও পড়ুন

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

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

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