সরাসরি পর্বে যান
পর্ব ৩১ · ৭০-এর মধ্যে · মডিউল ৫

Interface আর contract — design by contract, error handling আর immutability

Interfaces & Contracts — Design by Contract, Error Handling, Immutability, Idempotency & Backward Compatibility

Taught in Bangla, this lesson treats interfaces as contracts: designing small interfaces that are hard to misuse, design by contract with preconditions, postconditions and invariants and who is at fault when one breaks, assertions versus always-on validation in Python and JavaScript, choosing exceptions, error codes or Result values, immutability and defensive copies, idempotent operations, alternatives to null, and backward-compatible interface changes.

এই পর্বে যা শিখবেন

  • Interface-কে চুক্তি হিসেবে দেখা, আর ছোট, সুসংহত, ভুলভাবে ব্যবহার করা কঠিন — এমন interface নকশা করা
  • যেকোনো method-এর precondition, postcondition, invariant আর old মান লিখতে পারা, আর contract ভাঙলে দায় কার তা বলা
  • Python আর JavaScript-এ assert, সবসময়-চলা check, type hint, Protocol আর decorator দিয়ে contract বসানো
  • Exception, error code আর Result-এর মধ্যে বেছে নেওয়া, আর ভালো error বার্তা লেখা
  • Immutability, defensive copy, idempotency, null-এর বিকল্প আর backward-compatible পরিবর্তনের কৌশল

১ · Interface মানে চুক্তি — “কী” আর “কীভাবে” আলাদা

বাড়ির দেয়ালের বিদ্যুতের সকেটের কথা ভাবুন। ফ্যান, ফোনের charger বা ইস্ত্রি — যার plug সকেটে আঁটে, সেটিই চলে। সকেট জানে না কোন যন্ত্র লাগানো হলো; যন্ত্রও জানে না বিদ্যুৎ এল কোন কেন্দ্র থেকে, সৌর প্যানেল না জেনারেটর থেকে। দুই পক্ষ শুধু একটি চুক্তি মানে: এত volt, এই আকারের পিন। এই চুক্তিটিই interface.

Software-এ interface হলো একটি module, class বা service বাইরের জগৎকে যা কথা দেয় — কোন operation আছে, প্রতিটি কী নেয়, কী ফেরত দেয়, কখন ব্যর্থ হয়। ভেতরে সেটি কীভাবে করা হলো, তা implementation. পর্ব ২৫-এ (modularity, coupling আর cohesion) দেখেছেন, David Parnas ১৯৭২ সালে বলেছিলেন module ভাগ করুন এমনভাবে, যাতে বদলাতে পারে এমন সিদ্ধান্তগুলো ভেতরে লুকানো থাকে — information hiding. Interface সেই লুকানোর দেয়াল; যা দেয়ালের বাইরে দেখা যায়, শুধু সেটুকুই চুক্তি।

দর্জির দোকানে আপনি মাপ দেন — বুক, কাঁধ, হাতা। দর্জি কথা দেন — ঈদের আগে এই মাপের পাঞ্জাবি। কাপড় কাটবেন কাঁচি না মেশিনে, সেলাই করবেন নিজে না কারিগর দিয়ে — সেটি তাঁর ব্যাপার (implementation). মাপ ভুল দিলে পাঞ্জাবি না আঁটলে দোষ আপনার; ঠিক মাপ দেওয়ার পরও না আঁটলে দোষ দর্জির। এই পর্বের প্রায় সব ধারণা এই দোকানেই আছে।

Program to an interface, not an implementation

১৯৯৪ সালের Design Patterns বইয়ে Gamma, Helm, Johnson আর Vlissides (Gang of Four) নকশার একটি মূল নীতি লেখেন: “Program to an interface, not an implementation.” অর্থাৎ variable, parameter আর field-এর type হিসেবে নির্দিষ্ট class নয়, বরং চুক্তিটুকু ব্যবহার করুন। এই নীতিটি program to an interface নামে পরিচিত।

from typing import Protocol

class Notifier(Protocol):                 # the contract: WHAT a notifier can do
    def send(self, phone: str, text: str) -> bool: ...

class SmsNotifier:                        # one HOW (no inheritance needed)
    def send(self, phone: str, text: str) -> bool:
        print(f"SMS to {phone}: {text}")
        return True

def remind_due(members: list[str], notifier: Notifier) -> int:
    return sum(notifier.send(p, "Kisti due on the 10th") for p in members)

একটি কাল্পনিক সমবায় সমিতির সঞ্চয় app-এর কিস্তি মনে করিয়ে দেওয়ার function remind_due শুধু জানে “যার send(phone, text) আছে”। আগামী বছর SMS-এর বদলে app notification বা email এলে remind_due-এর একটি অক্ষরও বদলাতে হবে না; test-এ একটি নকল notifier দিলেই চলবে (পর্ব ৪৯-এ test double). Python-এর typing.Protocol (PEP 544, Python ৩.৮) এই চুক্তিকে type checker-এর কাছে লিখে রাখে — class-টিকে Notifier থেকে উত্তরাধিকার নিতে হয় না, শুধু আকার মিললেই হয়; একে বলে structural typing. Java বা C#-এ একই কাজ করে interface keyword, যেখানে class-কে স্পষ্টভাবে implements লিখতে হয় (nominal typing). বিস্তারিত দেখুন Python কোর্সের ABC, duck typing আর Protocol আর Java কোর্সের abstract class আর interface পর্বে।

মূল ধারণা

Method-এর নাম আর parameter-এর type হলো চুক্তির আকার (signature). কিন্তু আকার পুরো চুক্তি নয়। withdraw(amount: int) -> int বলে না: amount ঋণাত্মক হলে কী হবে, ব্যালান্সের বেশি চাইলে কী হবে, ফেরত মানটি কী — নতুন ব্যালান্স, না তোলা টাকা? এই আচরণের অংশটুকু লেখার পদ্ধতিই contract, আর এই পর্বের বাকিটা তা নিয়েই।

২ · ভালো interface-এর নকশা — ছোট, সুসংহত, ভুল করা কঠিন

একবার interface প্রকাশ হলে অন্যরা তার ওপর কোড লেখেন, তাই ভুল interface বদলানো ভেতরের কোড বদলানোর চেয়ে অনেক দামি। কয়েকটি পরীক্ষিত নির্দেশনা:

  • ছোট আর সুসংহত। একটি interface একটি দায়িত্বের কথা বলবে। বিশটি method-এর “সব কাজের” interface সবাইকে এমন method-এর ওপর নির্ভর করায়, যা তাঁরা ব্যবহারই করেন না — পর্ব ২৮-এর interface segregation (SOLID ২) আর পর্ব ২৫-এর cohesion ঠিক এই কথা বলে।
  • ঠিকভাবে ব্যবহার সহজ, ভুলভাবে ব্যবহার কঠিন। Scott Meyers-এর নামে প্রচলিত একটি পরামর্শ (তাঁর Effective C++-এর পরের সংস্করণগুলোতে আর একটি প্রবন্ধে এভাবে লেখা বলে উদ্ধৃত হয়): “Make interfaces easy to use correctly and hard to use incorrectly.” ভুল হলে দোষ ব্যবহারকারীর নয়, interface-এর নকশার — এই দৃষ্টিভঙ্গিটাই মূল।
  • নাম দিয়ে অর্থ। process(data) কিছুই বলে না; reserve_seat(trip_id, seat_no) বলে। কিছু ফেরত দেওয়া প্রশ্ন (balance()) আর অবস্থা বদলানো আদেশ (withdraw()) আলাদা রাখার নীতি — পর্ব ২৬-এর command-query separation (নকশার নীতি) — নামকেও সৎ রাখে।
  • এক ধরনের error. একই library-র একটি function ব্যর্থ হলে None, আরেকটি -1, তৃতীয়টি exception দিলে ব্যবহারকারীকে প্রতিটির নিয়ম মুখস্থ করতে হয়। পুরো interface-এ একটি নিয়ম রাখুন (§৬)।
  • একক আর সীমা type-এ বা নামে। set_timeout(30) — সেকেন্ড না মিলিসেকেন্ড? set_timeout(seconds=30) বা একটি Duration type এই প্রশ্ন মুছে দেয়। ১৯৯৯ সালের Mars Climate Orbiter-এর ঘটনা (পর্ব ০২, software-এর ইতিহাস) মনে করিয়ে দেয়, একক নিয়ে অস্পষ্ট interface কতটা দামি হতে পারে।
ভুল করা সহজসমস্যাভুল করা কঠিন
transfer(a, b, 500)কে পাঠাচ্ছেন, কে পাচ্ছেন — ক্রম উল্টে গেলেও চলেtransfer(from_acc=a, to_acc=b, amount=Taka(500))
book(trip, 7, True, False)দুটি bool-এর অর্থ শুধু documentation-এbook(trip, seat=7, berth=Berth.LOWER)
open_account() তারপর init() না ডাকলে crashক্রম মনে রাখার দায় ব্যবহারকারীরConstructor-ই সম্পূর্ণ, বৈধ object বানায়
get_members() ভেতরের list ফেরত দেয়বাইরে থেকে বদলালে ভেতরের অবস্থা নষ্টTuple বা কপি ফেরত দেওয়া (§৭)

Interface “সবার জন্য সব” বানানোর লোভ সামলান। প্রতিটি নতুন parameter, flag আর method চিরকালের প্রতিশ্রুতি — পরে সরাতে গেলে breaking change. পর্ব ২৬-এর YAGNI এখানে দ্বিগুণ প্রযোজ্য: ভেতরের কোডে বাড়তি জিনিস শুধু জট বাড়ায়, কিন্তু public API-তে বাড়তি জিনিস হলো ভবিষ্যতের ঋণ।

৩ · Design by contract — precondition, postcondition আর invariant

ফরাসি কম্পিউটার বিজ্ঞানী Bertrand Meyer ১৯৮০-র দশকের মাঝামাঝি Eiffel নামে একটি object-oriented ভাষা নকশা করেন, যার কেন্দ্রে একটি ধারণা: প্রতিটি class আর method তার ব্যবহারকারীর সাথে একটি ব্যবসায়িক চুক্তির মতো সম্পর্কে থাকে। তিনি এর নাম দেন design by contract (DbC); ধারণাটি বিস্তারিত আসে তাঁর বই Object-Oriented Software Construction-এ (প্রথম সংস্করণ ১৯৮৮, দ্বিতীয় ১৯৯৭)। এর শিকড় আরও পুরোনো — ১৯৬৯ সালে C. A. R. Hoare-এর Hoare triple {P} S {Q}: P সত্য অবস্থায় S চালালে শেষে Q সত্য হবে। Meyer এই গাণিতিক চিন্তাকে প্রতিদিনের কোডে নামিয়ে আনেন। (“Design by Contract” শব্দবন্ধটি Eiffel Software-এর trademark হিসেবেও নিবন্ধিত বলে জানা যায়, তাই অনেক লেখায় “contract programming” বলা হয়।)

চুক্তির তিনটি অংশ:

📥 Precondition · require

ডাকার আগে যা সত্য থাকতেই হবে। এটি মানা caller-এর দায়। যেমন: amount > 0 আর amount ≤ balance.

দর্জিকে ঠিক মাপ দেওয়া

📤 Postcondition · ensure

কাজ শেষে যা সত্য হবে বলে কথা দেওয়া। এটি রক্ষা supplier-এর দায়। যেমন: balance = old balance − amount.

মাপমতো পাঞ্জাবি

🧱 Class invariant

