OOP সিরিজ

Encapsulation: তোমার bKash balance কেউ সরাসরি ছুঁতে পারে না

তোমার bKash-এ টাকা আছে।

কিন্তু ভাবো একটু। সেই টাকাটা আসলে কোথায় আছে? কোনো server-এ। কোনো database-এ। একটা variable-এ। হয়তো balance = 5420.50

তুমি কি সেই variable-এ সরাসরি হাত দিতে পারো? গিয়ে লিখতে পারো balance = 999999?

পারো না।

তোমাকে নির্দিষ্ট পথ দিয়ে আসতে হবে। টাকা পাঠাতে হলে “Send Money” তে যাও। বের করতে হলে “Cash Out”। জানতে হলে “Check Balance”। PIN ছাড়া কিছুই হবে না। আর ভেতরে কীভাবে সব হচ্ছে সেটা তুমি জানো না, জানার দরকারও নেই।

এই ব্যবস্থার নাম Encapsulation।


১. Encapsulation কী?

সহজ করে বললে: Encapsulation = Data Hiding + Controlled Access।

একটা class-এর ভেতরে data (variables) আর behavior (methods) একসাথে রাখা, এবং সেই data-তে সরাসরি access না দিয়ে শুধু নির্দিষ্ট পথ দিয়ে access দেওয়া।

bKash-এর ATM analogy ভাবো। তুমি সরাসরি bank-এর vault-এ ঢুকে balance বদলাতে পারো না। ATM-এর মাধ্যমে যাও। ATM তোমাকে তিনটাই দেয়: deposit(), withdraw(), get_balance()। ভেতরে কী হচ্ছে? সেটা তোমার ব্যাপার না।

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F5F5F5', 'primaryTextColor': '#000', 'primaryBorderColor': '#333', 'lineColor': '#333', 'background': '#fff'}}}%% graph LR classDef box fill:#F5F5F5,color:#000,stroke:#333 U["You (User)"]:::box A["Public Interface deposit() withdraw() getBalance()"]:::box V["Private Data balance account_number pin_hash"]:::box U -->|"only through here"| A A -->|"controlled access"| V U -.->|"direct access blocked"| V

Encapsulation কেন দরকার?

প্রথমত, sensitive data লুকিয়ে রাখা যায়। Balance, password, card number সরাসরি দেখা যাবে না।

দ্বিতীয়ত, controlled validation। কেউ -500 টাকা deposit করতে চাইলে আটকানো যাবে। Method-এর ভেতরে rule থাকবে।

তৃতীয়ত, maintainability। ভেতরের implementation বদলালেও বাইরের code ভাঙবে না। bKash চাইলে তাদের balance integer থেকে float-এ বদলাতে পারে, তুমি টেরও পাবে না।


২. Access Modifiers: কতটুকু দেখাবে?

Encapsulation implement করার মূল হাতিয়ার হলো Access Modifiers। এগুলো নিয়ন্ত্রণ করে class-এর কোন অংশ বাইরে থেকে দেখা যাবে।

তিনটা মূল modifier:

private: শুধু ওই class-এর ভেতরে। সবচেয়ে বেশি ব্যবহার হয় data লুকাতে। বাইরে থেকে কেউ ছুঁতে পারবে না।

protected: ওই class আর তার subclass-এ। Inheritance-এ কাজে লাগে।

public: সবাই দেখতে পারে। এটাই controlled interface।

সহজ নিয়ম: সব কিছু default-এ private রাখো, দরকার হলে public করো।

class DigitalWallet:
    def __init__(self, owner, initial_balance):
        self.__owner = owner              # private — not visible outside
        self.__balance = initial_balance  # private — cannot be changed directly
        self.__pin = None                 # private

    def get_balance(self):               # public — read only
        return self.__balance

    def deposit(self, amount):           # public — controlled access
        if amount <= 0:
            print("Invalid amount")
            return False
        self.__balance += amount
        return True

    def withdraw(self, amount):          # public — controlled access
        if amount <= 0:
            print("Invalid amount")
            return False
        if amount > self.__balance:
            print("Insufficient balance")
            return False
        self.__balance -= amount
        return True

Python-এ __ দিয়ে private করলে সরাসরি access block হয়ে যায়। দেখো কী হয়:

wallet = DigitalWallet("Raian", 1000)

# correct path
print(wallet.get_balance())   # 1000
wallet.deposit(500)
print(wallet.get_balance())   # 1500

# attempt direct access
wallet.__balance = 999999     # this won't work!
print(wallet.get_balance())   # still 1500

৩. Getters এবং Setters: নিয়ন্ত্রিত দরজা

