সরাসরি পর্বে যান
পর্ব ৪৮ · ৫৯-এর মধ্যে · মডিউল ৭

design pattern আর clean code — পরীক্ষিত নকশা, পড়ার মতো কোড

Design Patterns and Clean Code — GoF Patterns in Modern Java, SOLID, Code Smells and Refactoring

Taught in Bangla, this lesson teaches design patterns and clean code in Java: SOLID, DRY, KISS and YAGNI, GoF patterns such as Builder, Adapter, Decorator, Facade, Strategy, Observer and State in an online shop checkout, how lambdas, records and sealed types simplify patterns, anti-patterns, code smells and step-by-step refactoring.

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

  • Design pattern আসলে কী, আর SOLID, DRY, KISS, YAGNI-এর সাথে তার সম্পর্ক
  • GoF-এর তিন পরিবার: Builder, Singleton, Adapter, Decorator, Facade, Strategy, Observer, State, Chain of Responsibility — Java-র বাস্তব উদাহরণসহ
  • Lambda, record, sealed আর Optional কীভাবে পুরোনো pattern-কে ছোট বা অপ্রয়োজনীয় করে
  • Anti-pattern আর অতিরিক্ত নকশা চেনা; code smell আর তার refactoring
  • ৩০ লাইনের বেশি একটি জট পাকানো method-কে চার ধাপে পরিষ্কার করা, আর AI-এর নকশা বিচার করা

১ · Pattern মানে নাম দেওয়া সমাধান, clean code মানে পরের পাঠকের প্রতি সম্মান

ক্রিকেট মাঠে অধিনায়ক বোলারের সাথে কথা বলে শুধু বলেন, “একটা slip আর একটা gully দাও।” দুই শব্দে পুরো একটি পরিকল্পনা বোঝানো হয়ে গেল — কোথায় দাঁড়াতে হবে, কেন, কোন ধরনের বলের জন্য। এই নামগুলো কেউ একদিনে আবিষ্কার করেনি; বহু বছরের খেলায় যে অবস্থানগুলো বারবার কাজে লেগেছে, সেগুলোর নাম হয়ে গেছে। নতুন খেলোয়াড় নামগুলো শিখলেই অভিজ্ঞদের ভাষায় কথা বলতে পারেন।

সফটওয়্যারের design pattern ঠিক এমন। এটা কোনো library নয়, কপি করে বসানোর কোডও নয়। এটা একটি বারবার আসা নকশা-সমস্যার পরীক্ষিত সমাধান-ধাঁচ, যার একটি নাম আছে। “ডেলিভারি চার্জের নিয়ম শহরভেদে বদলায়, আর নতুন নিয়ম প্রায়ই আসে” — এই সমস্যার ধাঁচটির নাম Strategy. সহকর্মীকে “এখানে Strategy বসাও” বললে তিনি সাথে সাথে বোঝেন: একটি interface, কয়েকটি বদলযোগ্য implementation, আর মূল কোড শুধু interface-কে চেনে। প্রতিটি pattern-এর তিনটি অংশ থাকে — কোন পরিস্থিতিতে, কোন সমস্যা (প্রায়ই দুটি টানাপোড়েনের মধ্যে), আর সমাধানের আকার ও তার দাম।

ঢাকার যেকোনো পুরোনো পাড়ায় ঘুরলে দেখবেন বাড়িগুলো আলাদা, কিন্তু কিছু সমাধান বারবার ফিরে আসে: সিঁড়িঘরের পাশে খোলা বারান্দা, রাস্তার দিকে দোকান আর ওপরে বাসা, ছাদে পানির ট্যাংক। কোনো রাজমিস্ত্রি নকশা কপি করেননি — একই সমস্যা (গরম, জায়গার অভাব, পানির চাপ) একই আকারের সমাধানের দিকে ঠেলে দিয়েছে। স্থপতি Christopher Alexander ঠিক এই পর্যবেক্ষণ থেকে “pattern” শব্দটি দাঁড় করান, আর সেখান থেকেই ধারণাটি সফটওয়্যারে আসে (ভাবনার প্রশ্ন ১)।

এবার clean code. কোড একবার লেখা হয়, কিন্তু পড়া হয় শতবার — bug খুঁজতে, নতুন feature যোগ করতে, রিভিউ করতে। Clean code-এর মাপকাঠি একটাই: পরের পাঠক কত তাড়াতাড়ি বুঝে কত নিরাপদে বদলাতে পারবেন। সেই পাঠক প্রায়ই ছয় মাস পরের আপনি নিজে, নতুন যোগ দেওয়া সহকর্মী, অথবা একটি AI সহকারী, যে আপনার কোড পড়ে পরামর্শ দেবে। অস্পষ্ট নাম আর জট পাকানো method মানুষকে যেমন বিভ্রান্ত করে, AI-কেও ভুল পথে নেয়।

মূল ধারণা

Pattern নীতির সেবক। আগে প্রশ্ন: “কোন জিনিসটা বদলাবে, আর সেই বদল যেন বাকি কোডে না ছড়ায় — কীভাবে?” সেই উত্তরের একটি চেনা আকার থাকলে তার নামই pattern. সমস্যা না থাকলে pattern শুধু বাড়তি class.

২ · আগে নীতি — SOLID, DRY, KISS আর YAGNI এক টেবিলে

এই কোর্সে আপনি এর মধ্যে বেশিরভাগ নীতির সাথে আলাদা আলাদা পর্বে দেখা করেছেন। এখানে সবগুলো এক জায়গায়, কারণ প্রতিটি pattern আসলে এদের কোনো একটিকে রক্ষা করার কৌশল। SOLID নামের পাঁচটি নীতি জনপ্রিয় করেন Robert C. Martin, আর আদ্যক্ষর দিয়ে নামটি দেন Michael Feathers.

নীতিএক লাইনেকোথায় দেখেছেন / কোন pattern সাহায্য করে
S — Single Responsibilityএকটি class-এর বদলানোর কারণ একটিইপর্ব ০২-এর দায়িত্বে ভাঙা; Facade, Observer
O — Open/Closedনতুন আচরণ যোগ হবে নতুন কোডে, পুরোনো কোড না খুলেপর্ব ২০; Strategy, Decorator, Observer
L — Liskov Substitutionchild-কে parent-এর জায়গায় বসালে কিছু ভাঙবে নাপর্ব ১৯; সব interface-ভিত্তিক pattern-এর ভিত্তি
I — Interface Segregationমোটা interface নয়, ছোট নির্দিষ্ট interfaceপর্ব ২১-এর ছোট চুক্তি; Adapter
D — Dependency Inversionওপরের স্তর চুক্তির ওপর নির্ভর করবে, concrete class-এর ওপর নয়পর্ব ২১; Adapter, Strategy, dependency injection
DRYএকই জ্ঞান বা নিয়ম এক জায়গায়Template Method; তবে “দেখতে একই” কোড জোর করে জোড়া নয়
KISSকাজ চলে এমন সবচেয়ে সরল সমাধানপ্রায়ই pattern-এর বিপক্ষে যুক্তি
YAGNI“হয়তো লাগবে” ভেবে আজ বানাবেন নাপর্ব ২১-এর ভাবনার প্রশ্ন ৫; অতিরিক্ত নকশার ওষুধ

খেয়াল করুন শেষ দুটি সারি। প্রথম পাঁচটি নীতি বলে “বদলের জন্য জায়গা রাখুন”, শেষ দুটি বলে “যে বদল আসেনি, তার জন্য জায়গা বানাবেন না”। এই দুইয়ের টানাপোড়েনেই নকশার আসল বিচার। একটা কাজের নিয়ম: একই ধরনের বদল দ্বিতীয়বার এলে pattern ভাবুন, প্রথমবারেই নয়। অনেকে আরও সতর্ক নিয়ম মানেন, যার নাম “rule of three”: দ্বিতীয়বার শুধু খেয়াল রাখুন, তৃতীয়বার একই জিনিস লিখতে গেলে তখন abstraction বানান।

আরেকটি শব্দ পুরো পর্বে বারবার আসবে: coupling আর cohesion. Coupling মানে দুটি অংশ একে অপরের কতটা গভীরে জানে; cohesion মানে একটি class-এর ভেতরের জিনিসগুলো কতটা একই কাজের। ভালো নকশা = কম coupling, বেশি cohesion. প্রায় প্রতিটি pattern এই দুটির কোনো একটিকে ঠিক করে।

৩ · তিন পরিবার, আর প্রথমটি — object বানানোর pattern

১৯৯৪ সালে Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides — যাঁদের ডাকনাম Gang of Four (GoF) — Design Patterns বইয়ে ২৩টি pattern তিনটি পরিবারে সাজান। আজও এই ভাগই সবচেয়ে প্রচলিত:

Creational

creational pattern — object কীভাবে তৈরি হবে, সেটা ব্যবহারকারীর কাছ থেকে লুকায়। Factory Method, Builder, Singleton, Abstract Factory, Prototype.

“কে, কখন, কোনটা বানাবে”

Structural

structural pattern — object-গুলো কীভাবে জোড়া লাগবে। Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight.

“কে কাকে মুড়ে রাখবে”

Behavioral

behavioral pattern — object-গুলো কীভাবে কাজ ভাগ করে আর কথা বলে। Strategy, Observer, Template Method, Command, State, Chain of Responsibility, Iterator, Visitor…

“কে কাকে কখন ডাকবে”

Factory Method আর static factory — সংক্ষেপে ফিরে দেখা

পর্ব ১৬-এ static factory method দেখেছেন: List.of(...), Integer.valueOf(42), Optional.empty(). Constructor-এর বদলে নামওয়ালা একটি method object দেয় — নাম দিয়ে উদ্দেশ্য বোঝায়, cache থেকে পুরোনো object ফেরত দিতে পারে, এমনকি subclass-এর object-ও দিতে পারে। GoF-এর আসল Factory Method একটু আলাদা: parent class একটি overridable method রাখে, কোন concrete object বানানো হবে সেটা subclass ঠিক করে। আধুনিক Java-তে দুটোর কাজই প্রায়ই একটি Supplier<T> বা static method দিয়ে সেরে ফেলা যায়।

Builder — telescoping constructor-এর ওষুধ

পর্ব ১৬-এর telescoping constructor মনে আছে? new Order("Rahim", "017...", "Rajshahi", null, false, true) — কোন true কিসের, কেউ বলতে পারে না। Builder বাধ্যতামূলক মান নেয় শুরুতে, ঐচ্ছিক মানগুলো নামওয়ালা method-এ, আর শেষে build() একবারে যাচাই করে অপরিবর্তনীয় object দেয়:

record Order(String customer, String phone, String city, List<String> items,
             String coupon, boolean giftWrap) {

    static Builder builder(String customer, String phone) {
        return new Builder(customer, phone);
    }

    static final class Builder {
        private final String customer, phone;          // বাধ্যতামূলক
        private String city = "Dhaka";                 // ঐচ্ছিক, ডিফল্টসহ
        private final List<String> items = new ArrayList<>();
        private String coupon;
        private boolean giftWrap;

        private Builder(String customer, String phone) {
            this.customer = customer;
            this.phone = phone;
        }
        Builder city(String c)   { city = c; return this; }
        Builder item(String i)   { items.add(i); return this; }
        Builder coupon(String c) { coupon = c; return this; }
        Builder giftWrap()       { giftWrap = true; return this; }

        Order build() {                                // যাচাই এক জায়গায়
            if (items.isEmpty()) throw new IllegalStateException("order has no items");
            return new Order(customer, phone, city, List.copyOf(items), coupon, giftWrap);
        }
    }
}

// ব্যবহার — পড়লেই বোঝা যায়
Order o = Order.builder("Rahim", "01711000000")
        .city("Rajshahi")
        .item("Panjabi").item("Attar")
        .giftWrap()
        .build();

প্রতিটি setter-ধাঁচের method this ফেরত দেয় বলে ডাকগুলো শিকলের মতো জোড়া যায় — এটাই fluent interface. List.copyOf দিয়ে তালিকার অপরিবর্তনীয় কপি নেওয়া হয়েছে, যাতে build()-এর পর builder-এ নতুন item যোগ করলেও তৈরি হওয়া Order না বদলায়। JDK-তেও Builder আছে: StringBuilder, Java 11-এর HttpRequest.newBuilder(), Java 21-এর Thread.ofVirtual().name("worker").start(task) (পর্ব ৪২)। চার-পাঁচটির বেশি parameter, যার অনেকগুলো ঐচ্ছিক — তখনই Builder; দুই-তিনটি হলে record-এর সাধারণ constructor যথেষ্ট।

Singleton — সবচেয়ে বিখ্যাত, সবচেয়ে বেশি অপব্যবহৃত

Singleton নিশ্চিত করে পুরো প্রোগ্রামে একটি class-এর একটিমাত্র object, আর সবাই তাকে একটি global পথে পায়। Java-তে লেখার সবচেয়ে নিরাপদ উপায় হলো একটি উপাদানের enum — Joshua Bloch-এর Effective Java বইও এটাই সুপারিশ করে:

enum AppConfig {
    INSTANCE;
    private final Map<String, String> values = new HashMap<>();
    String get(String key) { return values.getOrDefault(key, ""); }
}

String sender = AppConfig.INSTANCE.get("sms.sender");

JVM নিজেই নিশ্চিত করে enum constant একবারই তৈরি হয় — thread-safe, আর serialization বা reflection দিয়ে দ্বিতীয় কপি বানানো যায় না। new AppConfig() লিখলে compiler আটকায়: error: enum classes may not be instantiated.

তবু Singleton প্রায়ই একটি anti-pattern. পর্ব ১৮-এ দেখেছেন, static global state লুকানো নির্ভরতা তৈরি করে: OrderService-এর constructor দেখে বোঝা যায় না যে সে ভেতরে AppConfig.INSTANCE ছোঁয়। টেস্টে আলাদা config বসানো কঠিন, আর এক টেস্টের বদল আরেক টেস্টে চুইয়ে পড়ে। বিকল্প সহজ: object একটিই বানান (main-এ বা Spring-এর মতো framework দিয়ে), তারপর constructor দিয়ে যার দরকার তাকে দিন। “একটি object” থাকল, কিন্তু “global পথে পাওয়া” থাকল না। Spring-এর মতো framework-এ bean ডিফল্টভাবে ঠিক এভাবেই “একটি object” থাকে, কিন্তু global পথে নয়।

৪ · জোড়া লাগানোর pattern — Adapter, Decorator, Facade, Proxy

এবার একটি অনলাইন দোকানের checkout ধরুন — এই পর্বের বাকি উদাহরণ এর ওপরেই। এখানে চারটি structural pattern স্বাভাবিকভাবে এসে পড়ে।

Adapter — তৃতীয় পক্ষের SMS API-কে নিজের চুক্তিতে আনা

দোকানের কোড পর্ব ২১-এর মতো একটি ছোট চুক্তি চেনে: SmsSender.send(phone, text). কিন্তু যে SMS কোম্পানির সাথে চুক্তি হলো, তাদের Java library-র method দেখতে সম্পূর্ণ আলাদা — API key লাগে, নম্বর লাগে দেশের কোডসহ, আর সফল হলে 202 ফেরত দেয়। ওদের কোড আপনি বদলাতে পারবেন না, নিজের সারা কোডে ওদের ভাষাও ছড়াতে চান না। সমাধান Adapter — বিদেশি প্লাগের জন্য একটি কনভার্টার:

// তৃতীয় পক্ষের library — এটা আপনি বদলাতে পারবেন না
class QuickSmsClient {
    int dispatch(String apiKey, String msisdn, String body, String mask) { ... }
}

interface SmsSender { boolean send(String phone, String text); }   // আপনার চুক্তি

class QuickSmsAdapter implements SmsSender {
    private final QuickSmsClient client;
    private final String apiKey;
    QuickSmsAdapter(QuickSmsClient client, String apiKey) {
        this.client = client;
        this.apiKey = apiKey;
    }
    public boolean send(String phone, String text) {
        String msisdn = "88" + phone;                    // 017... → 88017...
        int status = client.dispatch(apiKey, msisdn, text, "ABCL");
        return status == 202;                            // ওদের "সফল" কোড
    }
}

এখন বাইরের কোম্পানির সব অদ্ভুততা এক class-এ বন্দি। কাল কোম্পানি বদলালে শুধু নতুন একটি adapter লিখবেন; OrderService একটি অক্ষরও জানবে না। JDK-তে চেনা উদাহরণ: InputStreamReader byte-এর stream-কে character-এর Reader-এ রূপ দেয়, আর Arrays.asList একটি array-কে List-এর চেহারা দেয়।

Decorator — মূল জিনিস না বদলে চারপাশে ক্ষমতা যোগ করা

পেমেন্ট gateway মাঝে মাঝে timeout দেয়। আপনি চান: (১) প্রতিটি চার্জ log হবে, (২) timeout হলে আরেকবার চেষ্টা হবে। Gateway class-এর ভেতরে এসব লিখলে তার দায়িত্ব বাড়ে, আর অন্য gateway-তে আবার লিখতে হয়। Decorator বলে: একই interface implement করা একটি মোড়ক বানান, যে ভেতরের object-কে ডাকে আর আগে-পরে নিজের কাজটুকু করে। মোড়কের ওপর মোড়ক বসানো যায়। পর্ব ৯ আর ৩৭-এ এর সবচেয়ে বিখ্যাত রূপ দেখেছেন: new BufferedReader(new InputStreamReader(in)) — প্রতিটি স্তর একটি করে ক্ষমতা যোগ করে।

interface PaymentGateway { String charge(int amount); }

class FlakyBank implements PaymentGateway {        // প্রথম ডাকে timeout
    private int calls = 0;
    public String charge(int amount) {
        if (++calls == 1) throw new IllegalStateException("timeout");
        return "OK-" + amount;
    }
}
record Logging(PaymentGateway inner) implements PaymentGateway {
    public String charge(int amount) {
        System.out.println("  log: charge " + amount);
        return inner.charge(amount);
    }
}
record Retry(PaymentGateway inner) implements PaymentGateway {
    public String charge(int amount) {
        try { return inner.charge(amount); }
        catch (IllegalStateException e) {
            System.out.println("retry: " + e.getMessage() + ", once more");
            return inner.charge(amount);
        }
    }
}
class Main {
    public static void main(String[] args) {
        PaymentGateway gw = new Retry(new Logging(new FlakyBank()));
        System.out.println("final: " + gw.charge(1560));
    }
}

আউটপুট চার লাইন: log: charge 1560, retry: timeout, once more, আবার log: charge 1560, শেষে final: OK-1560. FlakyBank ইচ্ছে করে প্রথম ডাকে ব্যর্থ হয়, যাতে আউটপুট প্রতিবার একই থাকে। Logging আর Retry record — ভেতরের gateway-টিই তাদের একমাত্র component, তাই constructor লিখতে হয়নি (পর্ব ২৫)। main জানে শুধু PaymentGateway; ভেতরে কয়টি স্তর আছে, তার কিছুই সে জানে না। বাস্তবে retry-র আগে অপেক্ষা আর সর্বোচ্চ চেষ্টার সংখ্যা থাকে, আর পেমেন্টে retry করার আগে নিশ্চিত হতে হয় যে একই চার্জ দুবার কাটা হবে না।

ডাকটি কোন পথে ভেতরে ঢোকে আর কোন পথে বেরোয়, ধাপে ধাপে দেখুন:

  1. maingw.charge(1560)শুধু interface চেনে
  2. Retryবাইরের মোড়কব্যর্থ হলে আরেকবার
  3. Loggingমাঝের মোড়কপ্রতিটি ডাক লিখে রাখে
  4. FlakyBankআসল gatewayপ্রথমবার timeout
ধাপ ১আউটপুট এখন পর্যন্ত
(কিছু ছাপা হয়নি)
main → Retry.charge(1560)
ধাপ ২আউটপুট এখন পর্যন্ত
  log: charge 1560
ধাপ ৩exception বাইরের দিকে
  log: charge 1560
FlakyBank ✗ IllegalStateException("timeout")
→ Logging ধরে না, পার হয়ে যায় → Retry
ধাপ ৪Retry ধরে আবার ডাকে
  log: charge 1560
retry: timeout, once more
  log: charge 1560
ধাপ ৫ফল ফিরে আসছে
FlakyBank → "OK-1560"
Logging → "OK-1560" (অপরিবর্তিত)
Retry → "OK-1560"
ধাপ ৬পুরো আউটপুট
  log: charge 1560
retry: timeout, once more
  log: charge 1560
final: OK-1560
  1. main ডাকে সবচেয়ে বাইরের মোড়ককে। ভ্যারিয়েবল gw-এর টাইপ PaymentGateway, কিন্তু আসল object Retry. সে নিজে কিছু ছাপে না, শুধু ভেতরের দিকে ডাক পাঠায়।
  2. Logging নিজের কাজ করে ডাক আরও ভেতরে পাঠায়। প্রতিটি মোড়ক একই চুক্তি মানে, তাই Retry জানে না তার ভেতরে Logging না আসল ব্যাংক।
  3. আসল gateway ব্যর্থ। Exception বাইরের দিকে উঠতে থাকে। Logging-এ কোনো catch নেই, তাই সেটা সোজা পার হয়ে Retry-তে পৌঁছায়।
  4. Retry ধরে ফেলে, আবার ভেতরে ডাকে। দ্বিতীয় ডাকও Logging-এর মধ্য দিয়ে যায়, তাই log লাইন আবার ছাপা হয় — Logging প্রতিটি চেষ্টা দেখে।
  5. এবার সফল। "OK-1560" একই পথে উল্টো দিকে ফেরে। কোনো মোড়ক চাইলে ফল বদলাতে বা cache করতে পারত — caching decorator ঠিক এটাই করে।
  6. main পায় চূড়ান্ত ফল। ক্রম বদলালে আচরণ বদলায়: new Logging(new Retry(...)) হলে log লাইন একবারই ছাপা হতো, কারণ তখন retry ঘটে Logging-এর ভেতরে। মোড়কের ক্রম একটি নকশার সিদ্ধান্ত।