Object-এর প্রতিটি বাইরে-থেকে-দেখা অবস্থায় যা সবসময় সত্য। যেমন: balance ≥ 0. সব public method-এর শুরু আর শেষে এটি ধরে রাখা class-এর দায়।

দোকানের স্থায়ী নিয়ম

Precondition, postcondition আর class invariant — তিনটিই সত্য-মিথ্যা প্রকাশ (boolean expression). Postcondition প্রায়ই কাজের আগের অবস্থার সাথে তুলনা করে; Eiffel-এ এজন্য আছে old keyword: old balance মানে method শুরুর মুহূর্তে balance-এর মান। এই old value ছাড়া “ঠিক amount টাকাই কমেছে” লেখা যায় না। পর্ব ২২-এ (use case আর user story) use case-এর precondition আর guarantee দেখেছিলেন — চিন্তাটি একই, কিন্তু সেখানে পুরো ব্যবহারকারী-লক্ষ্যের স্তরে; এখানে একটি method-এর স্তরে, কোডে যাচাই করা যায় এমনভাবে।

class ACCOUNT
feature
    balance: INTEGER

    withdraw (amount: INTEGER)
        require
            positive: amount > 0
            enough_money: amount <= balance
        do
            balance := balance - amount
        ensure
            deducted: balance = old balance - amount
        end
invariant
    never_negative: balance >= 0
end

প্রতিটি শর্তের একটি নাম (positive, enough_money) আছে — ভাঙলে error বার্তায় সেই নাম আসে। নিচের চিত্রটি Meyer-এর বইয়ে ব্যবহৃত ধরনের একটি সারণি: চুক্তিতে প্রতিটি পক্ষের একটি দায়িত্ব আর একটি লাভ, আর এক পক্ষের দায়িত্বই অন্য পক্ষের লাভ।

withdraw(amount)-এর চুক্তি — দুই পক্ষের দায় আর লাভ

Obligations and benefits of client and supplier (after Meyer)

Design by contract: দায় আর লাভের সারণি দায়িত্ব (obligation) লাভ (benefit) Client (caller) Supplier (withdraw) precondition মানা amount > 0 amount ≤ balance ডাকার আগে যাচাই postcondition পাওয়া balance ঠিক amount টাকাই কমেছে ফলের ওপর ভরসা postcondition দেওয়া balance = old balance − amount invariant রাখা balance ≥ 0 precondition ধরে নেওয়া ভুল amount নিয়ে ভাবতে হয় না কোড ছোট, একবার মাত্র যাচাই precondition ভাঙল → caller-এর bug postcondition বা invariant ভাঙল → supplier-এর bug
ছবি ৩১.১ — চুক্তির সারণি। Caller-এর দায়িত্ব (precondition) supplier-এর লাভ: supplier ধরে নিতে পারে amount বৈধ। Supplier-এর দায়িত্ব (postcondition আর invariant) caller-এর লাভ: caller ফলের ওপর ভরসা করতে পারেন। কমলা তীর এই আদান-প্রদান দেখায়; নিচের দুই লাইন দায় নির্ধারণের নিয়ম।

একটি চুক্তি-যাচাই করা ডাক ঠিক কোন ক্রমে চলে, নিচের stepper-এ withdraw(300) দিয়ে দেখুন। শুরুতে balance ১০০০ টাকা।

1withdraw(amount):2  require 0 < amount ≤ balance3  old_balance = balance4  balance = balance - amount5  ensure balance == old_balance - amount6  invariant balance ≥ 0
অবস্থা (Watch)
amount
balance
old balance
যাচাই
দায়
  1. ডাক এল। সমিতির app withdraw(300) ডাকল; balance ১০০০. এখনো কোনো কোড চলেনি — আগে চুক্তি দেখা হবে।
  2. Precondition যাচাই। ৩০০ > ০ আর ৩০০ ≤ ১০০০ — দুটিই সত্য। Caller তাঁর দায়িত্ব পালন করেছেন; এখান থেকে supplier ধরে নিতে পারে amount বৈধ।
  3. Old মান রাখা। Body চলার আগেই balance-এর ছবি তুলে রাখা হলো: old balance = ১০০০. এটি না রাখলে পরে “কতটা কমল” মাপার উপায় থাকত না।
  4. Body চলল। balance হলো ৭০০. এই মুহূর্তে কেউ জানে না কাজটি ঠিক হয়েছে কি না — implementation-এ ভুল থাকতেই পারে।
  5. Postcondition যাচাই। ৭০০ == ১০০০ − ৩০০ — সত্য। Supplier কথা রেখেছে। এখানে মিথ্যা হলে দোষ পুরোপুরি withdraw-এর কোডের, কারণ precondition তো মানা হয়েছিল।
  6. Invariant যাচাই আর ফেরত। ৭০০ ≥ ০ — সত্য; object বৈধ অবস্থায় আছে। ফল ফেরত গেল: ৭০০. Eiffel-এ invariant প্রতিটি বাইরের ডাকের শুরু আর শেষে দেখা হয়, ভেতরের সহায়ক ডাকের মাঝপথে নয় — কাজের মাঝখানে অল্প সময়ের জন্য নিয়ম ভাঙা অবস্থা থাকতে পারে।

৪ · Contract ভাঙলে দোষ কার — আর কোডে তা ধরা

DbC-এর সবচেয়ে কাজের দিক হলো দায় ভাগ করা। একটি contract ভাঙা মানে সবসময় একটি bug — ব্যবহারকারীর ভুল input বা network বন্ধ হওয়া নয়, বরং কোনো programmer-এর ভুল। কোন শর্ত ভাঙল দেখে সরাসরি বলা যায় কার কোড ঠিক করতে হবে। নিচের stepper ছয়টি পরিস্থিতি একে একে দেখায়।

  1. Callerwithdraw(x)ডাকার পক্ষ
  2. Prerequirecaller-এর দায়
  3. Bodydoimplementation
  4. Post · Invensuresupplier-এর দায়
পরিস্থিতি (সমিতির app, কাল্পনিক)
ঘটনা
কোন শর্ত
দায়
সংশোধন
  1. বেশি টাকা চাওয়া। Balance ১০০০, caller চাইলেন ৫০০০. Precondition amount ≤ balance মিথ্যা। withdraw-এর কোডে কোনো ভুল নেই — caller চুক্তি না মেনে ডেকেছেন। সংশোধন caller-এর দিকে।
  2. ঋণাত্মক amount. UI-র হিসাবে চিহ্ন উল্টে withdraw(-50) এল — ধরা না পড়লে টাকা তোলার বদলে জমা হয়ে যেত! Precondition amount > 0 ঠিক এই ভুলটি সীমানাতেই থামায়; দায় আবার caller-এর।
  3. Subclass-এর গোপন ফি। Precondition মানা হয়েছে, কিন্তু balance কমল ৩০৫ — postcondition মিথ্যা। দায় implementation-এর। পর্ব ২৮-এর Liskov substitution-এর ভাষায়, subclass parent-এর postcondition দুর্বল করেছে; Eiffel-এ redefine করা method শুধু precondition দুর্বল (require else) আর postcondition শক্ত (ensure then) করতে পারে।
  4. Invariant ভাঙা। একটি ভেতরের helper ভুল হিসাবে balance −৫ করে ফেলল। কোনো caller ভুল করেননি; class নিজের স্থায়ী নিয়ম রাখেনি। দায় class-এর লেখকের — এ ধরনের bug খুঁজতেই invariant সবচেয়ে কাজের।
  5. যাচাই-অযোগ্য precondition. চুক্তিতে লেখা “অ্যাকাউন্ট লক থাকলে ডাকবেন না”, কিন্তু caller লক আছে কি না জানার কোনো উপায় নেই। Meyer-এর নিয়মে precondition-এ এমন কিছু থাকা উচিত নয়, যা caller নিজে যাচাই করতে পারেন না। দায় চুক্তির নকশাকারীর — is_locked() যোগ করুন, নয়তো শর্তটি precondition থেকে সরিয়ে supplier-এর সামলানো ব্যর্থতা হিসেবে লিখুন।
  6. প্রত্যাশিত ব্যর্থতা — contract ভাঙা নয়। Server সাড়া না দেওয়া কারও bug নয়; সবকিছু ঠিক থাকলেও এটি ঘটতে পারে। এটি contract-এর অংশ হিসেবেই লেখা থাকা উচিত (“network না পেলে BankUnavailable তোলে”), আর এখানে §৬-এর error handling কাজে লাগে। মনে রাখুন: contract violation = bug; প্রত্যাশিত ব্যর্থতা = চুক্তির অংশ।

Python-এ একটি contract decorator

Eiffel ছাড়া অন্য ভাষায় এই যাচাই নিজে বসাতে হয়। নিচের প্রোগ্রামে একটি decorator @contract(pre=…, old=…, post=…) method-কে মুড়ে দেয়: আগে precondition, তারপর old মানের ছবি, তারপর আসল কাজ, শেষে postcondition. দুই ধরনের ভাঙনের জন্য দুটি আলাদা exception class — নাম দেখেই দায় বোঝা যায়। একই WITHDRAW চুক্তি parent আর subclass দুটিতেই বসানো, কারণ চুক্তিটি interface-এর, কোনো একটি implementation-এর নয়।

import functools, inspect
class ContractError(Exception): pass
class PreconditionError(ContractError): pass    # blame: the caller
class PostconditionError(ContractError): pass   # blame: the supplier (implementation)
def contract(pre=None, old=None, post=None):
    def wrap(fn):
        sig = inspect.signature(fn)
        @functools.wraps(fn)
        def inner(*args, **kwargs):
            a = sig.bind(*args, **kwargs).arguments
            call = f"{type(a['self']).__name__}.{fn.__name__}(amount={a.get('amount')})"
            if pre and not pre(**a):
                raise PreconditionError(f"caller broke pre of {call}")
            snapshot = old(**a) if old else None      # 'old' value, taken BEFORE the body runs
            result = fn(*args, **kwargs)
            if post and not post(snapshot, result, **a):
                raise PostconditionError(f"supplier broke post of {call}")
            return result
        return inner
    return wrap

WITHDRAW = contract(pre=lambda self, amount: 0 < amount <= self.balance,
                    old=lambda self, amount: self.balance,
                    post=lambda old, result, self, amount: self.balance == old - amount == result)
class Account:
    def __init__(self, balance): self.balance = balance
    @WITHDRAW
    def withdraw(self, amount): self.balance -= amount; return self.balance
class FeeAccount(Account):                      # a "clever" subclass with a hidden fee
    @WITHDRAW
    def withdraw(self, amount): self.balance -= amount + 5; return self.balance

for acct, amt in [(Account(1000), 300), (Account(1000), 5000), (FeeAccount(1000), 300)]:
    try: print(type(acct).__name__, amt, "->", acct.withdraw(amt))
    except ContractError as e: print(f"{type(e).__name__}: {e} | balance {acct.balance}")

আউটপুট:

Account 300 -> 700
PreconditionError: caller broke pre of Account.withdraw(amount=5000) | balance 1000
PostconditionError: supplier broke post of FeeAccount.withdraw(amount=300) | balance 695