Private data পড়তে এবং লিখতে যে public methods ব্যবহার হয় সেগুলোকে বলে Getter আর Setter।

Getter: শুধু পড়তে দেয়। get_balance() balance দেখায়, কিন্তু বদলাতে দেয় না।

Setter: লিখতে দেয়, কিন্তু validation সহ। Invalid value ঢুকতে পারে না।

class BankAccount:
    def __init__(self):
        self.__balance = 0
        self.__account_holder = ""

    # Getter
    def get_balance(self):
        return self.__balance

    # Getter
    def get_account_holder(self):
        return self.__account_holder

    # Setter with validation
    def set_account_holder(self, name):
        if not name or len(name.strip()) == 0:
            raise ValueError("Account holder name cannot be empty")
        self.__account_holder = name.strip()

    # Business method with validation
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self.__balance += amount
        print(f"Deposited {amount}. New balance: {self.__balance}")

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self.__balance:
            raise ValueError("Insufficient funds")
        self.__balance -= amount
        print(f"Withdrew {amount}. New balance: {self.__balance}")

Setter-এর কারণে account_holder = "" বা balance = -500 কখনো set হবে না।


বাস্তব উদাহরণ: Payment Processor

এই example-টা সবচেয়ে শক্তিশালী।

তুমি একটা payment system বানাচ্ছ। Customer-এর credit card number system-এ রাখতে হবে। কিন্তু পুরো number কখনো store করা যাবে না, এটা security risk। শুধু masked version রাখতে হবে: ****-****-****-1234

Encapsulation এই সমস্যার perfect সমাধান:

class PaymentProcessor:
    def __init__(self, card_number: str, amount: float):
        # raw card number is never stored
        # masked immediately in the constructor
        self.__masked_card = self.__mask_card(card_number)
        self.__amount = amount
        self.__is_processed = False

    def __mask_card(self, card_number: str) -> str:
        # private method — cannot be called from outside
        digits_only = card_number.replace("-", "").replace(" ", "")
        return f"****-****-****-{digits_only[-4:]}"

    def process_payment(self) -> bool:
        if self.__is_processed:
            print("This payment has already been processed.")
            return False

        print(f"Processing ${self.__amount} via card {self.__masked_card}...")
        self.__is_processed = True
        print("Payment successful!")
        return True

    def get_masked_card(self) -> str:
        return self.__masked_card

    def get_amount(self) -> float:
        return self.__amount
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F5F5F5', 'primaryTextColor': '#000', 'primaryBorderColor': '#333', 'lineColor': '#333', 'background': '#fff'}}}%% graph TD classDef box fill:#F5F5F5,color:#000,stroke:#333 IN["Input 4111-1111-1111-1234 $500"]:::box PP["PaymentProcessor (constructor runs)"]:::box PRIV["Private Data __masked_card: ****-****-****-1234 __amount: 500 __is_processed: False"]:::box PUB["Public Methods process_payment() get_masked_card() get_amount()"]:::box IN -->|"create"| PP PP -->|"masks and stores"| PRIV PP --> PUB
processor = PaymentProcessor("4111-1111-1111-1234", 500)

# correct path
processor.process_payment()
# Processing $500 via card ****-****-****-1234...
# Payment successful!

print(processor.get_masked_card())  # ****-****-****-1234

# raw card number is never accessible
# processor.__mask_card() won't work (private method)

এই design-এর সৌন্দর্য কোথায়? Raw card number object-এ ঢোকার সাথে সাথে mask হয়ে যায়। এরপর যদি কেউ object inspect করে, log করে, বা debug করে, শুধু masked version দেখবে। Original number চিরতরে চলে গেছে।


সারসংক্ষেপ

গল্পের ভাষায় প্রযুক্তির ভাষায়
bKash balance সরাসরি বদলানো যায় না Private variable
ATM-এর তিনটা button Public methods (controlled interface)
PIN ছাড়া কিছু হয় না Access modifier: private
Cashier নিজে জানে কিন্তু বলে না Getter without setter
Invalid amount deposit ঠেকানো Setter with validation
Card number mask করে রাখা Encapsulation for security

Data private রাখো, access দাও শুধু method-এর মাধ্যমে।

Method-এর ভেতরে validation রাখলে invalid state কখনো তৈরি হয় না।

Implementation বদলালেও বাইরের code ভাঙবে না, এটাই maintainability।


পরবর্তী প্রশ্ন: Encapsulation detail লুকায়। কিন্তু একটা class-এর পুরো complexity যদি লুকিয়ে, শুধু essential অংশটুকু দেখাতে পারতাম? সেই ধারণার নাম Abstraction।

OOP সিরিজের পরবর্তী পর্ব: Abstraction, জটিলতা লুকানোর শিল্প