Facade — জটিল ব্যবস্থার সামনে একটি সরল দরজা

অর্ডার নিশ্চিত করতে চারটি subsystem লাগে: স্টক সংরক্ষণ, পেমেন্ট, কুরিয়ার বুকিং, আর খবর পাঠানো। Controller বা অ্যাপের বোতাম যদি নিজেই এই চারটিকে ঠিক ক্রমে ডাকে, তাহলে সেই ক্রম দশ জায়গায় ছড়িয়ে পড়বে। Facade একটি সরল class, যার একটি method পুরো কাজ ঠিক ক্রমে করে দেয়:

class CheckoutService {
    private final Inventory inventory;
    private final PaymentGateway payment;      // ভেতরে Retry(Logging(...)) থাকতে পারে
    private final Shipping shipping;
    private final OrderEvents events;

    CheckoutService(Inventory i, PaymentGateway p, Shipping s, OrderEvents e) {
        inventory = i; payment = p; shipping = s; events = e;
    }
    Receipt checkout(Cart cart, String address) {
        inventory.reserve(cart.items());
        String txn = payment.charge(cart.total());
        String tracking = shipping.book(cart.items(), address);
        events.publish(cart.id(), OrderStatus.PLACED);
        return new Receipt(txn, tracking);
    }
}

বাইরের জগৎ জানে শুধু checkout(cart, address). সব নির্ভরতা constructor দিয়ে আসছে — এটাই dependency injection, তাই টেস্টে প্রতিটির নকল বসানো যায় (পর্ব ৪৭)। Facade-এর ঝুঁকি একটাই: ধীরে ধীরে সব কাজ এতে ঢুকে পড়লে সে নিজেই একটি God class হয়ে ওঠে। Facade শুধু সমন্বয় করবে; আসল যুক্তি থাকবে subsystem-গুলোতে।

Proxy — একই চেহারার প্রতিনিধি

Proxy দেখতে Decorator-এর মতো — একই interface, ভেতরে আসল object. পার্থক্য উদ্দেশ্যে: Decorator নতুন ক্ষমতা যোগ করে, Proxy আসল object-এ পৌঁছানো নিয়ন্ত্রণ করে — দেরিতে তৈরি করা (lazy), অনুমতি যাচাই, দূরের সার্ভারের প্রতিনিধিত্ব। পর্ব ৪৫-এ দেখেছেন, java.lang.reflect.Proxy দিয়ে চলার সময় এমন dynamic proxy বানানো যায়, আর Spring-এর @Transactional এভাবেই আপনার method-এর চারপাশে transaction শুরু ও শেষ করে।

৫ · আচরণের pattern (১) — Strategy আর Observer

Strategy — বদলযোগ্য নিয়ম আলাদা object-এ

ডেলিভারি চার্জ: ঢাকার ভেতরে ৬০ টাকা (২০০০ টাকা বা বেশি কেনাকাটায় ফ্রি), ঢাকার বাইরে ১২০ টাকা, ঈদে সবার জন্য ফ্রি। if-else-এর শিকল লিখলে প্রতিটি নতুন অফারে checkout-এর কোড খুলতে হবে — Open/Closed ভাঙা। Strategy বলে: নিয়মটিকে একটি interface বানান, প্রতিটি নিয়ম একটি implementation, আর checkout শুধু interface-কে ডাকুক। JDK-র সবচেয়ে চেনা Strategy হলো Comparator — list.sort(...) একই থাকে, সাজানোর নিয়ম বাইরে থেকে আসে (পর্ব ৩৩)।

import java.util.List;

interface DeliveryFee { int feeFor(int subtotal); }

class InsideDhaka implements DeliveryFee {
    public int feeFor(int subtotal) { return subtotal >= 2000 ? 0 : 60; }
}
class OutsideDhaka implements DeliveryFee {
    public int feeFor(int subtotal) { return 120; }
}
record Checkout(String label, DeliveryFee fee) {
    int total(int subtotal) { return subtotal + fee.feeFor(subtotal); }
}
class Main {
    public static void main(String[] args) {
        var options = List.of(
            new Checkout("Dhaka", new InsideDhaka()),
            new Checkout("Outside", new OutsideDhaka()),
            new Checkout("Eid", subtotal -> 0));       // lambda = নতুন strategy
        for (var c : options)
            System.out.println(c.label() + ": " + c.total(1500) + ", " + c.total(2500));
    }
}

আউটপুট: Dhaka: 1560, 2500, Outside: 1620, 2620, Eid: 1500, 2500. Checkout.total() একটি লাইন, আর তিনটি ভিন্ন নিয়মের জন্য সেটা একবারও বদলায়নি। তৃতীয় নিয়মের জন্য কোনো class লেখা হয়নি: DeliveryFee-এ abstract method একটিই, তাই এটি একটি functional interface, আর subtotal -> 0 lambda-টিই পুরো strategy (পর্ব ৩৪)। যে নিয়মের নিজস্ব নাম, state বা টেস্ট লাগবে, তার জন্য class; এক লাইনের নিয়মের জন্য lambda.

একই total(), ভিন্ন strategy — কোন কোড কখন চলে, দেখুন:

1interface DeliveryFee { int feeFor(int s); }2class InsideDhaka implements DeliveryFee {3  public int feeFor(int s) { return s >= 2000 ? 0 : 60; } }4class OutsideDhaka implements DeliveryFee {5  public int feeFor(int s) { return 120; } }6int total(int subtotal, DeliveryFee fee) {7  return subtotal + fee.feeFor(subtotal); }8total(1500, new InsideDhaka());9total(1500, new OutsideDhaka());10total(2500, new InsideDhaka());11total(1500, s -> 0);   // lambda
ডাকের ভেতরে
subtotal
fee (আসল object)
fee.feeFor(...)
ফেরত মান
  1. Strategy বাইরে থেকে আসে। total()-এর parameter-এর টাইপ DeliveryFee; কোন নিয়ম আসবে, সেটা ডাকার জায়গা ঠিক করে। Checkout-এর কোডে কোনো if (city == ...) নেই।
  2. লাইন ৭ চলার সময় ঠিক করে কোন কোড চলবে। fee আসলে একটি InsideDhaka object, তাই পর্ব ২০-এর dynamic dispatch তার feeFor চালায়।
  3. একই লাইন, অন্য নিয়ম। লাইন ৭-এ একটি অক্ষরও বদলায়নি, শুধু ভেতরে যে object এসেছে সেটা বদলেছে। নতুন শহরের নিয়ম মানে নতুন একটি class, পুরোনো কোডে হাত নেই।
  4. নিয়মের খুঁটিনাটি strategy-র ভেতরে। “২০০০ টাকায় ফ্রি” — এই জ্ঞান শুধু InsideDhaka জানে। কাল সীমা বদলালে এক জায়গায় বদল।
  5. Lambda — এক লাইনের strategy. DeliveryFee-এ abstract method একটি, তাই s -> 0 লিখলেই compiler একটি DeliveryFee object বানিয়ে দেয়। Java 8-এর আগে এর জন্য একটি পুরো class বা anonymous class লাগত — Strategy pattern-এর বেশিরভাগ “অনুষ্ঠান” lambda মুছে দিয়েছে।

Observer — একটি ঘটনা, অনেক শ্রোতা

অর্ডারের অবস্থা বদলালে (রওনা হলো, পৌঁছাল) গ্রাহককে SMS, অফিসে Email, আর হিসাবের জন্য analytics — তিন জায়গায় খবর যাবে। Order-এর কোড যদি এই তিনটিকে নিজে ডাকে, তাহলে চতুর্থ চ্যানেল (WhatsApp) যোগ করতে order-এর কোড খুলতে হবে, আর order জানবে SMS-এর খুঁটিনাটি। Observer উল্টো দিক থেকে ভাবে: যারা খবর চায়, তারা নিজেরাই নাম লেখায় (subscribe); ঘটনা ঘটলে প্রকাশক শুধু তালিকা ধরে সবাইকে জানায়। পর্ব ২৭-এর event listener ঠিক এই pattern.

interface OrderListener { void onStatusChange(String orderId, OrderStatus status); }

class OrderEvents {
    private final List<OrderListener> listeners = new ArrayList<>();
    void subscribe(OrderListener l) { listeners.add(l); }
    void publish(String orderId, OrderStatus status) {
        for (OrderListener l : listeners) l.onStatusChange(orderId, status);
    }
}

events.subscribe((id, st) -> sms.send(phone, "Order " + id + ": " + st));
events.subscribe((id, st) -> System.out.println("email: order " + id + " is " + st));
events.subscribe((id, st) -> { if (st == OrderStatus.DELIVERED) analytics.count(id); });
  1. OrderEventslisteners: [3]শুধু তালিকা চেনে
  2. SMSশ্রোতা ১গ্রাহককে
  3. Emailশ্রোতা ২অফিসে
  4. Analyticsশ্রোতা ৩শুধু DELIVERED
ধাপ ১নাম লেখানো
subscribe(SMS) → subscribe(Email) → subscribe(Analytics)
তালিকা: [SMS, Email, Analytics]
ধাপ ২publish("A-17", SHIPPED)
SMS → 017...: Order A-17: SHIPPED
email: order A-17 is SHIPPED
Analytics: SHIPPED, তাই কিছু করল না
ধাপ ৩publish("A-17", DELIVERED)
SMS → 017...: Order A-17: DELIVERED
email: order A-17 is DELIVERED
analytics: delivered +1
ধাপ ৪নতুন চ্যানেল যোগ
events.subscribe(whatsApp);
OrderEvents-এর কোড: ০ লাইন বদল
  1. শ্রোতারা নিজেরাই নাম লেখায়। OrderEvents-এর কাছে শুধু একটি List<OrderListener>. সে জানে না কে SMS পাঠায়, কে Email.
  2. একটি publish, তালিকার ক্রমে সবার কাছে খবর। ArrayList-এ রাখা বলে নাম লেখানোর ক্রমেই ডাক যায় — আউটপুট নির্দিষ্ট থাকে।
  3. প্রতিটি শ্রোতা নিজে ঠিক করে কী করবে। Analytics শুধু DELIVERED-এ সাড়া দেয়। ছাঁকার যুক্তি প্রকাশকের নয়, শ্রোতার।
  4. নতুন চ্যানেল = নতুন subscribe. প্রকাশকের কোড বন্ধ, কিন্তু ব্যবস্থা খোলা — Open/Closed. সাবধানতা: একটি শ্রোতা exception ছুড়লে এই সরল লুপে বাকিরা খবর পাবে না, আর ধীর শ্রোতা সবাইকে আটকে রাখবে। বাস্তবে প্রতিটি ডাক try-catch-এ মোড়া হয়, বা আলাদা thread/queue-তে পাঠানো হয়।