প্রথম লাইনে সব শর্ত মানা হয়েছে, ফল ৭০০. দ্বিতীয় লাইনে precondition (5000 <= 1000) মিথ্যা — body চলেইনি, তাই balance অটুট ১০০০, আর exception-এর নাম বলছে দোষ caller-এর। তৃতীয় লাইনে precondition মানা হয়েছিল, কিন্তু balance কমল ৩০৫ (১০০০ → ৬৯৫), তাই PostconditionError — দোষ FeeAccount-এর implementation-এর। একটি গুরুত্বপূর্ণ খুঁটিনাটি: postcondition ধরা পড়ে কাজ হয়ে যাওয়ার পরে, তাই balance ৬৯৫-ই থেকে গেছে। Contract ভুল ধরে, নিজে থেকে আগের অবস্থায় ফেরায় না — টাকার মতো ক্ষেত্রে transaction আর rollback (DBMS কোর্সে) আলাদা দরকার। inspect.signature(...).bind positional আর keyword — দুই ধরনের argument-কেই নাম ধরে সাজায়, তাই lambda-গুলো self আর amount নামে মান পায়।

৫ · Eiffel ছাড়া contract — assert, validation আর defensive programming

বেশিরভাগ ভাষায় contract-এর জন্য আলাদা keyword নেই; চারটি সাধারণ হাতিয়ার দিয়ে কাজ চলে:

  1. Assertion: assert শর্ত, "বার্তা" — শর্ত মিথ্যা হলে AssertionError. ভেতরের invariant আর “এমন হওয়ার কথাই নয়” ধরনের অনুমান লেখার জন্য চমৎকার। কিন্তু Python-কে -O flag দিয়ে চালালে (বা PYTHONOPTIMIZE environment variable দিলে) সব assert statement compile-এর সময়েই বাদ পড়ে, আর __debug__ হয়ে যায় False. C-তে NDEBUG দিলে assert একইভাবে উধাও হয়, আর Java-তে assertion ডিফল্টভাবে বন্ধই থাকে (-ea দিয়ে চালু করতে হয়)।
  2. সবসময়-চলা check: if not …: raise ValueError(…) — flag যা-ই হোক, চলবেই। বাইরে থেকে আসা data যাচাইয়ের (input validation) একমাত্র নিরাপদ পথ।
  3. Type hint আর Protocol: amount: int বা §১-এর Notifier — mypy বা pyright-এর মতো checker চালানোর আগেই আকারের ভুল ধরে। কিন্তু Python চলার সময় type hint নিজে কিছু যাচাই করে না, আর “amount > 0”-এর মতো আচরণের শর্ত type-এ প্রকাশ করা যায় না।
  4. Contract decorator বা library: §৪-এর মতো নিজে লেখা, বা icontract, deal-এর মতো third-party library (নাম আর API নিজে যাচাই করে নিন)। কিছু ভাষায় এটি ভাষারই অংশ: Ada ২০১২-এর Pre/Post aspect, আর C++26-এর জন্য contract-এর প্রস্তাব গৃহীত হয়েছে বলে জানা যায়।

নিচের প্রোগ্রাম একই ছাড়ের (discount) function দুইভাবে লেখে — একটি assert দিয়ে input যাচাই করে (ভুল পথ), অন্যটি সবসময়-চলা check দিয়ে। -O flag-এর ওপর নির্ভর না করে, Python-এর built-in compile(..., optimize=1) দিয়ে একই কোড ঠিক সেভাবে compile করা হয়েছে, যেভাবে python -O করত।

SRC = '''
def discount_assert(price, pct):
    assert 0 <= pct <= 100, f"pct out of range: {pct}"   # assert used as input check (wrong)
    return price * (100 - pct) // 100

def discount_checked(price, pct):
    if not 0 <= pct <= 100:                               # real validation: always runs
        raise ValueError(f"pct must be 0..100, got {pct}")
    return price * (100 - pct) // 100
'''
for level in (0, 1):            # optimize=1 compiles the same code the way "python -O" would
    ns = {}
    exec(compile(SRC, "shop.py", "exec", optimize=level), ns)
    for name in ("discount_assert", "discount_checked"):
        try:
            print(f"optimize={level} {name}(500, 150) -> {ns[name](500, 150)}")
        except (AssertionError, ValueError) as e:
            print(f"optimize={level} {name}(500, 150) -> {type(e).__name__}: {e}")

def apply_coupon(cart_total, coupon):                     # internal invariant: a fine use of assert
    new_total = cart_total - min(coupon, cart_total)
    assert 0 <= new_total <= cart_total, "coupon logic is broken"
    return new_total
print("apply_coupon(300, 500) ->", apply_coupon(300, 500), "| __debug__ =", __debug__)

আউটপুট:

optimize=0 discount_assert(500, 150) -> AssertionError: pct out of range: 150
optimize=0 discount_checked(500, 150) -> ValueError: pct must be 0..100, got 150
optimize=1 discount_assert(500, 150) -> -250
optimize=1 discount_checked(500, 150) -> ValueError: pct must be 0..100, got 150
apply_coupon(300, 500) -> 0 | __debug__ = True

সাধারণভাবে (optimize=0) দুটি function-ই ১৫০% ছাড় আটকায়। কিন্তু optimize=1-এ assert-এর লাইনটি আর কোডেই নেই — ৫০০ টাকার জিনিসের দাম হয়ে গেল −২৫০, অর্থাৎ দোকান ক্রেতাকে টাকা দেবে! discount_checked দুই অবস্থাতেই একই আচরণ করে। শেষ লাইনে assert-এর ঠিক ব্যবহার: ৫০০ টাকার কুপন ৩০০ টাকার cart-এ দিলে মোট ০ — কুপনের হিসাব নিজের ভেতরের যুক্তিতে কখনো ঋণাত্মক বা আগের চেয়ে বেশি হওয়ার কথা নয়; হলে সেটি আমাদের কোডের bug. এই assert মুছে গেলেও প্রোগ্রাম ঠিক থাকে, শুধু একটি নিরাপত্তা-জাল কমে। নিয়মটি তাই: assert মুছে দিলে প্রোগ্রামের সঠিকতা যেন না বদলায়।

Defensive programming বনাম design by contract

Defensive programming বলে: প্রতিটি function প্রতিটি input সন্দেহ করবে, সব যাচাই করবে। DbC বলে উল্টো: precondition caller-এর দায়, supplier আবার যাচাই করে কোড ভারী করবে না (Meyer একে বলেন non-redundancy). দুই চিন্তার বিরোধ মেটে একটি ধারণায় — trust boundary:

কোথায়কী আসেকী দিয়ে যাচাইভাঙলে
Trust boundary: form, API request, file, অন্য দলের serviceবিশ্বাস করা যায় না এমন dataসবসময়-চলা validationস্পষ্ট error (৪০০ status, ValueError) — প্রত্যাশিত ঘটনা
ভেতরের module-এর মধ্যেনিজেদের কোড থেকে আসা মানPrecondition (assert বা contract check)Bug — যত দ্রুত সম্ভব থামা
Object-এর ভেতরনিজের অবস্থাInvariant আর postconditionBug — supplier-এর কোড ঠিক করা

নিরাপত্তার check — যেমন “এই ব্যবহারকারী কি এই অ্যাকাউন্টের মালিক?” — কখনো assert-এ লিখবেন না। -O দিয়ে চালানো একটি production server-এ সেই লাইন না থাকলে যে-কেউ যে-কারও টাকা তুলতে পারবেন। Bandit-এর মতো Python security linter assert-এর এমন ব্যবহারকে আলাদাভাবে চিহ্নিত করে।

৬ · Error handling — exception, error code আর Result

§৪-এর শেষ পরিস্থিতি — server সাড়া দিচ্ছে না — contract ভাঙা নয়, তবু caller-কে জানাতে হবে। ব্যর্থতা জানানোর তিনটি প্রধান পথ আছে, প্রত্যেকটির নিজস্ব দাম:

পথকীভাবেসুবিধাঝুঁকি
Exceptionraise/throw; কেউ catch না করা পর্যন্ত ডাকের শিকল বেয়ে ওপরে ওঠেমাঝের function-গুলোকে কিছু লিখতে হয় না; উপেক্ষা করা কঠিনSignature দেখে বোঝা যায় না কী তুলতে পারে; লুকানো control flow
Error codeবিশেষ ফেরত মান: -1, NULL, C-র errnoসরল, খরচ কম, C-তে প্রচলিতচুপচাপ উপেক্ষা করা খুব সহজ; বৈধ মানের সাথে গুলিয়ে যায়
Result / Eitherফেরত মান নিজেই “সফল + মান” বা “ব্যর্থ + কারণ” (Rust-এর Result, Haskell-এর Either)Type-এ লেখা থাকে ব্যর্থ হতে পারে; সব error জমিয়ে রাখা সহজপ্রতিটি ধাপে হাতে পার করতে হয়; ভাষার সমর্থন না থাকলে কোড লম্বা

Go মাঝামাঝি পথ নেয় — function দুটি মান ফেরত দেয়, value, err, আর caller-কে if err != nil লিখতে হয়। নিচের প্রোগ্রাম একটি কাল্পনিক দোকানের পণ্য-তালিকা import-এ দুই পথ পাশাপাশি দেখায়: প্রতিটি লাইন নাম,পরিমাণ,দাম; তিনটি ধাপ — পরিমাণ পড়া, দাম পড়া, সীমা (সর্বোচ্চ ১০) যাচাই।

// Fictional shop import: "name,qty,price" lines -> order total
const lines = ["rice,2,60", "oil,x,180", "sugar,1,-5", "salt,3,20", "dal,40,95"];

// Style A: exceptions — one try around the whole batch
function parseA(line) {
  const [name, q, p] = line.split(",");
  if (!/^\d+$/.test(q)) throw new Error(`bad qty "${q}"`);
  if (!(Number(p) > 0)) throw new Error(`bad price "${p}"`);
  if (Number(q) > 10) throw new Error(`qty ${q} over limit 10`);
  return { name, qty: Number(q), price: Number(p) };
}
let totalA = 0, doneA = 0;
try { for (const l of lines) { const it = parseA(l); totalA += it.qty * it.price; doneA++; } }
catch (e) { console.log(`A: stopped after ${doneA} line(s): ${e.message}; total ${totalA}`); }

// Style B: Result values — every stage returns Ok or Err
const Ok = value => ({ ok: true, value }), Err = error => ({ ok: false, error });
let skipped = 0;                          // stages an Err passed through untouched
const then = (r, f) => { if (r.ok) return f(r.value); skipped++; return r; };
const qty   = ([n, q, p]) => /^\d+$/.test(q) ? Ok([n, Number(q), p]) : Err(`bad qty "${q}"`);
const price = ([n, q, p]) => Number(p) > 0 ? Ok([n, q, Number(p)]) : Err(`bad price "${p}"`);
const limit = ([n, q, p]) => q <= 10 ? Ok({ name: n, qty: q, price: p }) : Err(`qty ${q} over limit 10`);
const results = lines.map(l => then(then(qty(l.split(",")), price), limit));

const good = results.filter(r => r.ok).map(r => r.value);
const bad = results.map((r, i) => r.ok ? null : `line ${i + 1}: ${r.error}`).filter(Boolean);
console.log(`B: ${good.length} ok, total ${good.reduce((s, it) => s + it.qty * it.price, 0)}`);
bad.forEach(m => console.log("  " + m));
console.log(`B: errors skipped ${skipped} later stage(s) on the way out`);

আউটপুট:

A: stopped after 1 line(s): bad qty "x"; total 120
B: 2 ok, total 180
  line 2: bad qty "x"
  line 3: bad price "-5"
  line 5: qty 40 over limit 10
B: errors skipped 3 later stage(s) on the way out