JDK-তে পুরোনো java.util.Observer/Observable আছে, কিন্তু Java 9 থেকে deprecated — কারণ Observable একটি class (interface নয়) হওয়ায় অন্য parent-ওয়ালা class সেটা নিতে পারত না, আর ঘটনার ধরন বোঝানোর ভালো উপায় ছিল না। এখন GUI-ধাঁচের কাজে java.beans.PropertyChangeListener, আর অনেক ঘটনার প্রবাহ ও চাপ সামলানোর (backpressure) জন্য Java 9-এর java.util.concurrent.Flow interface-গুলো আছে — reactive library-গুলো সেই চুক্তিই মানে।

৬ · আচরণের pattern (২) — State, Chain of Responsibility আর পুরোনো বন্ধুরা

State — object-এর অবস্থা অনুযায়ী আচরণ

অর্ডার “Placed” অবস্থায় বাতিল করা যায়, “Shipped” হলে আর যায় না, “Delivered” হলে কিছুই বদলায় না। প্রতিটি method-এ if (status == ...) লিখলে নিয়মগুলো সারা class-এ ছড়িয়ে পড়ে। State pattern প্রতিটি অবস্থাকে একটি object বানায়, যে নিজেই জানে সে কী অনুমতি দেয় আর পরের অবস্থা কী:

sealed interface OrderState permits Placed, Shipped, Delivered, Cancelled {
    OrderState next();
    OrderState cancel();
}
record Placed() implements OrderState {
    public OrderState next()   { return new Shipped(); }
    public OrderState cancel() { return new Cancelled(); }
}
record Shipped() implements OrderState {
    public OrderState next()   { return new Delivered(); }
    public OrderState cancel() { throw new IllegalStateException("already shipped"); }
}
// Delivered আর Cancelled একইভাবে ...

পর্ব ২৪-এ একই কাজ enum দিয়ে করেছিলেন — state machine, যেখানে প্রতিটি constant-এর নিজস্ব শরীর থাকে। কখন কোনটা? অবস্থাগুলোর কোনো নিজস্ব ডেটা না থাকলে enum সবচেয়ে সরল ও যথেষ্ট। কোনো অবস্থার নিজস্ব ডেটা লাগলে — যেমন Shipped-এর tracking নম্বর, Cancelled-এর কারণ — তখন sealed interface + record, কারণ enum constant-এর ডেটা সব object-এর জন্য একই। দুটোই State pattern-এর Java-রূপ; GoF-এর মূল সংস্করণের মতো আলাদা context class প্রায়ই লাগে না।

Chain of Responsibility — অনুরোধ এক হাত থেকে আরেক হাতে

অর্ডার গ্রহণের আগে কয়েকটি যাচাই: কার্ট খালি কি না, ফোন নম্বর ঠিক কি না, অঙ্কটা হাতে-যাচাইয়ের মতো বড় কি না। প্রতিটি যাচাই একটি আলাদা handler; অনুরোধ শিকল ধরে এগোয়, যেকোনো handler থামিয়ে দিতে পারে। এটাই Chain of Responsibility. আধুনিক Java-তে একটি তালিকা আর stream দিয়েই লেখা যায়:

interface Check { Optional<String> validate(Req r); }   // খালি = পাস

List<Check> chain = List.of(
    r -> r.items().isEmpty() ? Optional.of("cart empty") : Optional.empty(),
    r -> r.phone().matches("01\\d{9}") ? Optional.empty() : Optional.of("bad phone"),
    r -> r.total() > 50_000 ? Optional.of("needs manual review") : Optional.empty());

Optional<String> problem = chain.stream()
        .map(c -> c.validate(req))
        .flatMap(Optional::stream)      // প্রথম সমস্যা পেলেই থামে
        .findFirst();

Stream lazy (পর্ব ৩৫), তাই প্রথম যে check সমস্যা পায়, তারপরের check আর চলেই না। নতুন নিয়ম মানে তালিকায় নতুন একটি lambda. Java-র ওয়েব জগতে এর সবচেয়ে বড় উদাহরণ Servlet Filter: প্রতিটি filter কাজ করে chain.doFilter(request, response) ডেকে অনুরোধ পরেরজনকে দেয়, বা না ডেকে থামিয়ে দেয়। Spring Security-র login, CSRF আর অনুমতি যাচাইও এমন একটি filter-শিকল।

যাদের আগেই চেনেন

Patternমূল কথাকোথায় দেখেছেন
Template Methodparent ধাপের ক্রম ঠিক করে, child একটি ধাপ পূরণ করেপর্ব ২১-এর ReportGenerator, InputStream
Commandএকটি কাজকে object বানানো — রাখা, সারিতে দেওয়া, undo করা যায়পর্ব ২২-এর CLI টুল; Runnable, executor-এ জমা দেওয়া কাজ (পর্ব ৪১)
Iteratorভেতরের গঠন না দেখিয়ে একে একে উপাদান দেওয়াপর্ব ১১-এর for-each, Iterator/Iterable
Proxyএকই চেহারার প্রতিনিধি, পৌঁছানো নিয়ন্ত্রণ করেপর্ব ৪৫-এর dynamic proxy

এখন পুরো checkout এক নজরে: বোতাম ডাকে Facade CheckoutService-কে; চার্জ হিসাব করে Strategy DeliveryFee; টাকা কাটে Decorator-মোড়া Retry(Logging(gateway)); ঘটনা ছড়ায় Observer OrderEvents; আর SMS যায় Adapter QuickSmsAdapter দিয়ে। পাঁচটি pattern, কিন্তু প্রতিটির পেছনে একটি বাস্তব বদলের কারণ আছে — সেটাই তাদের থাকার অধিকার দেয়।

৭ · আধুনিক Java pattern বদলে দিয়েছে — আর যখন pattern নিজেই সমস্যা

GoF বইটি লেখা হয়েছিল ১৯৯৪ সালের C++ আর Smalltalk মাথায় রেখে। অনেক pattern আসলে ভাষার কোনো অভাব পূরণের কৌশল ছিল। ভাষা বদলালে সেই অভাব কমে, pattern-ও ছোট হয়:

পুরোনো রূপআধুনিক Javaকী বদলাল
Strategy/Command: interface + প্রতিটি নিয়মে একটি classlambda, method reference (Java 8 থেকে)এক লাইনের নিয়মে class লাগে না
Visitor: accept/visit double dispatchsealed interface + record + switch pattern matching (Java 21-এ স্থায়ী)নতুন অপারেশন = নতুন একটি method-এ switch; সব ঘর ধরা হয়েছে কি না compiler যাচাই করে (পর্ব ২৬)
Null Object: “কিছু নেই” বোঝাতে নিরীহ objectOptional (পর্ব ৩৬)“না থাকতে পারে” টাইপেই লেখা; তবে আচরণ-ভিত্তিক Null Object (যেমন কিছু-না-করা listener) এখনো কাজের
Builder: হাতে লেখা অনেক boilerplaterecord + compact constructor; ছোট ক্ষেত্রে শুধু recordদুই-তিনটি field-এ Builder লাগেই না
Iterator: হাতে লেখা hasNext/nextStream API, for-eachপ্রায় কখনো নিজে লিখতে হয় না

Anti-pattern — যে সমাধান সমস্যা তৈরি করে

Anti-pattern হলো এমন একটি ধাঁচ, যা শুরুতে যুক্তিসংগত মনে হয় কিন্তু বারবার একই ক্ষতি করে। এই পর্বের প্রসঙ্গে চারটি:

  • God class: OrderManager নামের একটি class স্টক, পেমেন্ট, SMS, রিপোর্ট, ডিসকাউন্ট — সব করে, তিন হাজার লাইন। কম cohesion, সবার সাথে coupling. সমাধান: দায়িত্ব ধরে ভাঙা (পর্ব ০২)।
  • Singleton-এর অপব্যবহার: “সুবিধার জন্য” সবকিছু XxxManager.getInstance(). আসলে এটা ছদ্মবেশী global ভ্যারিয়েবল — অংশ ৩-এর সতর্কতা দেখুন।
  • Over-engineering: দুটি ডিসকাউন্ট নিয়মের জন্য DiscountStrategyFactoryProvider. Spring Framework-এ সত্যিই AbstractSingletonProxyFactoryBean নামে একটি class আছে — প্রোগ্রামারদের আড্ডায় দীর্ঘ নামের উদাহরণ হিসেবে এটা প্রায়ই হাসির বিষয় হয়। ন্যায্যভাবে বললে, framework-এর ভেতরে সেই class-এর একটি নির্দিষ্ট কাজ আছে; কিন্তু একটি ছোট দোকানের অ্যাপে এমন স্তর বানানো over-engineering.
  • Pattern-itis: নতুন pattern শিখে প্রতিটি সমস্যায় সেটা বসানোর রোগ। লক্ষণ: class-এর নামে pattern-এর নাম (OrderVisitorFactory), কিন্তু কেউ বলতে পারে না কোন বদল থেকে এটা রক্ষা করছে।
প্রশ্ন “কোন pattern বসাব?” নয়।
প্রশ্ন “কোন জিনিসটা বদলাবে, আর সেই বদল কোথায় আটকে রাখব?”
C/C++/Python জানলে

GoF বইয়ের উদাহরণ মূলত C++-এ; সেখানে Strategy প্রায়ই template বা std::function দিয়ে লেখা হয়, আর ownership-এর জন্য smart pointer ভাবতে হয় — Java-তে garbage collector সেই ভার নেয়। Python-এ function প্রথম শ্রেণির মান, তাই Strategy মানে প্রায়ই শুধু একটি function পাঠানো, আর অনেক creational pattern-এর দরকারই পড়ে না। সাবধান: Python-এর @decorator syntax আর GoF-এর Decorator এক জিনিস নয় — প্রথমটি function সংজ্ঞার সময় তাকে মুড়ে দেয়, দ্বিতীয়টি চলার সময় object মোড়ে। গভীরে দেখুন C++-এর ডিজাইন নীতি আর Python-এর decorator পর্ব।

৮ · Clean code — নাম, ছোট method, code smell আর ধাপে ধাপে refactoring

Pattern বড় কাঠামোর ব্যাপার; প্রতিদিনের পরিচ্ছন্নতা আসে ছোট অভ্যাস থেকে।

  • অর্থপূর্ণ নাম: d, tmp, data2 নয় — deliveryFee, daysSinceOrder. Method-এর নাম ক্রিয়া (calculateTotal), boolean-এর নাম প্রশ্ন (isPaid). নাম ঠিকমতো দিতে না পারলে প্রায়ই দায়িত্বটাই অস্পষ্ট।
  • ছোট, এক কাজের method: একটি method এক বাক্যে বর্ণনা করা গেলে ভালো। “এবং” লাগলে ভাঙার কথা ভাবুন। তবে সংখ্যা ধরে নিয়ম নয় — ভাবনার প্রশ্ন ৪ দেখুন।
  • Comment বলবে কেন, কী নয়: // i এক বাড়াও অর্থহীন। // কুরিয়ার কোম্পানি ১১ অঙ্কের নম্বর ছাড়া নেয় না — এটাই দরকারি, কারণ কোড দেখে এটা বোঝা যায় না।
  • Magic number নয়: if (sub >= 2000)-এর বদলে FREE_DELIVERY_FROM নামের constant (পর্ব ১৮)।
  • একই ধাঁচের formatting: দলের সবাই একই নিয়ম — IDE-র formatter বা build-এ চালানো tool দিয়ে, তর্ক দিয়ে নয়।
  • Guard clause/early return: অবৈধ অবস্থা শুরুতেই return বা throw, যাতে মূল যুক্তি বাসা-বাঁধা if-এর গভীরে না ডোবে (পর্ব ১০)।

Code smell আর তার ওষুধ

Code smell নিজে bug নয়, একটি লক্ষণ — যেমন রান্নাঘরে পোড়া গন্ধ মানেই আগুন নয়, কিন্তু একবার দেখে আসা উচিত। শব্দটি জনপ্রিয় হয় Martin Fowler-এর Refactoring বই থেকে, আর প্রতিটি smell-এর একটি নামওয়ালা refactoring আছে — বাইরের আচরণ না বদলে ভেতরের গঠন উন্নত করা।

Smellলক্ষণসাধারণ ওষুধ
Long methodস্ক্রল করে পড়তে হয়, ভেতরে comment দিয়ে “অংশ” ভাগ করাExtract Method
Long parameter list৫-৬টি parameter, ক্রম গুলিয়ে যায়Introduce Parameter Object — Java-তে একটি record
Feature envymethod নিজের class-এর চেয়ে অন্য class-এর getter বেশি ডাকেmethod-টিকে সেই class-এ সরানো (Move Method)
Primitive obsessionফোন, টাকা, ডেলিভারির ধরন — সব String বা intrecord, enum বা ছোট value class (পর্ব ২৩–২৫)
Duplicated codeএকই নিয়ম দুই জায়গায়, একটি বদলালে অন্যটি ভুলে যাওয়াExtract Method, Template Method
Type-code switch/if শিকলএকই if (type.equals(...)) কয়েক জায়গায়Replace Conditional with Polymorphism

এবার একটি বাস্তব জট। প্রথম সংস্করণে লেখা অর্ডার প্রসেস করার method — চলে, কিন্তু ছুঁতে ভয় লাগে:

int process(String name, String phone, String city, String type,
            int price, int qty, String coupon) {
    if (name != null && !name.isBlank()) {
        if (phone != null && phone.length() == 11) {
            int sub = price * qty;
            if (coupon != null && coupon.equals("EID10")) {
                sub = sub - sub / 10;
            }
            int fee;
            if (type.equals("STANDARD")) {
                if (city.equals("Dhaka")) fee = 60; else fee = 120;
            } else if (type.equals("EXPRESS")) {
                if (city.equals("Dhaka")) fee = 100; else fee = 200;
            } else if (type.equals("PICKUP")) {
                fee = 0;
            } else {
                throw new IllegalArgumentException("bad type");
            }
            if (sub >= 2000) fee = 0;
            int total = sub + fee;
            // send sms
            String msg = "Dear " + name + ", your order total is " + total;
            smsApi.dispatch("KEY-123", "88" + phone, msg, "ABCL");
            return total;
        } else {
            throw new IllegalArgumentException("bad phone");
        }
    } else {
        throw new IllegalArgumentException("bad name");
    }
}

গুনে দেখুন: সাতটি parameter, তিন স্তরের বাসা-বাঁধা if, 2000, 60, "EID10"-এর মতো magic value, type একটি String (বানান ভুল করলে চলার সময় ধরা পড়ে), কোডে বসানো API key, আর যাচাই, হিসাব, SMS — তিনটি দায়িত্ব এক method-এ। শুরুর আগে একটি শর্ত: এই method-এর বর্তমান আচরণ ধরে রাখে এমন টেস্ট (পর্ব ৪৭)। টেস্ট ছাড়া refactoring হলো চোখ বেঁধে দেয়াল সরানো। এবার চার ধাপ:

  1. শুরু৩০+ লাইনসব এক জায়গায়
  2. Extract Method৪টি ছোট methodনাম দিয়ে অর্থ
  3. Parameter objectrecord৭ → ১ parameter
  4. Guard clauseearly throwবাসা ভাঙা
  5. Polymorphismenum DeliveryTypeif-শিকল বিদায়
শুরুsmell-এর তালিকা
long method · long parameter list (7)
magic number: 2000, 60, 120, "EID10"
primitive obsession: String type
nested if (৩ স্তর) · ৩টি দায়িত্ব
ধাপ ১Extract Method
int process(...7 params...) {
    validate(name, phone);
    int sub = discounted(price * qty, coupon);
    int total = sub + deliveryFee(type, city, sub);
    notifyCustomer(name, phone, total);
    return total;
}
ধাপ ২record parameter object
record Customer(String name, String phone) {}
record OrderRequest(Customer customer, String city,
    String type, int unitPrice, int qty, String coupon) {}

int process(OrderRequest req) { ... }
ধাপ ৩guard clause
private void validate(Customer c) {
    if (c.name() == null || c.name().isBlank())
        throw new IllegalArgumentException("name required");
    if (c.phone() == null || c.phone().length() != 11)
        throw new IllegalArgumentException("phone must be 11 digits");
}   // else-এর পিরামিড আর নেই
ধাপ ৪conditional → polymorphism
enum DeliveryType {
    STANDARD { int fee(boolean dhaka) { return dhaka ? 60 : 120; } },
    EXPRESS  { int fee(boolean dhaka) { return dhaka ? 100 : 200; } },
    PICKUP   { int fee(boolean dhaka) { return 0; } };
    abstract int fee(boolean insideDhaka);
}
  1. আগে রোগ নির্ণয়। সব smell নাম ধরে লিখে ফেলুন। নাম দিতে পারলে ওষুধও জানা — প্রতিটি smell-এর একটি পরিচিত refactoring আছে। এই মুহূর্তে কোনো কোড বদলায়নি, শুধু টেস্ট সবুজ কি না দেখে নিন।
  2. Extract Method. প্রতিটি “অংশ” নিজের নামওয়ালা method-এ গেল। process() এখন একটি সূচিপত্রের মতো পড়া যায়। discounted()-এর ভেতরে হিসাব হুবহু আগের sub - sub / 10 — refactoring-এ ফলাফল এক পয়সাও বদলাবে না। প্রতিটি ছোট ধাপের পর টেস্ট চালান।
  3. Parameter object. সাত parameter দুটি record-এ। একসাথে চলাফেরা করা মানগুলো (নাম আর ফোন) একটি ধারণা পেল: Customer. ডাকার জায়গায় ক্রম গুলিয়ে ফেলার ভয় শেষ, আর পরে নতুন field যোগ করলে signature বদলাতে হয় না।
  4. Guard clause. যাচাই এখন শুরুতেই ব্যর্থ হয়ে বেরিয়ে যায়; মূল পথ সোজা, কোনো বাসা নেই। Error message-ও এখন কী ভুল সেটা স্পষ্ট বলে।
  5. Conditional → polymorphism. String type-এর বদলে enum DeliveryType (পর্ব ২৪), প্রতিটি constant নিজের চার্জ জানে। বানান ভুল এখন compile error, নতুন ধরন যোগ করলে compiler fee লিখতে বাধ্য করে। ঢাকার ভেতরে-বাইরে নিয়ম আরও জটিল হলে এখান থেকেই অংশ ৫-এর Strategy-তে যাওয়া যায়।

শেষ ফল — process() এখন পাঁচ লাইন, আর প্রতিটি সাহায্যকারী method এক কাজের:

class OrderService {
    private static final int FREE_DELIVERY_FROM = 2000;
    private static final String EID_COUPON = "EID10";
    private final SmsSender sms;                       // Adapter, constructor দিয়ে
    OrderService(SmsSender sms) { this.sms = sms; }

    int process(OrderRequest req) {
        validate(req.customer());
        int subtotal = discounted(req.unitPrice() * req.qty(), req.coupon());
        int total = subtotal + deliveryFee(req.type(), req.city(), subtotal);
        notifyCustomer(req.customer(), total);
        return total;
    }
    private int discounted(int amount, String coupon) {
        return EID_COUPON.equals(coupon) ? amount - amount / 10 : amount;   // ১০% ছাড়
    }
    private int deliveryFee(DeliveryType type, String city, int subtotal) {
        if (subtotal >= FREE_DELIVERY_FROM) return 0;
        return type.fee("Dhaka".equals(city));
    }
    // validate() আর notifyCustomer() ওপরের stepper-এর মতো
}

"Dhaka".equals(city) উল্টো করে লেখা — city null হলেও NullPointerException হবে না। API key কোড থেকে সরে adapter-এ, আর adapter বাইরের config থেকে key পায়। খেয়াল করুন, আমরা কোনো “pattern বসাতে” শুরু করিনি; smell ধরে ধরে এগোতে গিয়ে শেষে polymorphism আর adapter নিজে থেকেই এসেছে। ভালো নকশা প্রায়ই এভাবেই জন্মায়।

Code review — দলের পরিচ্ছন্নতার অভ্যাস

Code review-তে অন্য কেউ আপনার পরিবর্তন মূল শাখায় যাওয়ার আগে পড়েন। ভালো review-এর সংস্কৃতি: ছোট পরিবর্তন পাঠান (কয়েকশো লাইনের বেশি হলে মানুষ মন দিয়ে পড়ে না); formatting-এর মতো যন্ত্রে ধরা যায় এমন জিনিস tool-কে ছেড়ে দিন; মন্তব্য কোড নিয়ে, মানুষ নিয়ে নয় — “এই নামটা পড়ে বুঝিনি, feeFor হলে কেমন?” প্রশ্নের আকারে; আর review-কে শেখার জায়গা ভাবুন, পরীক্ষার নয়। AI review-ও এখন সাধারণ — সেটা পরের অংশে।