Style A-তে দ্বিতীয় লাইনের "x" exception তুলল, আর সেটি এক লাফে বাইরের catch-এ পৌঁছাল — মাঝের কোডকে কিছুই লিখতে হয়নি, কিন্তু বাকি চার লাইন আর দেখাই হলো না; total শুধু চালের ২ × ৬০ = ১২০. Style B-তে প্রতিটি ধাপ Ok বা Err ফেরত দেয়, আর then ব্যর্থ ফলকে পরের ধাপগুলো পার করিয়ে দেয় না-ছুঁয়ে। গোনা: লাইন ২ পরিমাণেই ব্যর্থ, তাই দাম আর সীমা — ২টি ধাপ এড়াল; লাইন ৩ দামে ব্যর্থ, সীমা — ১টি; লাইন ৫ শেষ ধাপে ব্যর্থ, ০টি। মোট ২ + ১ + ০ = ৩. ভালো লাইন দুটি (চাল ১২০ + লবণ ৬০) মিলে ১৮০, আর তিনটি error-ই লাইন নম্বরসহ একবারে পাওয়া গেল — import-এর মতো কাজে ব্যবহারকারী একবারে সব ভুল ঠিক করতে পারেন। Exception দিয়েও এটি সম্ভব (প্রতিটি লাইনে আলাদা try), কিন্তু Result-এ “ব্যর্থ হতে পারে” কথাটি ফেরত মানের আকারেই লেখা থাকে।

যে পথই নিন — পাঁচটি নিয়ম

  • Error কখনো চুপচাপ গিলবেন না। except Exception: pass বা খালি catch {} — bug লুকানোর সবচেয়ে নিশ্চিত উপায়, যাকে বলে swallowed exception. সামলাতে না পারলে ধরবেন না; ধরলে log করে, প্রসঙ্গ যোগ করে আবার তুলুন (Python-এ raise … from e, JavaScript-এ new Error(msg, { cause: e }))।
  • Fail fast. ভুল অবস্থা দেখামাত্র থামুন, যাতে ভুলটি যেখানে জন্মেছে তার কাছেই ধরা পড়ে — ভুল মান নিয়ে আরও দশটি function পার হয়ে database-এ পৌঁছানোর আগে। Fail fast-এর ধারণা পর্ব ২৬-এ দেখেছেন; Jim Shore-এর ২০০৪ সালের একটি IEEE Software প্রবন্ধ এই নামটিকে জনপ্রিয় করে বলে উদ্ধৃত হয়।
  • Error-এর বংশতালিকা। একটি মূল class (SavingsError), তার নিচে InsufficientFunds, AccountLocked, BankUnavailable. Caller চাইলে নির্দিষ্টটি ধরেন, চাইলে সবগুলো একসাথে। এই error hierarchy interface-এর অংশ — documentation-এ লিখুন।
  • Checked বনাম unchecked (Java). Java-তে checked exception (যেমন IOException) method-এর signature-এ throws দিয়ে ঘোষণা করতে হয়, আর caller-কে ধরতেই হয় বা আবার ঘোষণা করতে হয়; RuntimeException-এর উত্তরসূরিরা unchecked. সাধারণ চলতি নির্দেশনা: caller যা থেকে বাস্তবে সেরে উঠতে পারেন সেখানে checked, programming bug-এ unchecked. C#, Kotlin-এর মতো পরের ভাষাগুলো checked exception রাখেনি (ভাবনার প্রশ্ন ২)।
  • বার্তা মানুষের জন্য। “Error occurred” কিছুই বলে না। ভালো বার্তা বলে কী হলো, কোন মান নিয়ে, আর কী করা যায়: pct must be 0..100, got 150. পাসওয়ার্ড, token বা পুরো কার্ড নম্বর বার্তায় বা log-এ রাখবেন না। কোডের ভেতরে error সামলানোর লেখার ধরন পর্ব ৪১-এ (clean code) আরও দেখবেন; HTTP API-তে এক ছাঁচের error-এর নকশা দেখুন System Design-এর REST API নকশা পর্বে।

৭ · Immutability — যে মান বদলায় না, তাকে নিয়ে ভাবা সহজ

পরীক্ষার খাতার খসড়া পাতায় আপনি কাটাকুটি করেন; ফেয়ার কপিতে নয়। দোকানের ছাপানো রসিদও তাই — ভুল হলে রসিদ ঘষে বদলানো হয় না, নতুন রসিদ দেওয়া হয়। Immutability মানে object তৈরির পরে তার অবস্থা আর বদলায় না; “বদল” চাইলে নতুন object তৈরি হয়। এতে তিনটি বড় লাভ:

  • ভাবা সহজ: একটি Money(100) হাতে পেলে আপনি নিশ্চিত, অন্য কেউ পেছন থেকে এটিকে ৫০ বানাবে না। Invariant একবার constructor-এ যাচাই করলেই চিরকাল সত্য।
  • Concurrency-তে নিরাপদ: একাধিক thread একই data একসাথে পড়লে কোনো race condition হয় না, কারণ কেউ লিখছেই না — lock লাগে না।
  • নিরাপদে ভাগ করা আর key বানানো: Immutable মানকে dictionary-র key বা cache-এর চাবি বানানো যায়; Python-এ str, tuple, frozenset এজন্যই hashable.

Domain-driven design-এ (Eric Evans, ২০০৩) এমন মানকে বলে value object: পরিচয় নয়, মানই আসল — দুটি ১০০ টাকার নোট সমান, কোনটি কোনটি তাতে কিছু যায় আসে না। Python-এ @dataclass(frozen=True) (Python কোর্সের dataclass পর্ব), Java-তে record (Java-র record) এই কাজ সহজ করে। JavaScript-এ আছে Object.freeze — কিন্তু একটি বড় ফাঁদসহ।

// A frozen order from a fictional grocery app
const order = Object.freeze({ id: "A-17", total: 900,
  items: [{ sku: "rice", qty: 2 }], customer: { city: "Khulna" } });

order.total = 0;                          // sloppy mode: silently ignored
order.items.push({ sku: "oil", qty: 1 }); // nested array is NOT frozen
order.customer.city = "Dhaka";            // nested object is NOT frozen
console.log("shallow:", order.total, order.items.length, order.customer.city,
  Object.isFrozen(order), Object.isFrozen(order.items));

function deepFreeze(obj) {
  for (const v of Object.values(obj))
    if (v !== null && typeof v === "object" && !Object.isFrozen(v)) deepFreeze(v);
  return Object.freeze(obj);
}
const safe = deepFreeze({ id: "A-18", total: 450,
  items: [{ sku: "dal", qty: 1 }], customer: { city: "Sylhet" } });

safe.customer.city = "Dhaka";             // sloppy mode: silently ignored
try { safe.items.push({ sku: "egg", qty: 12 }); }   // push throws even in sloppy mode
catch (e) { console.log("push:", e.name); }
(function () {
  "use strict";                           // strict mode: every write to frozen data throws
  try { safe.total = 0; } catch (e) { console.log("strict:", e.name); }
})();
console.log("deep:", safe.total, safe.items.length, safe.customer.city,
  Object.isFrozen(safe.items[0]));

আউটপুট:

shallow: 900 2 Dhaka true false
push: TypeError
strict: TypeError
deep: 450 1 Sylhet true

প্রথম লাইনটিই ফাঁদ: order frozen (true), তাই total ৯০০-ই রইল — কিন্তু কোনো error ছাড়াই, চুপচাপ (sloppy mode-এ)। অথচ ভেতরের items array frozen নয় (false): তেল যোগ হয়ে item ২টি, আর শহর খুলনা থেকে ঢাকা হয়ে গেছে। Object.freeze shallow — শুধু প্রথম স্তর। deepFreeze নিচের প্রতিটি object আগে freeze করে তারপর নিজেকে; ফলে শহর বদল চুপচাপ উপেক্ষিত হলো, push sloppy mode-এও TypeError দিল (array-এর length বদলাতে না পারায়), আর "use strict"-এর ভেতরে সাধারণ লেখাও TypeError. শেষ লাইন: সব আগের মতো, আর সবচেয়ে ভেতরের item-ও frozen. TypeError-এর বার্তার লেখা browser-ভেদে আলাদা হয়, তাই এখানে শুধু নাম ছাপা হয়েছে। চুপচাপ ব্যর্থ হওয়াও fail fast-এর উল্টো — এ কারণেও আধুনিক JavaScript-এ (module আর class-এর ভেতরে) strict mode ডিফল্ট।

Defensive copy — mutable জিনিস ঢোকা আর বেরোনোর সময়

সব কিছু immutable করা না গেলে সীমানায় কপি করুন। Constructor একটি list পেলে তার কপি রাখুন, যাতে caller পরে নিজের list বদলালে object-এর অবস্থা না বদলায়; getter ভেতরের list ফেরত না দিয়ে কপি বা tuple দিন। এই defensive copy §২-এর টেবিলের শেষ সারির সমাধান। Frozen dataclass-এর ভেতরেও list রাখলে সেটি বদলানো যায় — অনুশীলন ১-এ এর একটি উদাহরণ আছে। কপির খরচ আছে, তাই বড় data-র ক্ষেত্রে persistent data structure (যেখানে নতুন version পুরোনোর বেশিরভাগ অংশ ভাগ করে নেয়) একটি বিকল্প; বিস্তারিত Python কোর্সের mutability আর copy পর্বে।

৮ · Idempotent কাজ আর null-এর ব্যবস্থা

লিফটের বোতাম একবার চাপুন বা দশবার — লিফট একবারই আসে। কিন্তু চায়ের দোকানে “আরেক কাপ” দশবার বললে দশ কাপ আসে। প্রথমটি idempotent: একই কাজ একবার করা আর বহুবার করার ফল একই। Interface-এর চুক্তিতে এটি লিখে রাখা খুব জরুরি, কারণ network-এ “উত্তর আসেনি” মানে কাজ হয়নি — এমন নয়; হয়তো কাজ হয়েছে, শুধু উত্তর হারিয়েছে। Caller তখন আবার চেষ্টা (retry) করবেন, আর কাজটি idempotent না হলে সমিতির সদস্যের কিস্তি দুবার কাটা যাবে।

IdempotentIdempotent নয়Idempotent বানানোর কৌশল
set_status(loan, "PAID")balance += 500“আরও যোগ”-এর বদলে “নির্দিষ্ট মান বসানো”
delete_member(id) (দ্বিতীয়বার: আগেই নেই)add_member(name)Caller-এর দেওয়া অনন্য ID দিয়ে তৈরি — একই ID আবার এলে আগের ফল ফেরত
HTTP GET, PUT, DELETE (সংজ্ঞায়)HTTP POSTIdempotency-Key header

শেষ কৌশলটি — প্রতিটি অনুরোধে একটি অনন্য key, আর server আগে দেখা key-এর জন্য আগের ফল ফেরত দেয় — payment API-গুলোতে বহুল প্রচলিত। এর খুঁটিনাটি (key কতদিন রাখবেন, একই key-তে ভিন্ন body এলে কী করবেন) REST API নকশা পর্বে আছে; এখানে মূল কথা হলো idempotency interface-এর চুক্তির অংশ, ভেতরের খুঁটিনাটি নয়।

Null — “নেই” কীভাবে বলবেন

“এই ফোন নম্বরে কোনো সদস্য নেই” — কীভাবে জানাবেন? সবচেয়ে প্রচলিত উত্তর null/None, আর এর জনক Tony Hoare নিজেই ২০০৯ সালের একটি বক্তৃতায় ১৯৬৫ সালে ALGOL W-তে null reference যোগ করাকে তাঁর “billion-dollar mistake” বলেছিলেন। সমস্যা হলো null signature-এ দেখা যায় না: find_member(phone) -> Member পড়ে কেউ বোঝেন না এটি কখনো কখনো কিছুই দেবে না। তিনটি বিকল্প:

Optional / Maybe

“থাকতেও পারে, না-ও পারে” কথাটি type-এ: Java ৮-এর Optional<Member>, Python-এ Member | None (type checker তখন যাচাই না করে ব্যবহার আটকায়), Kotlin আর Swift-এ Member?.

Optional type

Sentinel মান

বিশেষ একটি মান, যেমন str.find-এর -1. সহজ, কিন্তু বৈধ মানের সাথে গুলিয়ে যেতে পারে আর উপেক্ষা করা সহজ — §৬-এর error code-এর সমস্যা।

Sentinel value

Null Object

“কিছু না করা” একটি আসল object: যেমন NoDiscount, যার apply(price) দাম না বদলে ফেরত দেয়। Caller-কে if লিখতে হয় না। Pattern-এর বিস্তারিত মডিউল ৬-এ।

Null Object pattern

কোনটি বেছে নেবেন? “নেই” যদি স্বাভাবিক উত্তর হয় (খোঁজা), Optional; “নেই”-র একটি নিরীহ ডিফল্ট আচরণ থাকলে (ছাড় নেই = শূন্য ছাড়), Null Object; আর “নেই” যদি আসলে ভুল হয় (সদস্যের ID দিয়ে আনতে গিয়ে পাওয়া গেল না, অথচ ID আমাদের নিজেদের database থেকেই এসেছে), exception — নিজের নিয়ম ভাঙার কথা চুপচাপ None-এ লুকাবেন না।

৯ · Interface বদলানো — পুরোনো ব্যবহারকারী না ভেঙে

একটি interface একবার অন্যরা ব্যবহার শুরু করলে প্রতিটি বদলের প্রশ্ন একটাই: পুরোনো নিয়মে লেখা কোড কি এখনো চলবে? চললে পরিবর্তনটি backward compatible. DbC-র ভাষায় নিরাপদ বদলের নিয়ম পর্ব ২৮-এর Liskov নিয়মের মতোই: precondition দুর্বল করা (আগের চেয়ে বেশি input গ্রহণ) আর postcondition শক্ত করা (আগের চেয়ে বেশি প্রতিশ্রুতি) পুরোনো caller ভাঙে না; উল্টোটা ভাঙে। নতুন optional parameter, নতুন method, নতুন error subclass — সাধারণত নিরাপদ; parameter সরানো, নাম বদলানো, ফেরত মানের অর্থ বদলানো — breaking. পর্ব ১৮-এর SemVer (ভালো commit, tag আর release) এই পার্থক্যকেই version নম্বরে লেখে; data format আর schema-র ক্ষেত্রে একই প্রশ্নের উত্তর System Design-এর serialization আর schema পর্বে।

Hyrum's law: Google-এর প্রকৌশলী Hyrum Wright-এর নামে প্রচলিত একটি পর্যবেক্ষণ, যা Software Engineering at Google (২০২০) বইয়েও আলোচিত — মোটামুটি এরকম: “একটি API-র ব্যবহারকারী যথেষ্ট বেশি হলে, চুক্তিতে আপনি কী কথা দিয়েছেন তাতে কিছু যায় আসে না; system-এর প্রতিটি দেখা-যায় এমন আচরণের ওপর কেউ না কেউ নির্ভর করবেন।” রেলের সময়সূচির কথা ভাবুন — ট্রেন বছরের পর বছর দশ মিনিট দেরিতে এলে যাত্রীরা সেই দেরি ধরেই বাড়ি থেকে বের হন; একদিন ট্রেন ঠিক সময়ে এলে অনেকে ট্রেন হারান। Error বার্তার লেখা, তালিকার ক্রম, সময়ের হিসাব — চুক্তিতে না থাকলেও কেউ এগুলো ধরে বসে আছেন।

নিচের stepper একটি কাল্পনিক SMS library-র interface ধাপে ধাপে বদলায় — কোনো client না ভেঙে, শেষে একটি পরিকল্পিত MAJOR পর্যন্ত। এই কৌশলের প্রচলিত নাম parallel change বা expand-contract: আগে নতুনটি যোগ (expand), সবাই সরে গেলে পুরোনোটি সরানো (contract).

  1. শুরুv1.4.0send_sms
  2. Expandv1.5.0নতুন যোগ
  3. Deprecatev1.6.0সতর্কবার্তা
  4. Contractv2.0.0পুরোনো সরানো
Library-র interface আর client-দের অবস্থা (কাল্পনিক)
Interface
Version
পুরোনো client
Hyrum-এর ঝুঁকি
  1. শুরু। send_sms(phone, text) সফল হলে True, ব্যর্থ হলে False ফেরত দেয় — কেন ব্যর্থ, তা বলে না। ১২টি client এটি ব্যবহার করে। নতুন চাহিদা: পাঠানোর নাম (sender) বেছে নেওয়া আর ব্যর্থতার কারণ জানা।
  2. ঐচ্ছিক parameter. sender যোগ হলো ডিফল্ট মানসহ। পুরোনো ডাক send_sms(p, t) হুবহু আগের মতো চলে — precondition দুর্বল হলো (বেশি ধরনের ডাক বৈধ), postcondition অপরিবর্তিত। SemVer-এ MINOR.
  3. পাশাপাশি নতুন পথ। নতুন send_message একটি Result ফেরত দেয় (§৬), যাতে ব্যর্থতার কারণ থাকে। পুরোনো send_sms এখন ভেতরে send_message ডেকে ফলকে True/False-এ রূপান্তর করে — দুটি implementation নয়, একটি। এখানে Hyrum's law ধরা পড়ল: একটি client ব্যর্থতার পরে log-এ “SMS failed” লেখাটি খোঁজে।
  4. Deprecation. send_sms ডাকলে এখন DeprecationWarning আসে, সাথে কোন version-এ সরবে আর কীভাবে send_message-এ যাবেন। SemVer ২.০.০ অনুযায়ী deprecation ঘোষণাতেও MINOR বাড়ে। চুক্তিতে না থাকলেও “SMS failed” লেখাটি সচেতনভাবে অপরিবর্তিত রাখা হলো।
  5. অপেক্ষা আর মাপা। কয়েক মাস ধরে log আর ব্যবহারের হিসাব দেখে বোঝা গেল ৯টি client সরে গেছে। বাকি ৩টির দলকে সরাসরি জানানো হলো। পুরোনো interface সরানোর তারিখ ঠিক হয় তথ্য দেখে, অনুমানে নয়।
  6. পরিকল্পিত breaking change. send_sms সরিয়ে 2.0.0 — MAJOR. Changelog-এ “Breaking” অংশ আর migration guide. যে client আপগ্রেড করবেন না, তিনি 1.6.x-এ থাকতে পারেন। মোট ফল: কোনো client হঠাৎ ভাঙেনি; প্রত্যেকে নিজের সময়ে সরেছেন।

Robustness principle-এর দ্বিধা: Jon Postel-এর নামে প্রচলিত TCP-যুগের নীতি — “যা পাঠান তাতে কঠোর হোন, যা গ্রহণ করেন তাতে উদার হোন” — দীর্ঘদিন interface নকশার সোনালি নিয়ম ছিল। কিন্তু উদারভাবে ভুল input মেনে নিলে সেই ভুলগুলোই Hyrum's law মেনে চিরস্থায়ী চুক্তি হয়ে যায়; IETF-এর RFC 9413 (২০২৩) এই সমালোচনাগুলো একত্র করে। আধুনিক পরামর্শ সাধারণত: trust boundary-তে স্পষ্টভাবে যাচাই করুন, অজানা field উপেক্ষা করার মতো উদারতা ভেবেচিন্তে আর লিখিতভাবে দিন।

আরও গভীরে: Meyer-এর Object-Oriented Software Construction (২য় সংস্করণ, ১৯৯৭)-এর contract-এর অধ্যায়গুলো এই পর্বের মূল উৎস; ১৯৯২ সালে IEEE Computer-এ প্রকাশিত তাঁর “Applying ‘Design by Contract’” প্রবন্ধটি একটি সংক্ষিপ্ত পরিচয়। Hoare-এর ১৯৬৯ সালের “An Axiomatic Basis for Computer Programming” আর Liskov ও Wing-এর ১৯৯৪ সালের behavioural subtyping-এর প্রবন্ধ এর তাত্ত্বিক ভিত্তি। ব্যবহারিক দিকে Joshua Bloch-এর Effective Java (exception, immutability আর defensive copy-র অধ্যায়) আর Bloch-এর API নকশা নিয়ে বক্তৃতা “How to Design a Good API and Why it Matters” আজও প্রাসঙ্গিক। Test-এর দিক থেকে contract যাচাই — consumer-driven contract test — পর্ব ৫১-এ আসবে।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Design by contract-এর তিনটি অংশ কী, আর কোনটির দায় কার? — precondition: caller; postcondition আর class invariant: supplier.
  • Python-এ input যাচাইয়ে assert কেন নয়? — python -O সব assert বাদ দেয়; তখন check-টিই থাকে না।
  • Checked আর unchecked exception-এর পার্থক্য (Java)? — checked: signature-এ ঘোষণা আর caller-এর সামলানো বাধ্যতামূলক; unchecked (RuntimeException): বাধ্যতামূলক নয়।
  • Idempotent operation কী? একটি উদাহরণ দিন। — বহুবার করলেও একবারের সমান ফল; যেমন HTTP PUT বা “status = PAID বসানো”।
  • JavaScript-এর Object.freeze কি deep? — না, shallow; ভেতরের object আলাদাভাবে freeze করতে হয়।

১০ · AI-এর যুগে interface আর contract

AI কোড-সহকারী দ্রুত function লেখে, কিন্তু চুক্তির প্রশ্নে তার ভুলগুলো খুব পরিচিত ধাঁচের। কারণ AI প্রায়ই শুধু “সুখের পথ” (happy path) দেখে, আর ইন্টারনেটে প্রচলিত অভ্যাস — ভালো-মন্দ মিলিয়ে — অনুকরণ করে।

AI-এর সাধারণ ভুলকীভাবে ধরবেন
Error “সামলাতে” except Exception: pass বা খালি catch, নয়তো return None দিয়ে ব্যর্থতা লুকানোDiff-এ প্রতিটি except/catch খুঁজুন; linter-এর নিয়ম (যেমন bare-except) চালু রাখুন
Input যাচাইয়ে assert; বা নিরাপত্তার check assert-এpython -O দিয়ে test চালিয়ে দেখুন; security linter
“Object.freeze object-কে পুরোপুরি immutable করে” — ভুল দাবিভেতরের একটি array-তে push করে নিজে চালিয়ে দেখুন
Refactor করতে গিয়ে public function-এর নাম বা parameter-এর ক্রম বদলানো — breaking change, অথচ version-এ PATCHPublic API-র diff আলাদা করে review; পুরোনো client-এর test রাখুন
নেই এমন contract library-র API, ভুল Eiffel syntax বা ভুল সাল (“DbC Java-র অংশ”)Official documentation আর মূল উৎসে মিলিয়ে নিন; import করে চালিয়ে দেখুন
সব জায়গায় Result type, Optional আর wrapper — ছোট script-এওপ্রশ্ন করুন: এই ব্যর্থতা কে, কোথায় সামলাবেন? না থাকলে সরল রাখুন