পরীক্ষা ও ইন্টারভিউতে
  • Design pattern কী, আর GoF-এর তিন ভাগ? — বারবার আসা নকশা-সমস্যার নাম দেওয়া পরীক্ষিত সমাধান; creational, structural, behavioral.
  • Java-তে thread-safe Singleton-এর সেরা উপায়? — একটি constant-ওয়ালা enum; JVM একবার তৈরি নিশ্চিত করে, serialization ও reflection-এও নিরাপদ। তবে সম্ভব হলে Singleton-এর বদলে dependency injection.
  • Decorator আর Proxy-র পার্থক্য? — গঠন এক (একই interface, ভেতরে আসল object); Decorator ক্ষমতা যোগ করে আর স্তরে স্তরে বসে, Proxy পৌঁছানো নিয়ন্ত্রণ করে।
  • Strategy আর State-এর পার্থক্য? — Strategy বাইরে থেকে বেছে দেওয়া হয়; State object নিজেই পরের অবস্থায় বদলে যায়।
  • SOLID-এর পূর্ণরূপ? — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.

৯ · AI-এর যুগে — pattern-এর ভারে চাপা না পড়া

AI-কে “একটি ডিসকাউন্ট সিস্টেম লিখে দাও, best practice মেনে” বললে প্রায়ই ফেরত আসে: DiscountStrategy interface, তিনটি implementation, একটি DiscountStrategyFactory, একটি Singleton DiscountManager, আর প্রতিটির জন্য আলাদা ফাইল — অথচ আসল নিয়ম দুটি, যা একটি switch-এ চার লাইনে লেখা যেত। AI “best practice” শব্দটিকে “যত বেশি pattern তত ভালো” হিসেবে পড়ে, কারণ তার শেখার উপাত্তে enterprise কোডের প্রাচুর্য। উল্টো দিকেও ভুল হয়: পুরোনো টিউটোরিয়াল থেকে শেখা anonymous class দিয়ে Strategy, double-checked locking দিয়ে Singleton, বা accept/visit-এর Visitor — যেখানে আধুনিক Java-তে lambda, enum আর sealed switch আছে।

AI যা করেকেন সমস্যাআপনি যা করবেন
ছোট সমস্যায় Factory + Strategy + Managerবোঝা ও বদলানো কঠিন, বদলের কোনো কারণ নেইজিজ্ঞেস করুন: কোন বদল থেকে এটা রক্ষা করছে?
getInstance()-ওয়ালা Singletonলুকানো global state, টেস্ট কঠিনconstructor injection চান; সত্যিই লাগলে enum
এক লাইনের নিয়মে পুরো class বা anonymous classJava 8-এর আগের অভ্যাসfunctional interface + lambda চান
বন্ধ টাইপ-পরিবারে VisitorJava 21-এ অপ্রয়োজনীয় জটিলতাsealed interface + record + pattern-matching switch
refactoring-এ চুপচাপ আচরণ বদলায় (rounding, null)refactoring-এর মূল শর্ত ভাঙাআগে-পরে একই টেস্ট চালান

AI-কে refactoring-এর পরামর্শদাতা বানান, সিদ্ধান্তদাতা নয়। নিচের কাঠামোয় শুরুতেই সীমা বেঁধে দেওয়া হয়েছে, যাতে সে pattern-এর মিছিল না বানায়:

আমি Java 21-এ কাজ করছি। নিচের method-টি refactor করতে চাই। [এখানে কোড] শর্ত: ১. আগে শুধু code smell-গুলোর তালিকা দাও, প্রতিটির নাম (Fowler-এর নাম) আর কোন লাইনে। এখনই কোড বদলাবে না। ২. তারপর ছোট ছোট ধাপে refactoring প্রস্তাব করো; প্রতিটি ধাপের পর বাইরের আচরণ হুবহু একই থাকবে (rounding, null, exception-এর বার্তাসহ)। ৩. কোনো design pattern তখনই প্রস্তাব করবে, যখন এক বাক্যে বলতে পারবে কোন বাস্তব বদল থেকে এটা রক্ষা করছে। না পারলে সরল কোড রাখো। ৪. Singleton বা getInstance() নয়; নির্ভরতা constructor দিয়ে। ৫. আধুনিক Java ব্যবহার করো: record, enum, sealed, switch expression, lambda. ৬. শেষে বলো: বর্তমান আচরণ ধরে রাখতে কোন কোন টেস্ট আগে লেখা উচিত।

AI-এর নকশা বা refactoring হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Software pattern-এর ধারণা এসেছে একজন স্থপতির বই থেকে। বাড়ির নকশা আর কোডের নকশার মধ্যে মিলটা আসলে কোথায়?

১৯৭৭ সালে স্থপতি Christopher Alexander, সহলেখক Sara Ishikawa ও Murray Silverstein-সহ, প্রকাশ করেন A Pattern Language. বইটিতে ২৫০টির বেশি pattern — শহরের বিন্যাস থেকে শুরু করে জানালার পাশে বসার জায়গা পর্যন্ত। প্রতিটি pattern একই কাঠামোয় লেখা: একটি প্রেক্ষাপট, সেখানে বারবার দেখা দেওয়া একটি সমস্যা (প্রায়ই দুটি বিপরীত টানের সংঘাত, যেমন আলো চাই কিন্তু গোপনীয়তাও চাই), আর এমন একটি সমাধান-আকার, যা “লক্ষবার ব্যবহার করা যায়, কিন্তু কখনো দুবার একইভাবে নয়।” তার পরের বই The Timeless Way of Building (১৯৭৯) এর পেছনের দর্শন ব্যাখ্যা করে।

প্রোগ্রামাররা এখানে নিজেদের চিনতে পেরেছিলেন। ১৯৮৭ সালে Kent Beck আর Ward Cunningham OOPSLA সম্মেলনে একটি ছোট প্রতিবেদন উপস্থাপন করেন, যেখানে Smalltalk-এ user interface বানানোর জন্য কয়েকটি pattern Alexander-এর ধাঁচে লেখা হয়। নব্বইয়ের দশকে Hillside Group তৈরি হয় আর PLoP (Pattern Languages of Programs) সম্মেলন শুরু হয়। Alexander নিজে ১৯৯৬ সালে OOPSLA-তে বক্তৃতা দেন — সেখানে তিনি প্রশংসার পাশাপাশি সতর্কও করেন যে সফটওয়্যার জগৎ তার ধারণার নৈতিক দিকটি, অর্থাৎ মানুষের জন্য জীবন্ত পরিবেশ বানানোর লক্ষ্য, অনেকটা বাদ দিয়ে শুধু কাঠামোটি নিয়েছে।

মিলটা গভীর। বাড়িতে যেমন “সিঁড়ির পাশে আলো” একটি pattern, কারণ অন্ধকার সিঁড়িতে মানুষ পড়ে যায়, তেমনি কোডে Adapter একটি pattern, কারণ বাইরের API-র অদ্ভুততা সারা কোডে ছড়িয়ে পড়ে। দুটি ক্ষেত্রেই সমাধানটি একটি নির্দিষ্ট বস্তু নয়, একটি সম্পর্কের আকার। আর দুটি ক্ষেত্রেই আসল জোর ভাষার ওপর: Alexander চেয়েছিলেন সাধারণ মানুষ এই ভাষা দিয়ে নিজের বাড়ি নিজে ভাবতে পারুক। সফটওয়্যারেও pattern-এর প্রধান উপহার হলো ভাগ করা শব্দভান্ডার, যা দিয়ে দলের সবাই দ্রুত নকশা নিয়ে কথা বলতে পারে।

অমিলও আছে। বাড়ি একবার বানালে দশকের পর দশক একই থাকে; সফটওয়্যার প্রতি সপ্তাহে বদলায়। তাই সফটওয়্যার pattern-এর মূল মূল্য প্রায়ই “বদলের খরচ কমানো”, যা স্থাপত্যে ততটা কেন্দ্রীয় নয়। Alexander ২০২২ সালে মারা যান, কিন্তু wiki-র ধারণা (Ward Cunningham-এর প্রথম wiki ছিল pattern নিয়ে আলোচনার জন্য) থেকে শুরু করে Agile আন্দোলন পর্যন্ত তার প্রভাব ছড়িয়ে আছে।

মূল উপলব্ধি: Pattern কোনো কোড-টুকরো নয়; প্রেক্ষাপট, টানাপোড়েন আর সমাধান-আকারের একটি বর্ণনা, যার আসল কাজ মানুষে মানুষে নকশার ভাষা তৈরি করা।

প্র ০২GoF বইটি ৩০ বছরের বেশি পুরোনো, উদাহরণ C++ আর Smalltalk-এ। তবু Java প্রোগ্রামারদের কাছে এটা কেন এত প্রভাবশালী, আর কোন অংশ আজ পুরোনো?

Design Patterns: Elements of Reusable Object-Oriented Software প্রকাশিত হয় ১৯৯৪ সালে। লেখক চারজন — Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides — তাই ডাকনাম “Gang of Four”. বইয়ে ২৩টি pattern, প্রতিটির জন্য উদ্দেশ্য, প্রেরণা, গঠন (তখনকার OMT নকশা-চিত্রে), অংশগ্রহণকারী, ফলাফল আর C++ বা Smalltalk-এ নমুনা কোড। সময়টা গুরুত্বপূর্ণ: object-oriented প্রোগ্রামিং তখন মূলধারায় আসছে, কিন্তু অভিজ্ঞ নকশাকারদের জ্ঞান ছিল মুখে মুখে। বইটি সেই জ্ঞানকে একটি সাধারণ ভাষায় লিখে ফেলে।

ঠিক পরের বছর, ১৯৯৫ সালে, Java প্রকাশিত হয়। Java-র নকশা — একক class inheritance, interface, garbage collection — GoF-এর দুটি মূল উপদেশের সাথে স্বাভাবিকভাবে মিলে যায়: “implementation নয়, interface-এর ওপর প্রোগ্রাম করুন” আর “class inheritance-এর চেয়ে object composition বেশি পছন্দ করুন।” Java-র standard library নিজেই pattern-এ ভরা: java.io-র Decorator, Iterator, Comparator-এর Strategy, Swing-এর Observer আর Composite. ফলে Java প্রজন্ম pattern শিখেছে ভাষার সাথেই। Erich Gamma পরে Kent Beck-এর সাথে JUnit তৈরি করেন আর Eclipse-এর Java tooling-এর নেতৃত্ব দেন — বইয়ের সাথে Java জগতের বন্ধন আরও গভীর হয়।

কোন অংশ পুরোনো? (১) কিছু pattern ভাষার সীমাবদ্ধতার প্রতিক্রিয়া ছিল — C++-এ function-কে মান হিসেবে পাঠানো কষ্টকর ছিল বলে Strategy আর Command-এর এত আনুষ্ঠানিকতা; Java 8-এর lambda (২০১৪) সেটা ছোট করেছে। (২) Visitor-এর double dispatch ছিল বন্ধ টাইপ-পরিবারে নতুন অপারেশন যোগ করার কৌশল; Java 21-এর sealed interface আর pattern-matching switch সেটা সরাসরি দেয়। (৩) Singleton-কে আজ বেশিরভাগ অভিজ্ঞ প্রোগ্রামার সন্দেহের চোখে দেখেন; dependency injection framework সেই ভূমিকা নিয়েছে। লেখকরা নিজেরাও পরে সাক্ষাৎকারে বলেছেন, আবার লিখলে কিছু pattern বাদ দিতেন বা বদলাতেন।

যা পুরোনো হয়নি: নকশার ভাষা, “কী বদলায় সেটা আলাদা করুন” — এই চিন্তা, আর প্রতিটি pattern-এর সাথে তার “ফলাফল” অংশে দাম স্পষ্ট লেখার অভ্যাস। বইটি পড়ার সময় কোড নমুনা দেখার চেয়ে “Applicability” আর “Consequences” অংশ বেশি মন দিয়ে পড়ুন।

মূল উপলব্ধি: GoF একটি সময়ের ভাষা আর সীমাবদ্ধতার ছাপ বহন করে; তার শব্দভান্ডার আর নকশা-চিন্তা চিরস্থায়ী, কিন্তু নির্দিষ্ট কোড-আকার আধুনিক Java-তে প্রায়ই অনেক ছোট হয়ে যায়।

প্র ০৩Peter Norvig ১৯৯৬ সালে বলেছিলেন অনেক pattern dynamic ভাষায় “অদৃশ্য” বা অনেক সরল। তাহলে কি design pattern আসলে ভাষার দুর্বলতার চিহ্ন?

১৯৯৬ সালে Peter Norvig — পরে Google-এর গবেষণা পরিচালক আর বিখ্যাত AI পাঠ্যবইয়ের সহলেখক — “Design Patterns in Dynamic Programming” নামে একটি উপস্থাপনা দেন। তার পর্যবেক্ষণ: GoF-এর ২৩টি pattern-এর মধ্যে ১৬টি Lisp বা Dylan-এর মতো ভাষায় গুণগতভাবে সরল, অন্তত কিছু ব্যবহারে — হয় ভাষার কোনো ফিচারেই মিশে যায় (অদৃশ্য), নয়তো অনেক কম কোডে লেখা যায়। কারণ সেই ভাষাগুলোতে function প্রথম শ্রেণির মান, multiple dispatch আছে, টাইপ চলার সময় বদলানো যায়।

উদাহরণ সহজ। Strategy: যে ভাষায় function পাঠানো যায়, সেখানে “একটি interface আর তিনটি class” লাগে না, শুধু একটি function দিলেই হয়। Command: closure-ই একটি command. Iterator: generator বা higher-order function থাকলে আলাদা iterator class লাগে না। Visitor: multiple dispatch থাকলে double dispatch-এর কৌশল অপ্রয়োজনীয়। এই পর্বের Strategy কোডে আপনি নিজেই দেখেছেন — subtotal -> 0 একটি পুরো strategy class-এর জায়গা নিয়েছে।

তাহলে কি pattern মানেই ভাষার দুর্বলতা? আংশিক সত্য। Paul Graham-এর মতো কেউ কেউ বলেছেন, কোডে বারবার একই আকার দেখা গেলে সেটা ইঙ্গিত যে ভাষা যথেষ্ট শক্তিশালী নয় — বা আপনি abstraction-টি ঠিকমতো ধরেননি। কিন্তু পুরো সত্য নয়। প্রথমত, ভাষা কোনো pattern শুষে নিলে pattern উধাও হয় না, ধারণাটা থেকে যায় — lambda দিয়ে লিখলেও আপনি Strategy-ই বানাচ্ছেন, আর “এখানে বদলযোগ্য নিয়ম” — এই চিন্তা এখনো দরকার। দ্বিতীয়ত, প্রতিটি ভাষার নিজের pattern আছে: functional জগতে monad বা fold, concurrency-তে producer-consumer, distributed system-এ retry, circuit breaker, saga — এগুলো কোনো ভাষার ফিচার দিয়ে মোছে না। তৃতীয়ত, static টাইপের ভাষা (Java) কিছু কৌশল কঠিন করে, বিনিময়ে compiler-এর যাচাই দেয় — এটা একটি সচেতন বিনিময়, নিছক দুর্বলতা নয়।

Java নিজেও এই পথে হেঁটেছে: Java 8-এর lambda, Java 16-এর record, Java 17-এর sealed class, Java 21-এর pattern matching — প্রতিটি কোনো না কোনো pattern-এর আনুষ্ঠানিকতা কমিয়েছে। তাই আধুনিক Java শেখা মানে পুরোনো pattern-কে নতুন, ছোট আকারে চিনতে শেখা।

মূল উপলব্ধি: ভাষা যত শক্তিশালী হয়, তত বেশি pattern ফিচারের ভেতরে মিলিয়ে যায়; কিন্তু নকশা-ধারণা হিসেবে pattern বেঁচে থাকে, শুধু তার কোড-আকার ছোট হয়।

প্র ০৪Robert C. Martin-এর “Clean Code” বইটি এক প্রজন্মকে প্রভাবিত করেছে, আবার কড়া সমালোচনাও পেয়েছে। এর কোন পরামর্শ রাখবেন আর কোনটা সাবধানে নেবেন?

Clean Code: A Handbook of Agile Software Craftsmanship প্রকাশিত হয় ২০০৮ সালে। লেখক Robert C. Martin, যিনি “Uncle Bob” নামে পরিচিত, আর যিনি SOLID নীতিগুলোও জনপ্রিয় করেছিলেন। বইটির উদাহরণ Java-তে, তাই Java জগতে এর প্রভাব বিশেষভাবে বেশি। অর্থপূর্ণ নাম, ছোট function, একটি function এক কাজ, comment-এর বদলে স্পষ্ট কোড, পরিষ্কার error handling, টেস্টের গুরুত্ব — অসংখ্য দলের কোডিং নির্দেশিকা আর code review-এর ভাষা এই বই থেকে এসেছে।

সমালোচনাও সমান জোরালো। সবচেয়ে বেশি আলোচিত হলো function-এর আকার নিয়ে উপদেশ — বইটি খুব ছোট function চায়, মাত্র কয়েক লাইনের। সমালোচকরা দেখান, এটা অক্ষরে অক্ষরে মানলে একটি যুক্তি অসংখ্য ছোট method-এ ভাগ হয়ে যায়, পাঠককে বারবার লাফ দিতে হয়, আর অনেক সময় ডেটা method-এর parameter-এ না গিয়ে লুকানো instance field-এ চলাচল করে, যা বোঝা আরও কঠিন। বইয়ের কিছু উদাহরণ refactoring-এর পর আগের চেয়ে কঠিন হয়ে গেছে বলে অনেক পাঠক লিখেছেন। Stanford-এর অধ্যাপক John Ousterhout তার A Philosophy of Software Design (২০১৮) বইয়ে “deep module”-এর পক্ষে যুক্তি দেন — সরল interface-এর পেছনে বেশ খানিকটা কাজ লুকানো module ভালো, অনেক “shallow” ছোট module নয়। পরে Ousterhout আর Martin এই বিষয়গুলো নিয়ে একটি লিখিত বিতর্কও প্রকাশ করেন। Performance নিয়েও প্রশ্ন উঠেছে: ২০২৩ সালে Casey Muratori একটি ভিডিওতে দেখান, polymorphism আর ছোট function-এ ভাগ করা কোড কিছু হিসাবনির্ভর কাজে সরল switch-এর চেয়ে অনেক ধীর হতে পারে।

তাহলে কী রাখবেন? নীতিগুলোর মূল উদ্দেশ্য — পাঠকের জন্য লেখা, নাম দিয়ে অর্থ বোঝানো, একটি জায়গায় একটি কাজ, টেস্ট দিয়ে নিরাপদে বদলানো। কী সাবধানে নেবেন? সংখ্যাভিত্তিক নিয়ম (“কয়েক লাইনের বেশি নয়”), আর এমন refactoring যা লুকানো state বাড়ায়। মাপকাঠি হিসেবে প্রশ্ন করুন: এই ভাগে কি পাঠকের মাথায় রাখার জিনিস কমল, না শুধু ফাইলের লাইন কমল? এই পর্বের refactoring-এ process() পাঁচ লাইনে নেমেছে, কিন্তু প্রতিটি সাহায্যকারী method নিজের parameter নেয় আর ফল ফেরত দেয় — কোনো লুকানো state নেই।

মূল উপলব্ধি: Clean Code-কে নিয়মের বই নয়, প্রশ্নের তালিকা হিসেবে পড়ুন; লক্ষ্য পাঠকের বোঝার খরচ কমানো, আর সেই লক্ষ্যের সাথে সাংঘর্ষিক হলে যেকোনো নিয়ম ভাঙা যায়।

প্র ০৫Kent Beck-এর “simple design”-এর চারটি নিয়ম কী, আর এগুলো দিয়ে কীভাবে ঠিক করবেন কোনো pattern বসানো উচিত কি না?

Kent Beck — Extreme Programming (XP)-এর জনক, JUnit-এর সহ-স্রষ্টা আর ১৯৮৭ সালের সেই pattern-প্রতিবেদনের সহলেখক — নব্বইয়ের দশকের শেষে XP-র অংশ হিসেবে “simple design”-এর চারটি নিয়ম বলেন, গুরুত্বের ক্রমে: (১) সব টেস্ট পাস করে; (২) উদ্দেশ্য প্রকাশ করে; (৩) কোনো পুনরাবৃত্তি নেই; (৪) সবচেয়ে কম উপাদান — class, method, স্তর। Martin Fowler-সহ অনেকে পরে এগুলো ব্যাখ্যা করেছেন; কেউ কেউ দ্বিতীয় আর তৃতীয়টির ক্রম নিয়ে তর্ক করেন, কিন্তু সবাই একমত যে প্রথমটি সবার আগে আর চতুর্থটি সবার শেষে।

লক্ষ করুন নিয়মগুলো কীভাবে একে অপরকে ভারসাম্যে রাখে। দ্বিতীয় আর তৃতীয় নিয়ম আপনাকে abstraction-এর দিকে ঠেলে — একই নিয়ম দুই জায়গায় থাকলে এক জায়গায় আনুন, অস্পষ্ট কোডকে নাম দিন। চতুর্থ নিয়ম উল্টো দিকে টানে — অপ্রয়োজনীয় interface, factory, স্তর মুছে ফেলুন। Pattern-এর প্রশ্নে এটাই সবচেয়ে কাজের মাপকাঠি: একটি pattern তখনই ঢুকবে, যখন সেটা পুনরাবৃত্তি কমায় বা উদ্দেশ্য স্পষ্ট করে, আর তার বাড়তি উপাদানের দাম সেই লাভের চেয়ে কম।

এই পর্বের checkout দিয়ে পরীক্ষা করুন। ডেলিভারি চার্জের নিয়ম তিনটি, আর ঈদে-পূজায় নতুন অফার আসে — Strategy পুনরাবৃত্তি কমায় (প্রতিটি নিয়ম এক জায়গায়) আর উদ্দেশ্য প্রকাশ করে (DeliveryFee নামটিই বলে কী বদলায়)। রাখার মতো। কিন্তু যদি নিয়ম একটিই হতো আর কখনো না বদলাত, তাহলে interface একটি বাড়তি উপাদান, চতুর্থ নিয়ম তাকে মুছতে বলে। একইভাবে, শুধু “পরে লাগতে পারে” ভেবে DeliveryFeeFactory কোনো নিয়মকেই সন্তুষ্ট করে না — এটা YAGNI-র সরাসরি লঙ্ঘন।

প্রথম নিয়মটি — “টেস্ট পাস করে” — সবার ওপরে রাখার কারণও এখানে স্পষ্ট। টেস্ট আছে বলেই আপনি আজ সরল নকশা রেখে কাল নিশ্চিন্তে বদলাতে পারেন। টেস্ট না থাকলে মানুষ ভয়ে আগেভাগে “নমনীয়” কাঠামো বানায়, আর সেখান থেকেই over-engineering জন্মায়। তাই পর্ব ৪৭-এর টেস্টিং আর এই পর্বের নকশা আসলে একই মুদ্রার দুই পিঠ।

মূল উপলব্ধি: সরল নকশা মানে কম কোড নয়, বরং টেস্টে সুরক্ষিত, উদ্দেশ্য-স্পষ্ট, পুনরাবৃত্তিহীন কোড যতটা সম্ভব কম উপাদানে; pattern সেখানে অতিথি, যাকে প্রতিবার নিজের প্রয়োজনীয়তা প্রমাণ করতে হয়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. Decorator-এর ক্রম উল্টে দিন: অংশ ৪-এর চালানো যায় এমন কোডে new Retry(new Logging(new FlakyBank()))-কে new Logging(new Retry(new FlakyBank())) করুন। না চালিয়ে আউটপুট অনুমান করুন, তারপর চালিয়ে মেলান। কোন ক্রম কখন ভালো?

    আউটপুট তিন লাইন: log: charge 1560, retry: timeout, once more, final: OK-1560. এবার Logging সবচেয়ে বাইরে, তাই সে একবারই ডাক দেখে; retry ঘটে তার ভেতরে, চোখের আড়ালে। প্রতিটি চেষ্টা আলাদা করে দেখতে চাইলে (কোন ব্যাংক কতবার timeout দিচ্ছে) Logging ভেতরে রাখুন; ব্যবসার দিক থেকে “একটি চার্জের অনুরোধ” একবার লিখতে চাইলে বাইরে। দুটোই বৈধ — মোড়কের ক্রম একটি সচেতন নকশা-সিদ্ধান্ত।

  2. নতুন strategy যোগ করুন: অংশ ৫-এর চালানো যায় এমন কোডে (ক) “Pickup” নামে একটি option যোগ করুন, চার্জ ০, lambda দিয়ে; (খ) “Express” নামে একটি class লিখুন — ঢাকার বাইরে ২০০ টাকা, কোনো ফ্রি সীমা নেই। Checkout-এ কয়টি লাইন বদলাতে হলো?
    class Express implements DeliveryFee {
        public int feeFor(int subtotal) { return 200; }
    }
    // main-এর তালিকায়:
    new Checkout("Pickup", subtotal -> 0),
    new Checkout("Express", new Express())

    নতুন লাইন ছাপবে Pickup: 1500, 2500 আর Express: 1700, 2700. Checkout-এ শূন্য লাইন বদল — Open/Closed-এর ফল। ভাবুন: Pickup-এর জন্য class লাগেনি কারণ নিয়মটি এক লাইনের; Express-এর জন্য class যুক্তিসংগত যদি পরে তার নিয়ম জটিল হয় বা আলাদা টেস্ট লাগে।

  3. Smell শনাক্ত করুন: ReportPrinter class-এর নিচের method-এ অন্তত তিনটি smell নাম ধরে বলুন, আর প্রতিটির ওষুধ:
    double calc(Student s) {
        return s.getMarks1() + s.getMarks2() + s.getMarks3() > 240
                ? s.getFee() * 0.5 : s.getFee();
    }

    (১) Feature envy — method-টি শুধু Student-এর ডেটা ব্যবহার করে, ReportPrinter-এর কিছুই নয়; একে Student-এ সরান, যেমন s.discountedFee(). (২) Magic number — 240 আর 0.5; SCHOLARSHIP_MARKS, SCHOLARSHIP_RATE নামের constant. (৩) অস্পষ্ট নাম — calc কী হিসাব করে? feeAfterScholarship. সাথে টাকার হিসাবে double — বাস্তব কাজে BigDecimal বা পয়সায় long. আর getMarks1/2/3 একটি primitive obsession-এর ইঙ্গিত — নম্বরগুলো একটি তালিকা বা record হতে পারে।

  4. Pattern লাগবে, না লাগবে না: প্রতিটি ক্ষেত্রে সিদ্ধান্ত দিন, এক লাইনের কারণসহ। (ক) দোকানে দুটি স্থির ডিসকাউন্ট নিয়ম, তিন বছরে বদলায়নি। (খ) পেমেন্টে bKash, Nagad, কার্ড — আর প্রতি কয়েক মাসে নতুন পদ্ধতি যোগ হয়। (গ) অর্ডার পৌঁছালে SMS, Email আর রিভিউ-অনুরোধ যাবে। (ঘ) পুরো অ্যাপে একটি config object লাগে।

    (ক) কোনো pattern নয় — একটি switch বা if; বদলের কোনো ইতিহাস নেই, YAGNI. (খ) Strategy (PaymentMethod interface), প্রতিটি সরবরাহকারীর API-র সামনে Adapter — বদল নিয়মিত আর বাইরের API ভিন্ন ভিন্ন। (গ) Observer — একটি ঘটনা, একাধিক স্বাধীন শ্রোতা, আর চ্যানেল বাড়বে। (ঘ) Singleton নয় — main-এ বা framework-এ একটি object বানিয়ে constructor দিয়ে দিন; “একটি object” আর “global পথে পাওয়া” আলাদা জিনিস।

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

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

ক্রমএকটি জট পাকানো Java method নিরাপদে refactor করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧪 বর্তমান আচরণ ধরে রাখে এমন টেস্ট লেখা ও সবুজ দেখা
  2. 👃 code smell-গুলো নাম ধরে চিহ্নিত করা
  3. ✂️ একটি ছোট refactoring করা, যেমন Extract Method
  4. ✅ আবার টেস্ট চালিয়ে আচরণ একই কি না দেখা
  5. 👀 ছোট পরিবর্তনটি code review-তে পাঠানো

একটি Java পেমেন্ট gateway-এর কোড না বদলে তার চারপাশে logging আর retry যোগ করতে চান, এমনভাবে যাতে দুটি ক্ষমতা স্তরে স্তরে জোড়া যায়। কোন pattern সবচেয়ে মানানসই?

  • Singleton
  • Decorator
  • Builder
  • Facade

Java-তে Singleton লেখার সবচেয়ে নিরাপদ উপায় হিসেবে একটি constant-ওয়ালা enum কেন সুপারিশ করা হয়?

  • enum-এর method সাধারণ class-এর চেয়ে দ্রুত চলে
  • enum ব্যবহার করলে global state-এর সমস্যা চলে যায়
  • JVM একবার তৈরি নিশ্চিত করে, আর serialization বা reflection দিয়ে দ্বিতীয় object বানানো যায় না
  • enum-এর সব field নিজে থেকেই অপরিবর্তনীয় হয়

আপনার কোড SmsSender.send(phone, text) চুক্তি চেনে, কিন্তু নতুন SMS কোম্পানির library-তে আছে dispatch(apiKey, msisdn, body, mask). Library বদলানো যায় না। কোন pattern দরকার?

  • Adapter
  • Observer
  • State
  • Template Method

interface DeliveryFee { int feeFor(int subtotal); } — Java 8 থেকে এর একটি নতুন strategy সবচেয়ে সংক্ষেপে কীভাবে লেখা যায়?

  • DeliveryFee-কে abstract class বানিয়ে subclass লেখা
  • একটি Singleton factory দিয়ে
  • anonymous class দিয়ে new DeliveryFee() { ... }
  • একটি lambda দিয়ে, যেমন s -> 0

ReportPrinter class-এর একটি method শুধু Student object-এর পাঁচটি getter ডেকে হিসাব করে, নিজের class-এর কোনো field ব্যবহার করে না। এই code smell-এর নাম কী?

  • God class
  • Feature envy
  • Primitive obsession
  • Long parameter list

একটি Java interface-এ ১৫টি method, আর বেশিরভাগ implementation অর্ধেক method-এ শুধু UnsupportedOperationException ছোড়ে। SOLID-এর কোন নীতি সবচেয়ে স্পষ্টভাবে ভাঙা হয়েছে?

  • Single Responsibility
  • Open/Closed
  • Interface Segregation
  • Dependency Inversion

শব্দকোষ

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

মনে রাখুন

  1. Pattern = নাম দেওয়া সমাধান-ধাঁচ, কপি করার কোড নয়। এর আসল উপহার ভাগ করা শব্দভান্ডার; প্রতিটি pattern-এর একটি প্রেক্ষাপট আর একটি দাম আছে।
  2. নীতি আগে: SOLID বদলের জন্য জায়গা রাখতে বলে, KISS আর YAGNI অপ্রয়োজনীয় জায়গা বানাতে নিষেধ করে। একই ধরনের বদল বারবার এলে তখন pattern.
  3. Checkout-এর পাঁচ pattern: Strategy (বদলযোগ্য নিয়ম), Observer (এক ঘটনা, অনেক শ্রোতা), Decorator (মোড়কে ক্ষমতা), Adapter (বাইরের API নিজের চুক্তিতে), Facade (সরল দরজা)। Singleton-এর বদলে constructor injection.
  4. আধুনিক Java pattern ছোট করে: lambda → Strategy/Command, sealed + record + switch → Visitor, Optional → অনেক Null Object, enum → State ও Singleton.
  5. Clean code = পরের পাঠকের জন্য লেখা। Smell চিনুন, টেস্টের সুরক্ষায় ছোট ধাপে refactor করুন, আর AI-এর pattern-ভারী প্রস্তাব “কোন বদল থেকে রক্ষা?” প্রশ্নে যাচাই করুন।

পরের পর্বে: JDBC আর ডেটাবেস। এতক্ষণ অর্ডার ছিল শুধু মেমরিতে; এবার সেটা স্থায়ীভাবে ডেটাবেসে রাখার পালা — Java থেকে ডেটাবেসে কীভাবে কথা বলতে হয়, সেটাই পরের পর্বের বিষয়। সেখানেও দেখবেন, ডেটাবেসের কোড একটি ছোট চুক্তির পেছনে লুকিয়ে রাখলে বাকি অ্যাপ কতটা নিশ্চিন্ত থাকে।

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

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