একটি function-এর চুক্তি লেখাতে AI-কে এভাবে কাজ দিন — কোডে আসল গ্রাহকের data, API key বা পাসওয়ার্ড থাকলে আগে মুছে নিন:

নিচে একটি কাল্পনিক সমবায় সমিতির app-এর একটি Python function দিলাম। ১. এর precondition, postcondition (দরকার হলে old মানসহ) আর class invariant আলাদা তালিকায় লেখো। প্রতিটি শর্ত Python expression হিসেবে লেখো, আর বলো caller কি সেটি নিজে যাচাই করতে পারেন। ২. কোন ব্যর্থতাগুলো contract ভাঙা (bug) আর কোনগুলো প্রত্যাশিত ব্যর্থতা (network, ব্যবহারকারীর ভুল input) — আলাদা করো। প্রত্যাশিত ব্যর্থতার জন্য কোন exception class বা Result মান হবে, প্রস্তাব করো। ৩. বাইরের input যাচাইয়ে assert ব্যবহার করবে না; কোথায় trust boundary, চিহ্নিত করো। ৪. কাজটি idempotent কি না বলো; না হলে retry নিরাপদ করার উপায় বলো। ৫. Function-এর বর্তমান signature বদলাবে না। বদলানো দরকার মনে হলে আলাদা করে লেখো, আর কোন পুরোনো caller ভাঙবে, তা বলো। ৬. কোনো library-র নাম বললে সেটির কোন version-এ API-টি আছে বলো; নিশ্চিত না হলে "নিশ্চিত নই" লেখো।

AI-এর খসড়া হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১১৯৯৬ সালের Ariane 5 Flight 501-এর দুর্ঘটনা কি design by contract দিয়ে ঠেকানো যেত? Meyer-এর দাবি আর তার সমালোচনা কী?

পর্ব ০২-এ (software-এর ইতিহাস) ঘটনাটি দেখেছেন: Ariane 4-এর জন্য লেখা inertial reference software Ariane 5-এ আবার ব্যবহার করা হয়েছিল। একটি ৬৪-bit floating-point মানকে ১৬-bit signed integer-এ রূপান্তরের সময় মানটি সীমা ছাড়িয়ে যায়, exception ওঠে, আর সেটি সামলানোর ব্যবস্থা না থাকায় দুটি reference unit-ই বন্ধ হয়। তদন্ত বোর্ডের প্রতিবেদন অনুযায়ী, Ariane 4-এর উড্ডয়ন-পথে ওই মান সীমার মধ্যে থাকবে বলে বিশ্লেষণ করে কিছু রূপান্তরে সুরক্ষা ইচ্ছা করেই রাখা হয়নি (প্রসেসরের কাজের চাপ কমাতে)।

১৯৯৭ সালে Jean-Marc Jézéquel আর Bertrand Meyer IEEE Computer-এ “Design by Contract: The Lessons of Ariane” প্রবন্ধে যুক্তি দেন: সমস্যাটি আসলে reuse-এর চুক্তির — module-টির একটি অলিখিত precondition ছিল (“horizontal bias-এর মান ১৬-bit-এ আঁটবে”)। এটি contract হিসেবে লেখা থাকলে Ariane 5-এর জন্য module পুনর্ব্যবহারের review-তে প্রশ্নটি চোখে পড়ত, আর test-এ assertion ব্যর্থ হতো।

সমালোচনাও জোরালো। কয়েকজন লেখক (যেমন Ken Garlington-এর একটি বহুল উদ্ধৃত প্রতিক্রিয়া) বলেন: রূপান্তরের সম্ভাব্য সীমা-লঙ্ঘন প্রকৌশলীরা জানতেন এবং সচেতনভাবেই সুরক্ষা বাদ দিয়েছিলেন; আর Ariane 5-এর আসল উড্ডয়ন-পথ দিয়ে পুরো system-এর simulation হলে ভুলটি এমনিতেই ধরা পড়ত — যা করা হয়নি। অর্থাৎ ঘাটতি শুধু একটি keyword-এর নয়, requirement আর system-level যাচাইয়ের। তা ছাড়া চলার সময় precondition ভাঙা ধরা পড়লেও উড়ন্ত রকেটে “থেমে যাওয়া” কোনো সমাধান নয় — fail fast সবখানে নিরাপদ পথ নয়।

দুই পক্ষের মিলনবিন্দু: অলিখিত অনুমান reuse-এর সবচেয়ে বড় শত্রু। Contract অনুমানকে দৃশ্যমান করে, কিন্তু সেই দৃশ্যমান অনুমান নতুন পরিবেশের সাথে মিলিয়ে দেখার দায়িত্ব মানুষের, আর ব্যর্থ হলে কী করতে হবে (degrade, বিকল্প পথ) সেটিও নকশার অংশ।

মূল উপলব্ধি: Contract কোনো জাদুর ঢাল নয়, কিন্তু reuse-এর সময় লুকানো precondition-কে প্রকাশ্যে আনে; আসল প্রতিরক্ষা আসে লিখিত অনুমান, নতুন পরিবেশে system-level যাচাই আর ব্যর্থতার পরিকল্পিত পথ — তিনটি একসাথে।

প্র ০২Java-র checked exception ভালো ধারণা হয়েও কেন পরের ভাষাগুলো (C#, Kotlin) এটি নেয়নি?

Checked exception-এর পেছনের চিন্তা DbC-র সাথে মেলে: কোন প্রত্যাশিত ব্যর্থতা ঘটতে পারে, তা method-এর চুক্তিতে — signature-এ — লেখা থাকুক, আর compiler নিশ্চিত করুক caller সেটি উপেক্ষা করছেন না। readFile() throws IOException পড়লেই জানা যায় file না থাকতে পারে।

বাস্তবে কয়েকটি সমস্যা দেখা দিল। প্রথম, versioning: একটি নিচের স্তরের method নতুন একটি checked exception তুলতে শুরু করলে তার ওপরের প্রতিটি method-এর signature বদলাতে হয় — একটি ছোট পরিবর্তন পুরো ডাকের শিকলে breaking change হয়ে ছড়ায়। দ্বিতীয়, স্তর মেশানো: একটি UserRepository interface-এর method যদি throws SQLException লেখে, তাহলে interface-ই ফাঁস করে দেয় ভেতরে SQL database আছে — information hiding ভাঙে। তৃতীয়, মানুষের অভ্যাস: compiler-কে খুশি রাখতে অনেকে খালি catch (Exception e) {} লেখেন, যা §৬-এর সবচেয়ে খারাপ অভ্যাস। চতুর্থ, lambda আর stream: Java ৮-এর functional interface-গুলো checked exception ঘোষণা করে না, তাই lambda-র ভেতরে সেগুলো সামলানো কষ্টকর।

C#-এর নকশাকারীরা প্রকাশ্য সাক্ষাৎকারে এই versioning আর scalability-র সমস্যার কথা বলে checked exception না রাখার সিদ্ধান্ত ব্যাখ্যা করেছেন বলে উদ্ধৃত হয়; Kotlin-ও সব exception-কে unchecked রাখে। Java-র ভেতরেও অনেক আধুনিক library (যেমন Spring-এর data access স্তর) checked exception-কে unchecked-এ মুড়ে দেয়।

তবে মূল চাহিদা — “ব্যর্থতা type-এ দেখা যাক” — হারায়নি; সেটি অন্য পথে ফিরে এসেছে। Rust-এর Result<T, E> আর Kotlin-এর Result type ব্যর্থতাকে চুক্তিতে রাখে, কিন্তু ফেরত মান হিসেবে, যা generic আর lambda-র সাথে ভালোভাবে মেশে। Swift-এর throws চিহ্ন signature-এ শুধু জানায় যে function ব্যর্থ হতে পারে, আর caller-কে try লিখতে বাধ্য করে — Java-র মতো প্রতিটি exception-এর তালিকা চায় না। সাধারণ চলতি নির্দেশনা দাঁড়িয়েছে: caller যা থেকে সত্যিই সেরে উঠতে পারেন সেটি চুক্তিতে প্রকাশ্য রাখুন; programming bug-কে unchecked রাখুন।

মূল উপলব্ধি: ব্যর্থতাকে চুক্তিতে দেখানো ভালো ধারণা, কিন্তু যে প্রক্রিয়ায় দেখানো হয় তার versioning আর ব্যবহারের খরচ নির্ধারণ করে মানুষ সেটি সৎভাবে ব্যবহার করবেন, না পাশ কাটাবেন।

প্র ০৩Tony Hoare null reference-কে কেন “billion-dollar mistake” বলেছিলেন, আর আধুনিক ভাষাগুলো এর কী সমাধান দিয়েছে?

Hoare ১৯৬৫ সালে ALGOL W ভাষার জন্য reference-এর নকশা করার সময় null reference যোগ করেন — তাঁর নিজের ভাষায়, কারণ এটি বানানো খুব সহজ ছিল। ২০০৯ সালে একটি সম্মেলনের বক্তৃতায় তিনি একে “billion-dollar mistake” বলেন: তাঁর ভাষায়, পরের প্রায় চল্লিশ বছরে এটি অগণিত error, নিরাপত্তা-ছিদ্র আর system crash ঘটিয়ে হয়তো শতকোটি ডলারের ক্ষতি করেছে। সংখ্যাটি তাঁর আনুমানিক অলংকার, মাপা হিসাব নয়।

Contract-এর দৃষ্টিতে সমস্যাটি পরিষ্কার। Member find(String phone) লেখা থাকলে type বলে “একটি Member ফেরত আসবে”, কিন্তু আসলে কখনো কখনো কিছুই আসে না। অর্থাৎ type system-এর চুক্তি মিথ্যা বলে, আর প্রতিটি caller-কে মনে রাখতে হয় কোন method null দিতে পারে — documentation পড়ে, বা crash খেয়ে। Java-তে NullPointerException, JavaScript-এ “Cannot read properties of undefined” — প্রতিদিনের পরিচিত error.

আধুনিক সমাধান তিন ধরনের। Type-এ null-যোগ্যতা: Kotlin আর Swift-এ Member আর Member? আলাদা type; প্রথমটিতে null রাখাই যায় না, দ্বিতীয়টি ব্যবহারের আগে compiler যাচাই চায়। TypeScript-এর strictNullChecks আর C# ৮-এর nullable reference type একই পথে গেছে। Python-এ Member | None লিখলে mypy-র মতো checker একই কাজ করে। Optional/Maybe মান: Java ৮-এর Optional, Haskell-এর Maybe, Rust-এর Option — “নেই” একটি আসল মান। Rust-এ সাধারণ reference null হতেই পারে না। নকশার কৌশল: Null Object, খালি collection ফেরত দেওয়া (null list-এর বদলে খালি list), আর constructor-এ সব field বাধ্যতামূলক করা।

তবু এগুলো সব সমস্যা মেটায় না: Java-র Optional field বা parameter হিসেবে ব্যবহার সাধারণত নিরুৎসাহিত, আর পুরোনো library-র সাথে মেশার সীমানায় null আবার ঢুকে পড়ে — সেখানে trust boundary-র মতো যাচাই লাগে।

মূল উপলব্ধি: null-এর আসল দোষ চুক্তিকে অদৃশ্য করা; সমাধান হলো “থাকতেও পারে, না-ও পারে” কথাটি type-এ বা নকশায় প্রকাশ্য করা, যাতে compiler বা reviewer তা যাচাই করতে পারেন।

প্র ০৪Hyrum's law সত্য হলে কি “শুধু documented আচরণই চুক্তি” কথাটি অর্থহীন? Library-র রক্ষণাবেক্ষণকারী কী করতে পারেন?

অর্থহীন নয়, কিন্তু অসম্পূর্ণ। Documented চুক্তি বলে কার দোষ: আপনি যদি তালিকার ক্রমের ওপর নির্ভর করেন অথচ চুক্তিতে ক্রমের কোনো প্রতিশ্রুতি নেই, তাহলে ক্রম বদলালে আইনত ও নৈতিকভাবে দোষ আপনার। Hyrum's law বলে কী ঘটবে: দোষ যারই হোক, ব্যবহারকারী যথেষ্ট বেশি হলে কারও কোড ভাঙবে, আর অভিযোগ আসবে library-র কাছেই। দুটি একসাথে সত্য।

বাস্তবের উদাহরণ অনেক। Go ভাষার map-এর ক্রম ইচ্ছা করেই এলোমেলো রাখা হয়, যাতে কেউ map-এর ক্রমের ওপর নির্ভর না করেন — চুক্তির বাইরের আচরণকে অস্থির করে দেওয়া। Python ৩.৩ থেকে string-এর hash ডিফল্টভাবে প্রতিবার চালানোর সময় ভিন্ন (নিরাপত্তার কারণে), তাতে hash-এর মান ধরে রাখা কিছু test ভেঙেছিল। আবার উল্টো দিকে, Python-এর dict-এ insertion order প্রথমে CPython ৩.৬-এর implementation-এর খুঁটিনাটি ছিল; এত মানুষ এর ওপর নির্ভর করতে শুরু করেন যে ৩.৭-এ এটি ভাষার আনুষ্ঠানিক চুক্তি হয়ে যায় — Hyrum's law চুক্তিকেই বদলে দিল।

রক্ষণাবেক্ষণকারীর হাতিয়ার: (১) চুক্তির বাইরের আচরণ লুকানো বা এলোমেলো করা — Go-এর map-এর মতো; ভেতরের field private রাখা। (২) চুক্তি স্পষ্ট লেখা — “ক্রমের কোনো প্রতিশ্রুতি নেই”, “error বার্তার লেখা বদলাতে পারে, exception-এর type বদলাবে না”। (৩) বদলের আগে মাপা — বড় প্রতিষ্ঠানে সব client-এর test চালিয়ে দেখা কে ভাঙে; open source-এ জনপ্রিয় dependent project-এর test চালানো। (৪) Deprecation-এর সময় আর পথ দেওয়া — §৯-এর stepper-এর মতো parallel change. (৫) নির্ভরতার জায়গা দেওয়া — মানুষ error বার্তার লেখা parse করলে বুঝতে হবে তাঁদের একটি error code বা type দরকার ছিল।

মূল উপলব্ধি: লেখা চুক্তি দায় ঠিক করে, আর দেখা-যায় এমন আচরণ নির্ভরতা তৈরি করে; ভালো API রক্ষণাবেক্ষণ মানে দুটির মধ্যের ফাঁক যতটা সম্ভব ছোট রাখা আর বদলের সময় সেই ফাঁকের দামও হিসাবে ধরা।

প্র ০৫Production-এ assertion চালু রাখবেন, না বন্ধ? দুই পক্ষের যুক্তি কী?

প্রচলিত অভ্যাস ছিল development-এ assertion চালু, release-এ বন্ধ — C-তে NDEBUG, Python-এ -O, Java-তে ডিফল্টভাবেই বন্ধ। যুক্তি: assertion সময় খরচ করে (কিছু invariant যাচাই, যেমন “পুরো tree balanced”, পুরো data হেঁটে দেখে), আর production-এ হঠাৎ থেমে যাওয়া ব্যবহারকারীর জন্য খারাপ অভিজ্ঞতা।

উল্টো পক্ষের একটি বিখ্যাত উপমা Hoare-এর নামে উদ্ধৃত হয় (Meyer-সহ অনেকে উল্লেখ করেছেন): development-এ assertion চালু রেখে production-এ বন্ধ করা মানে ডাঙায় অনুশীলনের সময় নাবিকের life jacket পরা আর সমুদ্রে যাওয়ার আগে খুলে ফেলা। বাস্তব ব্যবহারকারীর অদ্ভুত data আর ভারী চাপ — bug ঠিক সেখানেই বের হয়। Assertion বন্ধ থাকলে ভুল অবস্থা চুপচাপ এগিয়ে যায় — হয়তো ভুল হিসাব database-এ লেখা হয়, যা দেরিতে ধরা পড়লে ঠিক করা অনেক দামি। Fail fast-এর পুরো যুক্তি production-এই সবচেয়ে বেশি খাটে।

মাঝামাঝি পথগুলো বাস্তবে বেশি দেখা যায়। (১) সস্তা check চালু, দামি check বন্ধ: O(1) precondition সবসময়; পুরো data হাঁটা invariant শুধু test-এ। Eiffel-এ class-ভিত্তিক যাচাইয়ের মাত্রা বেছে নেওয়া যায় (শুধু precondition, বা সব), আর অনেক দল release-এ অন্তত precondition চালু রাখেন। (২) থামা নয়, জানানো: production-এ assertion ভাঙলে প্রক্রিয়া বন্ধ না করে error log আর monitoring-এ সতর্কতা, তারপর সেই অনুরোধটি ব্যর্থ করা — পুরো server নয়। (৩) ঝুঁকিভেদে: টাকার হিসাব বা চিকিৎসা-যন্ত্রে ভুল অবস্থা নিয়ে এগোনোর চেয়ে থামা নিরাপদ; আবার উড়ন্ত বিমানে থামা নিজেই বিপদ, সেখানে নিরাপদ বিকল্প অবস্থায় যাওয়া (প্র ০১)।

আর যা-ই বেছে নিন, একটি নিয়ম অটল: যে check বন্ধ হলে প্রোগ্রামের আচরণ ভুল বা অনিরাপদ হয়, সেটি assertion নয় — সেটি validation, আর তাকে সবসময় চালু থাকা কোডে লিখতে হবে (§৫)।

মূল উপলব্ধি: প্রশ্নটি “চালু না বন্ধ” নয়, বরং “কোন check-এর খরচ আর কোন ব্যর্থতার ঝুঁকি কত” — সস্তা আর জরুরি check production-এ রাখুন, আর ভাঙলে কী ঘটবে সেটিও নকশা করুন।

অনুশীলন

আউটপুট অনুমান, চুক্তি লেখা, version-এর হিসাব আর একটি error-handling নকশার সমালোচনা — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।

  1. আউটপুট অনুমান করুন: নিচের Python প্রোগ্রাম চালালে চারটি লাইন ছাপা হবে। চালানোর আগে প্রতিটি লাইন লিখুন, আর বলুন Basket কি সত্যিই immutable.
    from dataclasses import dataclass, FrozenInstanceError
    
    @dataclass(frozen=True)
    class Money:
        amount: int
        currency: str = "BDT"
        def add(self, other):
            return Money(self.amount + other.amount, self.currency)
    
    @dataclass(frozen=True)
    class Basket:
        items: list
    
    a = Money(100)
    b = a.add(Money(50))
    print(a, b, a == Money(100))
    try:
        a.amount = 0
    except FrozenInstanceError as e:
        print("blocked:", e)
    k = Basket(["rice"])
    k.items.append("oil")
    print(k.items, hash(a) == hash(Money(100)))
    try:
        hash(k)
    except TypeError as e:
        print("TypeError:", e)
    Money(amount=100, currency='BDT') Money(amount=150, currency='BDT') True
    blocked: cannot assign to field 'amount'
    ['rice', 'oil'] True
    TypeError: unhashable type: 'list'

    লাইন ১: add a-কে বদলায় না, নতুন Money(150) বানায় — value object-এর ধরন। Dataclass __eq__ বানায় field মিলিয়ে, তাই a == Money(100) True, যদিও দুটি আলাদা object. লাইন ২: frozen dataclass-এ field-এ লেখা FrozenInstanceError তোলে (Python ৩.১৪-এ বার্তা এরকম; version-ভেদে লেখা সামান্য আলাদা হতে পারে)। লাইন ৩: ফাঁদ — items field-টিকে অন্য list-এ বদলানো যায় না, কিন্তু list-টির ভেতরে append করা যায়; তাই ['rice', 'oil']. JavaScript-এর shallow Object.freeze-এর মতোই। frozen=True আর eq=True থাকায় dataclass __hash__-ও বানায়, আর সমান মানের hash সমান — True. লাইন ৪: Basket-এর hash বানাতে গিয়ে ভেতরের list-এর hash লাগে, আর list unhashable — TypeError. উত্তর: Basket শুধু ওপরের স্তরে immutable; সত্যিকারের immutable করতে items: tuple রাখুন আর constructor-এ tuple(items) দিয়ে defensive copy নিন।

  2. চুক্তিটি লিখুন: ঈদের আগে একটি কাল্পনিক বাস কোম্পানির টিকিট app. Class Trip-এ আছে capacity (মোট আসন), booked (আসন নম্বর থেকে যাত্রীর ID-র dictionary), আর method reserve_seat(seat_no, passenger_id). প্রতিটি যাত্রী একটির বেশি আসন নিতে পারবেন না। এই method-এর precondition, postcondition (দরকার হলে old মানসহ) আর Trip-এর class invariant লিখুন। বলুন কোন শর্ত ভাঙলে দায় কার, আর একই অনুরোধ দুবার এলে (network retry) কী হওয়া উচিত।

    Precondition (caller-এর দায়):

    • 1 <= seat_no <= capacity — আসন নম্বর বৈধ।
    • passenger_id খালি নয়।
    • Caller যাচাই করতে পারেন এমনভাবে: is_free(seat_no) নামে একটি public query থাকা দরকার। কিন্তু বহু মানুষ একসাথে বুক করলে caller যাচাইয়ের পর আর ডাকের আগে আসনটি অন্য কেউ নিয়ে নিতে পারেন (race condition). তাই “আসন খালি” precondition-এ না রেখে প্রত্যাশিত ব্যর্থতা হিসেবে রাখা ভালো: আসন নেওয়া থাকলে SeatTaken exception বা Err("seat taken"). এটি §৪-এর পঞ্চম ধাপের নিয়মের প্রয়োগ।

    Postcondition (সফল হলে, supplier-এর দায়):

    • booked[seat_no] == passenger_id
    • len(booked) == old len(booked) + 1 — ঠিক একটি আসন যোগ হয়েছে (নতুন বুকিংয়ের ক্ষেত্রে)।
    • অন্য সব আসন অপরিবর্তিত: old booked থেকে seat_no বাদে বাকি সব হুবহু একই।

    Class invariant (class-এর দায়): len(booked) <= capacity; booked-এর প্রতিটি key ১ থেকে capacity-এর মধ্যে; কোনো passenger_id দুবার নেই (len(set(booked.values())) == len(booked)).

    দায়: আসন নম্বর ৫০ অথচ capacity ৪০ — caller-এর bug. সফল ফেরত এল কিন্তু booked[seat_no]-এ অন্য যাত্রী — supplier-এর bug. কোনো ডাকের পরে একই যাত্রী দুই আসনে — invariant ভাঙা, class-এর bug.

    Idempotency: একই (seat_no, passenger_id) আবার এলে আসনটি ইতিমধ্যে সেই যাত্রীরই — তখন সফল হিসেবে আগের ফল ফেরত দিন, SeatTaken নয়, আর len(booked) বাড়বে না। তাই দ্বিতীয় postcondition-টি লিখুন শর্তসহ: “আগে এই যাত্রীর না থাকলে +১, থাকলে অপরিবর্তিত”। এতে retry নিরাপদ। ভিন্ন যাত্রী একই আসন চাইলে SeatTaken; একই যাত্রী ভিন্ন আসন চাইলে invariant রক্ষায় AlreadyBooked.

  3. Version-এর হিসাব: একটি কাল্পনিক সমিতির হিসাব library বর্তমানে 2.3.1. পরপর পাঁচটি release-এ এই পরিবর্তনগুলো আসে: (ক) সুদের হিসাবে rounding-এর একটি bug ঠিক করা; (খ) calc_interest-এ ঐচ্ছিক parameter currency="BDT" যোগ; (গ) পুরোনো calc_tax_old-কে deprecated ঘোষণা; (ঘ) exception-এর বার্তার লেখা বদলানো — চুক্তিতে লেখা আছে শুধু exception-এর type-ই public; (ঙ) withdraw-এর precondition শক্ত করা (এখন সর্বোচ্চ ১,০০,০০০ টাকা, আগে যেকোনো ধনাত্মক মান) আর একই release-এ calc_tax_old সরানো। SemVer ২.০.০ মেনে প্রতিটি release-এর version লিখুন। কোন ধাপে Hyrum's law-এর ঝুঁকি সবচেয়ে বেশি, আর (ঙ)-এর আগে কী করা উচিত?
    • (ক) 2.3.2 — পুরোনো ব্যবহার না ভেঙে শুধু bug fix: PATCH.
    • (খ) 2.4.0 — নতুন, backward-compatible সুবিধা (precondition দুর্বল হলো — বেশি ধরনের ডাক বৈধ): MINOR, আর PATCH শূন্যে ফেরে।
    • (গ) 2.5.0 — SemVer ২.০.০-এর নিয়মে public API-র কিছু deprecated ঘোষণা করলে MINOR বাড়াতে হয়।
    • (ঘ) 2.5.1 — চুক্তি অনুযায়ী বার্তার লেখা public নয়, তাই PATCH. কিন্তু এখানেই Hyrum's law-এর ঝুঁকি সবচেয়ে বেশি: কেউ হয়তো বার্তার লেখা মিলিয়ে সিদ্ধান্ত নেন। Changelog-এ স্পষ্ট লিখুন, আর যদি দেখা যায় অনেকে লেখার ওপর নির্ভর করছেন, তাহলে তাঁদের জন্য একটি error code বা নির্দিষ্ট exception subclass দেওয়ার কথা ভাবুন।
    • (ঙ) 3.0.0 — precondition শক্ত করা মানে আগে বৈধ ডাক (যেমন ২,০০,০০০ টাকা তোলা) এখন অবৈধ: breaking. Deprecated function সরানোও breaking. দুটি একই MAJOR-এ: MINOR আর PATCH শূন্যে ফেরে।

    ক্রম: 2.3.1 → 2.3.2 → 2.4.0 → 2.5.0 → 2.5.1 → 3.0.0; পাঁচটি release-এ একটি MAJOR, দুটি MINOR, দুটি PATCH. (ঙ)-এর আগে: নতুন সীমাটি আগে MINOR-এ সতর্কবার্তা হিসেবে আনুন (সীমার বেশি হলে DeprecationWarning বা log), ব্যবহার মাপুন, migration guide লিখুন, তারপর ৩.০.০ — §৯-এর parallel change. Tag আর changelog-এর ব্যবহারিক দিক পর্ব ১৮-এ দেখেছেন।

  4. সমালোচনা করুন: একটি কাল্পনিক অনলাইন বাজার app-এর checkout-এর কোড নিচে। Error handling-এর নকশায় অন্তত পাঁচটি সমস্যা চিহ্নিত করুন, আর একটি উন্নত নকশা প্রস্তাব করুন।
    def load_price(sku):
        try:
            row = db.find(sku)
            return float(row["price"])
        except Exception:
            return -1
    
    def checkout(cart):
        total = 0
        for sku, qty in cart:
            p = load_price(sku)
            if p == None:
                print("error")
            total += p * qty
        return total
    1. সব error গিলে ফেলা: except Exception database বন্ধ থাকা, sku না পাওয়া, row-এর None হওয়া, এমনকি বানান-ভুলের NameError — সব একই -1-এ লুকায়। Programming bug আর প্রত্যাশিত ব্যর্থতা আলাদা করা যায় না।
    2. Sentinel-এর সংঘর্ষ আর অসামঞ্জস্য: load_price ব্যর্থতায় -1 দেয়, কিন্তু checkout খোঁজে None — check কখনো সত্য হয় না। ফলে -1 × qty মোটের সাথে যোগ হয়: দাম না পাওয়া পণ্য ক্রেতাকে ছাড় দেয়! এক interface-এ এক ধরনের error-এর নিয়ম (§২) ভাঙা।
    3. Fail fast নেই: ভুল ধরা পড়লেও (ধরা যাক check ঠিক হলো) কোড print করে এগিয়ে যায় আর ভুল মোট ফেরত দেয়। Caller জানতেই পারেন না কিছু ভুল হয়েছে।
    4. অর্থহীন বার্তা: “error” — কোন sku, কী সমস্যা, কী করা যায়, কিছুই নেই; আর print log বা monitoring-এ পৌঁছায় না।
    5. Precondition আর validation নেই: qty ঋণাত্মক বা শূন্য হলে কী হবে? Cart ব্যবহারকারীর কাছ থেকে আসে — এটি trust boundary, এখানে সবসময়-চলা validation দরকার।
    6. টাকার জন্য float: binary floating-point-এ দশমিকের হিসাবে ছোট ভুল জমে; পয়সায় int বা Decimal ব্যবহার করুন। আর == None-এর বদলে is None.

    উন্নত নকশা: একটি error hierarchy — CheckoutError, তার নিচে UnknownProduct(sku) আর PriceServiceDown. load_price শুধু database-এর নির্দিষ্ট exception ধরে raise PriceServiceDown(...) from e করে; row না পেলে UnknownProduct(sku); দাম ফেরত দেয় পয়সায় int. checkout-এর শুরুতে cart validation (qty ১ থেকে ধরা যাক ১০০, sku খালি নয়) — ভাঙলে ValueError, বার্তায় কোন লাইন আর কোন মান। একাধিক অজানা পণ্য একবারে দেখাতে চাইলে Result-এর মতো সব error জমিয়ে একটি CheckoutError-এ তালিকাসহ তুলুন। Postcondition হিসেবে total >= 0 একটি assert. আর caller (web স্তর) PriceServiceDown ধরে ব্যবহারকারীকে “কিছুক্ষণ পরে চেষ্টা করুন” দেখায়, UnknownProduct-এ পণ্যটি cart থেকে সরানোর প্রস্তাব দেয় — প্রতিটি ব্যর্থতার আলাদা, অর্থপূর্ণ উত্তর।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমDesign by contract মেনে যাচাই করা একটি method ডাকে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📞 Caller method ডাকেন
  2. 📥 Precondition যাচাই — মিথ্যা হলে দায় caller-এর
  3. 📸 Postcondition-এর জন্য দরকারি old মান রাখা
  4. ⚙️ Method-এর body চলে
  5. 📤 Postcondition যাচাই — মিথ্যা হলে দায় supplier-এর
  6. 🧱 Class invariant যাচাই, তারপর ফল ফেরত

একটি method-এর precondition “amount > 0” আর postcondition “balance = old balance − amount”. একটি ডাকে amount = −৫০ দেওয়ায় precondition মিথ্যা হলো। Design by contract অনুযায়ী bug কোথায়?

  • Method-এর implementation-এ (supplier)
  • ডাকার পক্ষের কোডে (caller)
  • Class invariant-এ
  • কারও নয় — এটি প্রত্যাশিত ব্যর্থতা

একটি Python function ব্যবহারকারীর দেওয়া ছাড়ের হার যাচাই করে assert 0 <= pct <= 100 দিয়ে। Server-এ প্রোগ্রামটি python -O app.py দিয়ে চালানো হলে pct = 150 এলে কী হবে?

  • আগের মতোই AssertionError ওঠে
  • AssertionError-এর বদলে ValueError ওঠে
  • Assert বাদ পড়ায় কোনো check হয় না, ১৫০% দিয়েই হিসাব চলে
  • প্রোগ্রাম চালু হওয়ার আগেই SyntaxError দেয়

JavaScript-এ const o = Object.freeze({ tags: ["a"] }); o.tags.push("b"); o.x = 1; — strict mode ছাড়া চালালে শেষে o.tags.length আর o.x কত?

  • ২ আর undefined
  • ১ আর undefined
  • ২ আর 1
  • প্রথম লাইনেই TypeError, কিছুই ছাপা হয় না

নিচের কোনটি idempotent operation — অর্থাৎ একবার করা আর বহুবার করার ফল একই?

  • balance += 500
  • add_member("Rina"), যা প্রতিবার নতুন ID দেয়
  • Log file-এ একটি লাইন append করা
  • set_status(loan_id, "PAID")

একটি library-র documentation-এ লেখা আছে, list_members() কোনো নির্দিষ্ট ক্রমে ফল দেয় না। বছরের পর বছর এটি নাম অনুযায়ী সাজানো ফল দিয়েছে। নতুন version-এ ক্রম বদলাতেই কয়েকটি client-এর report ভুল হলো। এই অভিজ্ঞতা কোন পর্যবেক্ষণের সবচেয়ে ভালো উদাহরণ?

  • Conway's law
  • Brooks's law
  • Hyrum's law
  • Liskov substitution principle

একটি Stack class-এর push(x)-এর postcondition: count = old count + 1 আর top = x. এখানে old count বলতে কী বোঝায়?

  • Stack প্রথম তৈরি হওয়ার সময় count-এর মান
  • push শুরুর ঠিক আগের মুহূর্তে count-এর মান
  • আগের push ডাক শেষ হওয়ার সময়ের মান
  • Class invariant-এ লেখা সর্বোচ্চ count

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Interface = চুক্তি (কী), implementation = কীভাবে। Implementation নয়, interface ধরে প্রোগ্রাম লিখুন; interface ছোট, সুসংহত, স্পষ্ট নামের আর ভুলভাবে ব্যবহার করা কঠিন রাখুন।
  2. Design by contract: precondition (caller-এর দায়), postcondition (supplier-এর দায়, old মানসহ), class invariant (প্রতিটি বাইরের ডাকের আগে-পরে)। Contract ভাঙা = bug; প্রত্যাশিত ব্যর্থতা চুক্তির অংশ।
  3. Assert ভেতরের অনুমানের জন্য, validation trust boundary-র জন্য। python -O assert মুছে দেয়; assert মুছলে প্রোগ্রামের সঠিকতা যেন না বদলায়।
  4. Exception, error code, Result — যেটিই নিন, error গিলবেন না, দ্রুত ব্যর্থ হোন, error-এর বংশতালিকা আর অর্থপূর্ণ বার্তা দিন। Java-র checked exception signature-এ ঘোষিত।
  5. Immutable value object (frozen dataclass, deep freeze — Object.freeze shallow), defensive copy, idempotent কাজ, null-এর বদলে Optional বা Null Object, আর expand-contract-এ interface বদল — Hyrum's law মনে রেখে।

পরের পর্বে: মডিউল ৬ শুরু — পর্ব ৩২-এ design pattern কী: Gang of Four, pattern-এর ভাষা, আর কখন pattern ব্যবহার না করাই ভালো। এই পর্বের interface আর contract-ই প্রায় প্রতিটি pattern-এর ভিত্তি।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান