এই পর্বে যা শিখবেন
- একটি feature-এর জন্য driving আর driven port চিহ্নিত করা, আর একই use case দুই রকম adapter দিয়ে চালানো
- Clean Architecture-এর চার বৃত্ত আর dependency rule, আর import দেখে নিয়মভঙ্গ খুঁজে বের করা
- Hexagonal, onion আর Clean — তিনটি নামের মিল আর ছোট পার্থক্য বলতে পারা
- Value object দিয়ে primitive obsession-এর bug ঠেকানো, আর aggregate দিয়ে invariant রক্ষা করা
- কখন এই architecture পোষায়, আর কখন সরল CRUD-ই সঠিক উত্তর — তা যুক্তিসহ বিচার করা
১ · সমস্যা — database-কে কেন্দ্র করে সাজানো স্তর
পর্ব ৩৮-এ layered architecture দেখেছেন: ওপরে presentation, মাঝে business logic, নিচে data access আর database. প্রতিটি স্তর ঠিক নিচের স্তরকে ডাকে। নকশাটি সহজ, পরিচিত, আর অনেক app-এর জন্য যথেষ্ট। কিন্তু এর ভেতরে একটি লুকানো সিদ্ধান্ত আছে: নির্ভরতার তীর নিচের দিকে, অর্থাৎ business-এর নিয়ম database-এর দিকে তাকিয়ে আছে।
একটি কাল্পনিক অনলাইন মুদি-বাজার app ধরুন। নিয়ম: “ক্রেতার wallet-এ যথেষ্ট টাকা থাকলে order PAID, না থাকলে REJECTED; আর একটি order-এর মোট দাম ক্রেতার credit limit ছাড়াবে না।” Layered নকশায় এই নিয়মের class সাধারণত এরকম দেখায়: ভেতরে ORM-এর model import করা, সরাসরি SQL session খোলা, আর web framework-এর request object হাতে নেওয়া। কয়েক বছর পর দেখা যায় তিনটি সমস্যা:
- নিয়মের test-এ database লাগে। “wallet-এ ৫০০ টাকা, order ৩০০ — PAID হবে” — এই এক লাইনের নিয়ম যাচাই করতেও test database চালু করতে হয়, table বানাতে হয়। Test ধীর হয়, তাই কম চালানো হয়।
- Framework-এর বদল নিয়ম ভাঙে। ORM-এর নতুন version-এ একটি method-এর নাম বদলালে order-এর নিয়মের file-ও বদলাতে হয়, যদিও ব্যবসার নিয়মে কিছুই বদলায়নি।
- নকশা শুরু হয় table থেকে। দল প্রথমে ভাবে “কোন কোন column?”, তারপর সেই column-এর চারপাশে কোড। ব্যবসার ভাষা (order, credit limit, wallet) হারিয়ে যায়
tbl_ord_mst-এর মতো নামে।
একটি রান্নাঘরের রেসিপি খাতার কথা ভাবুন। ভালো রেসিপি লেখা হয় এভাবে: “দুই কাপ চাল, মাঝারি আঁচে ১৫ মিনিট।” খারাপ রেসিপি লেখা হয় এভাবে: “অমুক কোম্পানির ৩ নম্বর মডেলের রাইস কুকারের লাল বোতাম চাপুন।” দ্বিতীয় খাতাটি সেই একটি যন্ত্র নষ্ট হলেই অকেজো; গ্যাসের চুলায় বা মাটির চুলায় সেই রেসিপি দিয়ে আর রান্না হয় না। Business-এর নিয়ম হলো রেসিপি; database আর web framework হলো চুলা আর হাঁড়ি। এই পর্বের সব architecture-এর মূল কথা একটিই: রেসিপি যেন চুলার ব্র্যান্ড না জানে।
এই চিন্তার ভিত্তি আপনি আগেই দেখেছেন। পর্ব ২৮-এর dependency inversion principle বলেছিল, উচ্চ-স্তরের নীতি নিম্ন-স্তরের খুঁটিনাটির ওপর নির্ভর করবে না; দুটিই abstraction-এর ওপর নির্ভর করবে। সেখানে কাজটি হয়েছিল একটি class-এর মাপে। এই পর্বে একই নীতি পুরো application-এর মাপে প্রয়োগ হবে, আর তার তিনটি জনপ্রিয় রূপ দেখবেন — hexagonal, onion আর Clean Architecture. শেষে আসবে domain-driven design-এর ধারণা, যা বলে কেন্দ্রের ভেতরটা কীভাবে সাজাবেন।
Layered নকশায় প্রশ্ন ছিল “কে কাকে ডাকে?” এই পর্বের architecture-গুলোর প্রশ্ন “কে কাকে চেনে?” ডাকের দিক (flow of control) বাইরের দিকে যেতে পারে — use case database-কে ডাকে। কিন্তু source code-এর নির্ভরতা (কে কার নাম import করে) সবসময় কেন্দ্রের দিকে — database-এর adapter use case-এর দেওয়া interface চেনে, use case database চেনে না।
২ · Hexagonal architecture — port আর adapter
এই ধারণার সবচেয়ে পরিচিত নাম hexagonal architecture. Alistair Cockburn এটি বর্ণনা করেন; তাঁর প্রবন্ধটি সাধারণত ২০০৫ সালের বলে উল্লেখ করা হয়, যদিও ধারণাটি নিয়ে তিনি তার আগেও লিখছিলেন। পরে তিনি নিজেই আরও বর্ণনামূলক নাম দেন: Ports and Adapters. তাঁর লক্ষ্য ছিল এমন application, যাকে মানুষ, অন্য program, automated test বা batch script — সবাই সমানভাবে চালাতে পারে, আর যাকে আসল database বা যন্ত্র ছাড়াই তৈরি ও test করা যায়।
ছয় কোণের আকারটির কোনো গাণিতিক মানে নেই। Cockburn ছবিতে জায়গা চেয়েছিলেন, যেখানে চারপাশে একাধিক port আঁকা যায়, আর “ওপর-নিচ” (layer) ভাবনার বদলে “ভেতর-বাইরে” ভাবনা আসে। তিনটি অংশ:
- Application core (ভেতর): business-এর নিয়ম আর use case. এখানে কোনো HTTP, SQL বা framework-এর import নেই।
- Port (সীমানার দরজা): core-এর নিজের ভাষায় লেখা একটি interface — “আমি এই কাজটি করতে পারি” বা “আমার এই কাজটি কাউকে করে দিতে হবে।” Port-এর মালিক core.
- Adapter (বাইরে): বাইরের একটি নির্দিষ্ট প্রযুক্তির সাথে port-এর অনুবাদ। এটি পর্ব ৩৪-এর adapter pattern-এর বড় রূপ — একটি interface-কে আরেকটির মাপে বসানো।
Driving আর driven — দুই দিকের port
🎮 Driving (primary) দিক
যে core-কে চালায়: web controller, CLI, message consumer, test script. Driving adapter বাইরের অনুরোধ (HTTP request, command-line argument) core-এর driving port-এর ডাকে অনুবাদ করে। এখানে port-টি core বাস্তবায়ন করে, adapter তাকে ডাকে।
উদাহরণ: PlaceOrder use case⚙️ Driven (secondary) দিক
যাকে core চালায়: database, payment gateway, SMS, email, ঘড়ি। Core একটি driven port-এর interface ঘোষণা করে; driven adapter সেটি বাস্তবায়ন করে, আর core তাকে ডাকে — কে আসলে আছে তা না জেনে।
উদাহরণ: PaymentPort, OrderRepository“Primary” আর “secondary” শব্দ দুটি এসেছে use case লেখার জগৎ থেকে — পর্ব ২২-এ primary actor (যিনি লক্ষ্য নিয়ে system-কে চালান) আর supporting actor (যার সাহায্য system নেয়) দেখেছেন। Cockburn নিজেও use case নিয়ে বিখ্যাত বইয়ের লেখক, তাই মিলটি কাকতালীয় নয়।
Hexagonal architecture — port আর adapter
টানা তীর = ডাক (call); ড্যাশ রেখা আর ফাঁপা ত্রিভুজ = UML realization (adapter port-এর interface বাস্তবায়ন করে)
প্রতিসাম্য আর test adapter
Cockburn-এর একটি সূক্ষ্ম কথা হলো প্রতিসাম্য (symmetry): layered নকশায় UI “ওপরে” আর database “নিচে” — দুটি সম্পূর্ণ আলাদা জিনিস। Hexagonal নকশায় দুটিই সমান মর্যাদার “বাইরের জগৎ”; শুধু দিক আলাদা — একটি core-কে চালায়, আরেকটিকে core চালায়। এর সবচেয়ে বড় লাভ test-এ। Driving দিকে browser-এর বদলে একটি test script সরাসরি use case ডাকে; driven দিকে আসল database-এর বদলে একটি memory-র dictionary বসে। মাঝের core-এর এক লাইনও বদলায় না।
নিচের stepper-এ একটি order দেওয়ার অনুরোধ বাইরে থেকে ভেতরে ঢুকে আবার বেরিয়ে যাচ্ছে। প্রতিটি ধাপে দেখুন data কোন রূপে আছে, আর কে কাকে চেনে।
- Web controller
POST /ordersdriving adapter - PlaceOrder
run()driving port - Use case
Order(...)core - PaymentPort
charge()driven port - OrderRepository
save()driven port - Response
201adapter-এ ফেরত
| এখন কোথায় | |
| data-র রূপ | |
| কে কাকে চেনে | |
| test-এ কী বসে |
- অনুরোধ এলো driving adapter-এ। Browser থেকে
POST /orders, body-তে customer=rina আর amount=300. Controller-ই একমাত্র জায়গা, যে HTTP, JSON আর status code বোঝে। - অনুবাদ করে driving port ডাকা। Controller HTTP-র খোলস খুলে সরল মান দিয়ে
PlaceOrder.run(1, "rina", 300)ডাকল। Use case জানে না ডাকটি browser থেকে, test থেকে, না একটি রাতের batch থেকে এলো। - Core-এর ভেতরে নিয়ম। Use case একটি
Orderentity বানাল, অবস্থা NEW. Order-এর file-এ কোনো framework, database বা HTTP-র import নেই। - Driven port দিয়ে টাকা কাটা। Use case ডাকল
PaymentPort.charge("rina", 300). পেছনে আছে WalletApi adapter, যে বাইরের payment সেবার সাথে কথা বলে। উত্তর True, তাই order PAID. ডাক বাইরের দিকে গেল, কিন্তু import-এর তীর ভেতরের দিকে: WalletApi চেনে PaymentPort-কে, উল্টোটা নয়। - Driven port দিয়ে সংরক্ষণ।
OrderRepository.save. পেছনে SqlOrders adapterINSERTচালায়। Test-এ এখানে বসত InMemoryOrders — একটি dictionary. - ফল ফিরল driving adapter-এ। Use case ফেরত দিল “PAID”; controller সেটিকে HTTP 201 আর JSON-এ অনুবাদ করল। পুরো যাত্রায় HTTP-র জ্ঞান ছিল শুধু প্রথম আর শেষ ধাপে, SQL-এর জ্ঞান শুধু ৫ম ধাপের adapter-এ।
এবার আসল কোডে। PaymentPort আর OrderRepository দুটি driven port — Python-এর typing.Protocol দিয়ে লেখা (Python-এ Protocol দেখুন)। PlaceOrder use case শুধু এই দুটি নাম জানে। প্রতিটি port-এর দুটি adapter: memory-তে চলা test adapter, আর একটি “production” adapter, যা আসল database-এর বদলে SQL statement ছাপায় (চালানোর যন্ত্রে database নেই, তাই এটি একটি খেলনা)। একই use case দুই জোড়া adapter দিয়ে চালানো হলো।
from dataclasses import dataclass
from typing import Protocol
@dataclass
class Order: # domain entity
id: int; customer: str; amount: int; status: str = "NEW"
class PaymentPort(Protocol): # driven ports, owned by the core
def charge(self, customer: str, amount: int) -> bool: ...
class OrderRepository(Protocol):
def save(self, order: Order) -> None: ...
class PlaceOrder: # use case: imports no adapter
def __init__(self, pay: PaymentPort, repo: OrderRepository): self.pay, self.repo = pay, repo
def run(self, oid, customer, amount):
order = Order(oid, customer, amount)
order.status = "PAID" if self.pay.charge(customer, amount) else "REJECTED"
self.repo.save(order)
return order.status
class InMemoryPayment: # test adapters
def __init__(self, wallet): self.wallet = wallet
def charge(self, c, amt):
if self.wallet.get(c, 0) < amt: return False
self.wallet[c] -= amt; return True
class InMemoryOrders(dict):
def save(self, o): self[o.id] = o.status
class SqlPayment: # "production" adapters (fake SQL)
def charge(self, c, amt):
print(f" SQL> UPDATE wallet SET bal = bal - {amt} WHERE cust = '{c}' AND bal >= {amt}")
return amt <= 1000 # pretend the DB answered
class SqlOrders:
def save(self, o): print(f" SQL> INSERT INTO orders VALUES ({o.id}, '{o.customer}', {o.amount}, '{o.status}')")
rows = InMemoryOrders()
test_uc = PlaceOrder(InMemoryPayment({"rina": 500}), rows)
print("test:", test_uc.run(1, "rina", 300), test_uc.run(2, "rina", 300), rows)
prod_uc = PlaceOrder(SqlPayment(), SqlOrders())
for oid, amt in ((3, 800), (4, 1500)): print("prod:", prod_uc.run(oid, "karim", amt))আউটপুট:
test: PAID REJECTED {1: 'PAID', 2: 'REJECTED'}
SQL> UPDATE wallet SET bal = bal - 800 WHERE cust = 'karim' AND bal >= 800
SQL> INSERT INTO orders VALUES (3, 'karim', 800, 'PAID')
prod: PAID
SQL> UPDATE wallet SET bal = bal - 1500 WHERE cust = 'karim' AND bal >= 1500
SQL> INSERT INTO orders VALUES (4, 'karim', 1500, 'REJECTED')
prod: REJECTED
প্রথম লাইন (test): rina-র wallet-এ ৫০০. প্রথম order ৩০০ — PAID, wallet-এ থাকল ২০০; দ্বিতীয় order ৩০০ — REJECTED. InMemoryOrders-এ দুটিই জমা: {1: 'PAID', 2: 'REJECTED'}. কোনো database, network বা file লাগেনি — হাজারটি এমন test এক সেকেন্ডের কম সময়ে চলতে পারে। পরের লাইনগুলো (prod): হুবহু একই PlaceOrder class, শুধু জোড়া আলাদা। ৮০০ টাকার order-এ adapter-এর “database” হ্যাঁ বলল, ১৫০০-তে না; প্রতিবার আগে UPDATE, পরে INSERT — কারণ use case আগে charge, পরে save ডাকে। লক্ষ করুন, PlaceOrder-এর ভেতরে InMemoryPayment বা SqlPayment নামটি কোথাও নেই। কোন adapter বসবে, তা ঠিক করে কোডের শেষের জোড়া দেওয়ার অংশ — পর্ব ৩৭-এর composition root. আর যে adapter memory-তে সত্যিই কাজ করে, test-এর ভাষায় সেটি একটি fake; test double-এর পুরো পরিবার পর্ব ৪৯-এ।
Port কার ভাষায় লেখা হবে: core-এর। PaymentPort.charge(customer, amount) — এখানে কোনো gateway-এর নিজস্ব শব্দ (merchant key, transaction token) নেই। বাইরের সেবার API যত অদ্ভুতই হোক, অনুবাদের ঝামেলা adapter-এর। বাইরের মডেল যাতে ভেতরে চুইয়ে না ঢোকে, তার জন্য DDD-তে anti-corruption layer নামের একটি বড় adapter-এর কথা বলা হয় (পর্ব ৩৪-এ সংক্ষেপে দেখেছেন)।
৩ · Onion আর Clean Architecture — বৃত্ত আর dependency rule
Hexagon-এর পরে এল বৃত্তের ছবি। ২০০৮ সালে Jeffrey Palermo কয়েকটি blog লেখায় onion architecture নাম দেন (সাধারণত এভাবেই উল্লেখ করা হয়): পেঁয়াজের মতো স্তর — একেবারে মাঝে domain model, তার চারপাশে domain service, তারপর application service, আর সবচেয়ে বাইরে UI, infrastructure আর test. তাঁর মূল দাবি: সব নির্ভরতা কেন্দ্রের দিকে, database কেন্দ্রে নয় বরং বাইরের একটি খুঁটিনাটি।
২০১২ সালের আগস্টে Robert C. Martin “The Clean Architecture” নামে একটি blog লেখা প্রকাশ করেন, আর ২০১৭ সালে একই নামের বই (Clean Architecture: A Craftsman's Guide to Software Structure and Design). Blog-এ তিনি নিজেই বলেন, এটি নতুন আবিষ্কার নয়; hexagonal, onion, Jacobson-এর BCE (boundary-control-entity) আর আরও কয়েকটি ধারণার সাধারণ সূত্রকে একটি নিয়মে বাঁধার চেষ্টা। Clean Architecture-এর চারটি বৃত্ত, ভেতর থেকে বাইরে:
| বৃত্ত | কী থাকে | মুদি-বাজার app-এ |
|---|---|---|
| Entities (সবচেয়ে ভেতরে) | পুরো প্রতিষ্ঠানের মূল ব্যবসায়িক নিয়ম; সবচেয়ে কম বদলায় | Order, Money, “মোট ≤ credit limit” |
| Use cases | এই application-এর নির্দিষ্ট কাজের ধারা (Martin-এর ভাষায় application-specific business rules); entity-কে পরিচালনা করে | PlaceOrder, CancelOrder, আর তাদের port |
| Interface adapters | Data-কে use case-এর সুবিধার রূপ থেকে বাইরের সুবিধার রূপে বদলায়: controller, presenter, gateway (repository-র বাস্তবায়ন); SQL থাকলে এই স্তরের বাইরে যায় না | OrderController, SqlOrderRepo |
| Frameworks & drivers (সবচেয়ে বাইরে) | Web framework, database নিজে, UI, device; মূলত জোড়া দেওয়ার “glue” কোড | web server, ORM library, main |
বৃত্তগুলোর ওপর একটিই আইন, যার নাম dependency rule: source code-এর নির্ভরতা শুধু ভেতরের দিকে যেতে পারে। ভেতরের বৃত্তের কোনো কিছু বাইরের বৃত্তের কোনো কিছুর নাম জানবে না — class, function, variable, এমনকি data-র format-ও নয়। Martin এটাও বলেন যে চারটি বৃত্ত একটি নমুনা, আইন নয়; বৃত্ত বেশি-কম হতে পারে, কিন্তু নির্ভরতার দিক বদলায় না।
Clean Architecture-এর চার বৃত্ত
তীর = source code-এর নির্ভরতা (import); সব তীর কেন্দ্রের দিকে
এই নিয়মটি যন্ত্র দিয়েও যাচাই করা যায় — import-এর তালিকা দেখে। নিচের stepper একটি ছোট project-এর ছয়টি import পরপর পরীক্ষা করছে। প্রতিটি module-এর একটি ring নম্বর (০ = entities, ৩ = frameworks). নিয়ম: কোনো module নিজের চেয়ে বড় নম্বরের ring-কে import করবে না; আর বাইরের library (ORM, web framework) import করতে পারবে শুধু ring ২ আর ৩।
1// domain/order.js (ring 0)2import { Money } from "./money.js"3import { Column } from "lib/orm"4// app/placeOrder.js (ring 1)5import { OrderRepository } from "./ports.js"6import { SqlOrderRepo } from "../adapters/sqlOrderRepo.js"7// adapters/orderController.js (ring 2)8import { PlaceOrder } from "../app/placeOrder.js"9// adapters/sqlOrderRepo.js (ring 2)10import { OrderRepository } from "../app/ports.js"
| import | |
| ring | |
| দিক | |
| নিয়মভঙ্গ |
order → money. দুটিই ring ০, একই বৃত্তের ভেতরে নির্ভরতা — অনুমোদিত। Entity আরেকটি value object ব্যবহার করতেই পারে।order → lib/orm. সবচেয়ে ভেতরের entity একটি ORM library-রColumnimport করছে — table-এর খুঁটিনাটি ঢুকে পড়েছে ব্যবসার মূল নিয়মে। প্রথম নিয়মভঙ্গ। সমাধান: mapping থাকবে adapter-এ, entity থাকবে সাধারণ class.placeOrder → ports. Use case তার নিজের বৃত্তের port চেনে — ring ১ থেকে ১, অনুমোদিত।placeOrder → sqlOrderRepo. Use case (ring ১) সরাসরি SQL adapter (ring ২) import করছে — নির্ভরতা বাইরের দিকে। দ্বিতীয় নিয়মভঙ্গ। সমাধান DIP: use case শুধুOrderRepositoryport চিনবে, আর আসল adapter composition root থেকে আসবে।orderController → placeOrder. Ring ২ থেকে ১ — বাইরে থেকে ভেতরে, ঠিক যেমন হওয়া উচিত। Driving adapter use case ডাকে।sqlOrderRepo → ports. Ring ২ থেকে ১ — adapter core-এর port বাস্তবায়ন করছে। ডাক যাবে use case থেকে adapter-এ (বাইরের দিকে), কিন্তু import-এর তীর adapter থেকে port-এ (ভেতরের দিকে)। এটিই dependency inversion.- ফল: ছয়টি import-এ দুটি নিয়মভঙ্গ। হাতে ধরে দেখা এই ছয়টি একটি বড় তালিকার অংশ; নিচের checker পুরো ১৭টি import পরীক্ষা করে আরও একটি লুকানো ভঙ্গ খুঁজে পাবে।
// Dependency-rule checker. Ring 0 = innermost (entities), 3 = outermost.
const ring = {
"domain/money": 0, "domain/order": 0,
"app/ports": 1, "app/placeOrder": 1,
"adapters/orderController": 2, "adapters/orderDto": 2, "adapters/sqlOrderRepo": 2,
"web/server": 3, "main": 3,
};
const libs = new Set(["lib/orm", "lib/web"]); // third-party: only rings 2-3 may import
const imports = {
"domain/money": [],
"domain/order": ["domain/money", "lib/orm"],
"app/ports": ["domain/order"],
"app/placeOrder": ["app/ports", "domain/order", "adapters/sqlOrderRepo", "adapters/orderDto"],
"adapters/orderController": ["app/placeOrder", "adapters/orderDto"],
"adapters/orderDto": [],
"adapters/sqlOrderRepo": ["app/ports", "domain/order", "lib/orm"],
"web/server": ["adapters/orderController", "lib/web"],
"main": ["web/server", "adapters/sqlOrderRepo", "app/placeOrder"],
};
const names = ["entities", "use cases", "adapters", "frameworks"];
let edges = 0;
const bad = [];
for (const [from, tos] of Object.entries(imports)) {
for (const to of tos) {
edges++;
const outward = libs.has(to) ? ring[from] < 2 : ring[to] > ring[from];
if (outward) bad.push([from, to]);
}
}
console.log("edges checked:", edges, "| violations:", bad.length);
for (const [from, to] of bad) {
const where = libs.has(to) ? "library" : names[ring[to]];
console.log(` OUTWARD ${from} (${names[ring[from]]}) -> ${to} (${where})`);
}
console.log("allowed edges:", edges - bad.length);আউটপুট:
edges checked: 17 | violations: 3
OUTWARD domain/order (entities) -> lib/orm (library)
OUTWARD app/placeOrder (use cases) -> adapters/sqlOrderRepo (adapters)
OUTWARD app/placeOrder (use cases) -> adapters/orderDto (adapters)
allowed edges: 14
১৭টি import-এর ১৪টি অনুমোদিত, ৩টি বাইরের দিকে। প্রথম দুটি stepper-এ দেখেছেন। তৃতীয়টি সূক্ষ্ম: app/placeOrder ফল ফেরত দিচ্ছে adapters/orderDto-তে সংজ্ঞায়িত একটি data class দিয়ে। দেখতে নিরীহ — “শুধু একটি data-র খোলস!” — কিন্তু use case এখন controller-এর বৃত্তের একটি নাম জানে; controller-এর জন্য DTO-র একটি field বদলালে use case-ও বদলাতে হবে। Martin-এর নকশায় use case-এর input আর output model use case-এর বৃত্তেই থাকে; controller সেগুলো নিজের format-এ রূপান্তর করে। লক্ষ করুন, adapters/sqlOrderRepo → lib/orm আর web/server → lib/web অনুমোদিত: বাইরের বৃত্তের কোড framework ব্যবহার করবেই — সেটিই তার কাজ। বাস্তবে এমন যাচাই হাতে করতে হয় না: বিভিন্ন ভাষায় import-এর নিয়ম পরীক্ষার tool আছে (Java-য় ArchUnit, JavaScript-এ dependency-cruiser, Python-এ import-linter-এর মতো — নাম আর ক্ষমতা version-ভেদে বদলায়), আর এগুলো CI-তে চালানো যায় (পর্ব ৫৮)।
৪ · Boundary পার হওয়া — DIP আর DTO
Dependency rule শুনে প্রথম প্রশ্ন আসে: use case-কে তো ফল দেখাতে হবে, database-এ লিখতে হবে — বাইরের কাউকে না চিনে সে বাইরে ডাকবে কীভাবে? উত্তর দুটি সরঞ্জামে।
ডাকের দিক উল্টানো — interface ভেতরে, বাস্তবায়ন বাইরে
Use case যখন বাইরে কিছু করাতে চায়, সে নিজের বৃত্তে একটি interface ঘোষণা করে — OrderRepository, PaymentPort, বা ফল দেখানোর জন্য একটি output port (Martin-এর ছবিতে “Use Case Output Port”, যা বাস্তবায়ন করে presenter). বাইরের adapter সেই interface বাস্তবায়ন করে। ফলে flow of control বাইরের দিকে যায়, কিন্তু source dependency ভেতরের দিকে থাকে। এটি হুবহু পর্ব ২৮-এর DIP: abstraction-এর মালিক policy, detail তার সাথে মানিয়ে নেয়। আর আসল adapter কে বানিয়ে use case-কে দেবে? Composition root — app-এর সবচেয়ে বাইরের main (পর্ব ৩৭)। সেজন্যই composition root সবচেয়ে বাইরের বৃত্তে থাকে: একমাত্র সেখানেই সবার নাম জানা বৈধ।
সীমানা পেরোয় শুধু সরল data — DTO
দ্বিতীয় প্রশ্ন: সীমানা পেরিয়ে কোন রূপে data যাবে? Martin-এর উত্তর: সরল data structure — সাধারণ object, dictionary বা function-এর argument. এদের বলা হয় DTO (data transfer object). যা যাবে না: ORM-এর row object (তাতে database-এর জ্ঞান লুকিয়ে থাকে), web framework-এর request object, বা অনেক সময় entity নিজেও (তাহলে বাইরের বৃত্ত entity-র ভেতরের খুঁটিনাটির ওপর নির্ভর করবে)।
@dataclass(frozen=True)
class PlaceOrderInput: # owned by the use-case ring
customer: str
amount: int
@dataclass(frozen=True)
class PlaceOrderOutput: # also owned by the use-case ring
order_id: int
status: str
class PlaceOrderPresenter(Protocol): # output port; a presenter implements it
def present(self, out: PlaceOrderOutput) -> None: ...
class PlaceOrder:
def __init__(self, pay, repo, presenter: PlaceOrderPresenter):
self.pay, self.repo, self.presenter = pay, repo, presenter
def run(self, cmd: PlaceOrderInput) -> None:
order = Order(self.repo.next_id(), cmd.customer, cmd.amount)
order.status = "PAID" if self.pay.charge(cmd.customer, cmd.amount) else "REJECTED"
self.repo.save(order)
self.presenter.present(PlaceOrderOutput(order.id, order.status))
class JsonPresenter: # interface-adapters ring
def present(self, out):
self.body = {"id": out.order_id, "status": out.status}উদাহরণটিতে PlaceOrderInput আর PlaceOrderOutput দুটিই use case-এর বৃত্তে, কারণ use case তাদের মালিক। Controller HTTP body থেকে PlaceOrderInput বানায়; presenter PlaceOrderOutput পেয়ে ঠিক করে সেটি JSON হবে, HTML হবে, না SMS-এর লেখা। Use case জানে না ফল কোথায় দেখানো হবে।
Mapping-এর দাম সত্যি: HTTP body → input DTO → entity → database row, আবার উল্টো পথে — প্রতিটি সীমানায় একটি রূপান্তর। ছোট app-এ এটি “একই field চারবার লেখা”-র মতো লাগে, আর লাগেও। অনেক দল তাই একটি মাঝামাঝি পথ নেয়: ভেতরের দিকে যাওয়া data-র জন্য আলাদা DTO, কিন্তু read-only প্রদর্শনের জন্য entity-র অপরিবর্তনীয় একটি রূপ সরাসরি বাইরে দেওয়া। এটি নিয়মের শিথিল প্রয়োগ — জেনে-বুঝে নেওয়া একটি trade-off, যা পর্ব ৩৮-এর ADR-এ লিখে রাখার মতো।
৫ · তিনটি নাম, একটি মূল ধারণা
পরীক্ষা আর ইন্টারভিউতে প্রায়ই জিজ্ঞাসা করা হয়: hexagonal, onion আর Clean — পার্থক্য কী? সৎ উত্তর হলো: মূল ধারণা একই — business-এর নিয়ম কেন্দ্রে, খুঁটিনাটি বাইরে, নির্ভরতা ভেতরের দিকে। পার্থক্য মূলত ছবিতে, শব্দে আর কোন দিকে জোর দেওয়া হয়েছে তাতে।
| দিক | Hexagonal (Ports & Adapters) | Onion | Clean |
|---|---|---|---|
| প্রবর্তক, সময় | Alistair Cockburn, প্রবন্ধ সাধারণত ২০০৫ বলে উল্লেখিত | Jeffrey Palermo, ২০০৮-এর blog লেখা | Robert C. Martin, ২০১২-র blog, ২০১৭-র বই |
| ছবি | ছয় কোণা; ভেতর আর বাইরে, চারপাশে port | পেঁয়াজের মতো কয়েকটি স্তর | চারটি এককেন্দ্রিক বৃত্ত |
| মূল শব্দ | port, adapter, driving, driven | domain model, domain service, application service, infrastructure | entities, use cases, interface adapters, frameworks & drivers, dependency rule |
| কোন দিকে জোর | সীমানা আর প্রতিসাম্য; বাইরের প্রতিটি জিনিস বদলানো ও test-এ নকল বসানো | Domain model-কে কেন্দ্রে রাখা, database-কে বাইরে ঠেলে দেওয়া | ভেতরের স্তরবিন্যাস আর নির্ভরতার একটি স্পষ্ট আইন |
| ভেতরে স্তর আছে? | বলে না — core একটি এক | হ্যাঁ, কয়েকটি | হ্যাঁ, entities আর use cases আলাদা |
একটি কাজের তুলনা: hexagonal বলে core-এর সীমানা কেমন হবে, Clean বলে সীমানার ভেতরে কী কী স্তর থাকবে, আর onion দুটির মাঝামাঝি। বাস্তবের অনেক codebase তিনটির শব্দ মিশিয়ে ব্যবহার করে — “use case” Clean থেকে, “port/adapter” hexagonal থেকে। এতে ভুল কিছু নেই, যদি দল একই শব্দে একই জিনিস বোঝে।
ধারণাটির শিকড় আরও পুরোনো। Ivar Jacobson-এর ১৯৯২ সালের Object-Oriented Software Engineering বইয়ে boundary, control আর entity object-এর ভাগ আছে — বাইরের সাথে কথা বলা, কাজের ধারা পরিচালনা আর ব্যবসার তথ্য। Clean Architecture-এর controller/use case/entity-র সাথে মিলটি চোখে পড়ার মতো। আর তারও আগে, ১৯৭২ সালে David Parnas-এর information hiding (পর্ব ২৫-এর coupling আর cohesion-এর আলোচনা) বলেছিল: যে সিদ্ধান্ত বদলাতে পারে, তাকে একটি module-এর পেছনে লুকান। Database-এর পছন্দ ঠিক এমন একটি সিদ্ধান্ত।
৬ · Domain-driven design-এর মূল কথা
Hexagonal আর Clean বলে core কোথায়, কিন্তু core-এর ভেতরটা কীভাবে সাজাবেন, তা প্রায় বলে না। এই শূন্যতা পূরণ করে domain-driven design (DDD) — Eric Evans-এর ২০০৩ সালের বই Domain-Driven Design: Tackling Complexity in the Heart of Software. পরে Vaughn Vernon-এর Implementing Domain-Driven Design (২০১৩) অনেক ব্যবহারিক নিয়ম যোগ করে। DDD-র মূল দাবি: জটিল software-এর আসল কঠিন অংশ প্রযুক্তি নয়, ব্যবসার জ্ঞান — তাই নকশা শুরু হোক domain থেকে, domain expert-দের সাথে বসে।
Ubiquitous language — দল আর কোডের এক ভাষা
Ubiquitous language মানে domain expert, developer আর কোড — সবাই একই শব্দ একই অর্থে ব্যবহার করবে। দোকানের হিসাবরক্ষক যদি বলেন “বাকির খাতা”, কোডে CreditLedger থাকুক, tbl_cust_bal2 নয়। কোনো শব্দ নিয়ে আলোচনায় বিভ্রান্তি দেখা দিলে সেটি নকশার সমস্যার সংকেত। পর্ব ২৩-এর domain model আর domain glossary এখানে কোডে রূপ নেয়।
Entity বনাম value object
🪪 Entity
পরিচয় (identity) দিয়ে চেনা যায়, আর সময়ের সাথে অবস্থা বদলায়। Order #7-এর দাম, ঠিকানা, অবস্থা বদলালেও সেটি সেই Order #7. দুটি order-এর সব field এক হলেও id আলাদা হলে তারা আলাদা।
Order, Customer, Shipment💵 Value object
শুধু মান দিয়ে চেনা যায়; কোনো id নেই, সাধারণত অপরিবর্তনীয়। দুটি ৫০০ টাকার নোট বিনিময়যোগ্য — কোনটি কোনটি, তাতে কিছু আসে যায় না। Value object নিজের নিয়ম নিজেই রক্ষা করে।
Money, Address, DateRangeValue object-এর অভাবে একটি পরিচিত ভুল হয়, যার নাম primitive obsession: টাকা, ফোন নম্বর, তারিখ — সবকিছু সাধারণ int বা str. তখন টাকার সাথে ডলার যোগ হয়ে যায়, আর কেউ টেরও পায় না (code smell হিসেবে এটি আবার আসবে পর্ব ৪২-এ)। ১৯৯৯ সালে Mars Climate Orbiter হারানোর তদন্তে একটি কারণ হিসেবে উঠে আসে যে এক দলের software ইম্পেরিয়াল একক (pound-force second) আর আরেক দল metric একক (newton second) ধরে নিয়েছিল — সংখ্যা ছিল, একক ছিল না। নিচের কোডে প্রথমে সেই ভুল, তারপর Money value object, তারপর একটি aggregate.
Aggregate আর invariant
Aggregate হলো কয়েকটি entity আর value object-এর একটি গুচ্ছ, যা একসাথে একটি নিয়ম (invariant) রক্ষা করে, আর বাইরে থেকে যাকে একটি একক হিসেবে বদলানো হয়। গুচ্ছের প্রবেশদ্বার একটি entity — aggregate root. Order একটি aggregate root; তার order line-গুলো তার ভেতরে। “মোট দাম ≤ credit limit” — এই invariant রক্ষা করা Order-এর দায়, কারণ শুধু Order-ই সব line একসাথে দেখে। বাইরের কোড line-এর list-এ সরাসরি হাত দেবে না; সব বদল root-এর method দিয়ে।
from dataclasses import dataclass, FrozenInstanceError
cart_taka, shipping_dollar = 500, 5 # primitive obsession: bare ints
print("primitive total:", cart_taka + shipping_dollar) # silently wrong
@dataclass(frozen=True)
class Money: # value object
amount: int # whole units, for simplicity
currency: str
def __add__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot add {self.currency} and {other.currency}")
return Money(self.amount + other.amount, self.currency)
def times(self, n): return Money(self.amount * n, self.currency)
print("equal by value:", Money(500, "BDT") == Money(500, "BDT"))
try: Money(500, "BDT") + Money(5, "USD")
except ValueError as e: print("Money says:", e)
try: Money(500, "BDT").amount = 1
except FrozenInstanceError: print("Money is immutable")
class Order: # aggregate root
def __init__(self, oid, credit_limit):
self.id, self.limit, self._lines = oid, credit_limit, []
def total(self):
t = Money(0, self.limit.currency)
for sku, price, qty in self._lines: t = t + price.times(qty)
return t
def add_line(self, sku, price, qty): # the only way in
new_total = self.total() + price.times(qty)
if new_total.amount > self.limit.amount:
raise ValueError(f"credit limit {self.limit.amount} < {new_total.amount}")
self._lines.append((sku, price, qty))
o = Order(7, Money(2000, "BDT"))
o.add_line("rice-5kg", Money(650, "BDT"), 2)
try: o.add_line("oil-5l", Money(900, "BDT"), 1)
except ValueError as e: print("rejected:", e)
print("total:", o.total(), "| lines:", len(o._lines))আউটপুট:
primitive total: 505
equal by value: True
Money says: cannot add BDT and USD
Money is immutable
rejected: credit limit 2000 < 2200
total: Money(amount=1300, currency='BDT') | lines: 1
primitive total: 505 — ৫০০ টাকা আর ৫ ডলার যোগ হয়ে “৫০৫”, যা কোনো মুদ্রাতেই সঠিক নয়, আর program কোনো আপত্তি করেনি। Money-তে একই ভুল সাথে সাথে ValueError. @dataclass নিজে __eq__ বানায় (তাই দুটি ৫০০ BDT সমান — মান দিয়ে তুলনা), আর frozen=True থাকায় field বদলানোর চেষ্টায় FrozenInstanceError ছোড়ে (Python-এ dataclass). এরপর aggregate: চাল ২ × ৬৫০ = ১৩০০, limit ২০০০-এর মধ্যে — গৃহীত। তেল ৯০০ যোগ করলে ২২০০ — add_line বদল করার আগেই হিসাব করে প্রত্যাখ্যান করল, তাই শেষে মোট ১৩০০ আর line একটিই। Aggregate কখনো এক মুহূর্তের জন্যও অবৈধ অবস্থায় যায়নি। একটি সৎ সতর্কতা: Python-এ _lines-এর আগের underscore শুধু একটি রীতি — কেউ চাইলে o._lines.append(...) লিখতে পারেন। ভাষা যা জোর করে না, তা code review আর রীতি দিয়ে রক্ষা করতে হয়; JavaScript-এর #private field সত্যিই আটকায়, যা পরের অংশে দেখবেন।
নিচের stepper-এ একই Order aggregate-এর ওপর পাঁচটি command, আর শেষে সংরক্ষণ। প্রতিটি command হয় পুরোপুরি গৃহীত, নয় পুরোপুরি প্রত্যাখ্যাত — মাঝামাঝি কিছু নেই।
- Order #7
limit 2000তৈরি - চাল
650 × 2গ্রহণ - তেল ৫ লি.
900 × 1প্রত্যাখ্যান - তেল ১ লি.
200 × 3গ্রহণ - লবণ
50 USDপ্রত্যাখ্যান - repo.save
orderএক transaction
| command | |
| হিসাব | |
| ফল | |
| line সংখ্যা | |
| মোট |
- Aggregate তৈরি। Order #7, credit limit ২০০০ টাকা, কোনো line নেই, মোট ০। Invariant শুরু থেকেই সত্য।
- চাল যোগ। ৬৫০ × ২ = ১৩০০; ০ + ১৩০০ = ১৩০০, যা ২০০০-এর কম।
add_lineহিসাব মিলিয়ে তবেই line যোগ করল। মোট ১৩০০, line ১টি। - ৫ লিটার তেল — প্রত্যাখ্যান। ১৩০০ + ৯০০ = ২২০০, limit ছাড়ায়। Aggregate বদলের আগেই
ValueErrorছুড়ল; line-এর তালিকায় কিছুই যোগ হয়নি, মোট এখনো ১৩০০। - ১ লিটারের ৩টি তেল — গ্রহণ। ২০০ × ৩ = ৬০০; ১৩০০ + ৬০০ = ১৯০০ ≤ ২০০০। Line ২টি, মোট ১৯০০।
- ডলারে দাম — প্রত্যাখ্যান, তবে অন্য পাহারাদারের হাতে। লবণের দাম ৫০ USD.
add_line-এর হিসাবে BDT মোটের সাথে USD যোগ করতে গেলেইMoneyনিজে আপত্তি করল। Aggregate-কে মুদ্রার নিয়ম আলাদা করে লিখতে হয়নি — value object তা রক্ষা করে। মোট ১৯০০-ই থাকল। - সংরক্ষণ — পুরো aggregate একসাথে। Repository পুরো Order (দুটি line-সহ) একটি transaction-এ জমা রাখে। DDD-র প্রচলিত নিয়ম (Vernon-এর বইয়ে জোর দিয়ে বলা): এক transaction-এ একটিই aggregate বদলাবে; অন্য aggregate-কে চিনবে শুধু id দিয়ে, object ধরে রেখে নয়।
Repository, domain service আর domain event
- Repository: প্রতিটি aggregate root-এর জন্য একটি repository — একটি collection-এর মতো interface (
save,get(id)), যার পেছনে database লুকানো। Hexagonal-এর ভাষায় এটি একটি driven port; Git-এর “repository”-র সাথে নামের মিল কাকতালীয়। - Domain service: যে নিয়ম কোনো একটি entity-র স্বাভাবিক দায় নয় — যেমন দুটি wallet-এর মধ্যে টাকা স্থানান্তর — তা একটি stateless service-এ। সতর্কতা: সব নিয়ম service-এ ঠেলে দিলে entity ফাঁপা হয়ে যায় (পরের অংশ)।
- Domain event: domain-এ ঘটে যাওয়া একটি অর্থবহ ঘটনা, অতীত কালে নাম:
OrderPlaced,PaymentRejected. অন্য অংশ (SMS পাঠানো, stock কমানো) এতে সাড়া দেয়, Order-কে তাদের চিনতে হয় না — পর্ব ৩৫-এর observer-এর বড় রূপ। Evans-এর মূল বইয়ে এটি building block হিসেবে প্রধান ছিল না; পরে DDD সমাজে এটি কেন্দ্রীয় হয়ে ওঠে বলে সাধারণত বলা হয়। Event দিয়ে পুরো অবস্থা রাখার কৌশল দেখুন System Design-এর event sourcing পর্বে।
Bounded context আর context map
একটি বড় প্রতিষ্ঠানে একই শব্দের অর্থ বিভাগভেদে আলাদা। মুদি-বাজারের বিক্রয় বিভাগে “product” মানে ছবি, দাম আর বিবরণ; গুদামে “product” মানে তাক নম্বর, ওজন আর মজুত। সবার জন্য একটিই বিশাল Product class বানালে সেটি কারও কাজের হয় না। DDD-র উত্তর bounded context: একটি স্পষ্ট সীমানা, যার ভেতরে একটি model আর একটি ভাষা সুসংগত। বিভিন্ন context-এর সম্পর্ক আঁকা হয় একটি context map-এ — কে কার কাছ থেকে data নেয় (customer/supplier), কে অন্যের model হুবহু মেনে নেয় (conformist), কোথায় অনুবাদের জন্য anti-corruption layer, কোথায় দুই দল একটি ছোট model ভাগ করে (shared kernel). Service-এর মাপে এই সীমানা কীভাবে microservice-এর সীমানা হয়, তা দেখুন System Design-এর microservices পর্বে; এখানে শুধু এটুকু মনে রাখুন: একটি codebase-এর ভেতরেও bounded context আলাদা module বা package হতে পারে।
৭ · Anemic বনাম rich domain model
২০০৩ সালে Martin Fowler তাঁর bliki-তে একটি লেখায় anemic domain model-কে একটি anti-pattern বলেন (লেখাটি সাধারণত এভাবেই উদ্ধৃত হয়)। লক্ষণ: domain object-গুলো দেখতে আসল — Order, Customer, getter-setter সব আছে — কিন্তু ভেতরে কোনো আচরণ নেই; সব নিয়ম থাকে আলাদা “service” class-এ। Fowler-এর আপত্তি: object-oriented নকশার মূল কথা — data আর তার ওপরের আচরণ একসাথে — বাদ পড়ে যায়, অথচ domain model-এর সব খরচ (mapping, স্তর) ঠিকই দিতে হয়। এর বিপরীত rich domain model: নিয়ম থাকে data-র পাশে, object নিজের invariant নিজে রক্ষা করে।
সবচেয়ে বড় ঝুঁকিটি হলো invariant “চুইয়ে বেরিয়ে যাওয়া”। নিচে একই নিয়ম (“মোট ≤ ২০০০”) দুইভাবে।
// Same rule, two models: order total must never exceed the credit limit.
const total = lines => lines.reduce((s, l) => s + l.price * l.qty, 0);
// ANEMIC: a data bag; the rule lives in a service that callers may skip.
const anemic = { limit: 2000, lines: [] };
const OrderService = {
addLine(o, sku, price, qty) {
if (total(o.lines) + price * qty > o.limit) throw new Error("limit exceeded");
o.lines.push({ sku, price, qty });
},
};
OrderService.addLine(anemic, "rice", 650, 2);
anemic.lines.push({ sku: "oil", price: 900, qty: 1 }); // an import script skips the service
anemic.lines[0].qty = 4; // a "quick fix" elsewhere
console.log("anemic total:", total(anemic.lines), "| broken:", total(anemic.lines) > anemic.limit);
// RICH: the data is private; every change goes through a guarded method.
class Order {
#lines = []; #limit;
constructor(limit) { this.#limit = limit; }
#commit(next) {
if (total(next) > this.#limit) throw new Error(`limit ${this.#limit} < ${total(next)}`);
this.#lines = next;
}
addLine(sku, price, qty) { this.#commit([...this.#lines, { sku, price, qty }]); }
changeQty(sku, qty) { this.#commit(this.#lines.map(l => (l.sku === sku ? { ...l, qty } : l))); }
get total() { return total(this.#lines); }
get lines() { return this.#lines.map(l => ({ ...l })); } // copies only
}
const rich = new Order(2000);
rich.addLine("rice", 650, 2);
for (const cmd of [() => rich.addLine("oil", 900, 1), () => rich.changeQty("rice", 4)]) {
try { cmd(); } catch (e) { console.log("rich rejected:", e.message); }
}
rich.lines.push({ sku: "oil", price: 900, qty: 1 }); // pushes into a copy
rich.lines[0].qty = 4; // edits a copy
console.log("rich total:", rich.total, "| lines:", rich.lines.length);আউটপুট:
anemic total: 3500 | broken: true
rich rejected: limit 2000 < 2200
rich rejected: limit 2000 < 2600
rich total: 1300 | lines: 1
Anemic: নিয়মটি OrderService.addLine-এ ঠিকঠাকই লেখা, আর চালের line সেখান দিয়েই ঢুকেছে। কিন্তু data খোলা: কয়েক মাস পরের একটি import script service এড়িয়ে সরাসরি lines.push করল, আরেকটি “দ্রুত সমাধান” চালের পরিমাণ ৪ করল। মোট ৬৫০ × ৪ + ৯০০ = ৩৫০০ — limit-এর প্রায় দ্বিগুণ, আর কোথাও কোনো error হয়নি। নিয়মটি ছিল, কিন্তু বাধ্যতামূলক ছিল না; প্রতিটি নতুন caller-কে মনে রাখতে হতো service ডাকার কথা। Rich: #lines সত্যিকারের private (JavaScript-এর # field বাইরে থেকে পড়া-লেখা যায় না)। বদলের দুটি পথই — addLine আর changeQty — একই #commit-এর পাহারা পার হয়: তেল ২২০০-তে, চাল ×৪ ২৬০০-তে প্রত্যাখ্যাত। lines getter কপি ফেরত দেয়, তাই বাইরের push বা qty = 4 কপিতে পড়ে, আসল order অক্ষত — মোট ১৩০০, line ১টি। #commit আগে নতুন তালিকা বানিয়ে যাচাই করে, তারপর বদলায় — তাই প্রত্যাখ্যানের পরও কোনো আধা-বদল থাকে না।
Anemic সবসময় ভুল নয়। যেখানে আসলে নিয়ম প্রায় নেই — form থেকে পড়ে table-এ লেখা — সেখানে data-র খোলস আর একটি সরল function-ই সঠিক নকশা; Fowler নিজেও Patterns of Enterprise Application Architecture (২০০২)-এ সরল logic-এর জন্য transaction script-কে বৈধ বিকল্প বলেছেন। আবার functional programming-এর ধারায় data আর function আলাদা রাখাই স্বাভাবিক; সেখানে invariant রক্ষা হয় অপরিবর্তনীয় data আর “শুধু এই function দিয়েই বানানো যায়” এমন constructor দিয়ে। সমস্যা anemic model নিজে নয় — সমস্যা হলো জটিল নিয়ম থাকা সত্ত্বেও তা এমন জায়গায় রাখা, যেটি এড়িয়ে যাওয়া যায়।
৮ · দাম, আর কখন ব্যবহার করবেন
এই পর্বের architecture-গুলো বিনামূল্যে আসে না। তিনটি দাম সবসময় দিতে হয়:
- Indirection: “Save চাপলে কী হয়?” — উত্তর খুঁজতে controller থেকে use case, use case থেকে port, port থেকে… কোন adapter? সেটি জানতে composition root-এ যেতে হয়। IDE-র “go to definition” আপনাকে interface-এ নিয়ে থামে।
- বেশি file আর mapping: একটি feature-এ controller, input DTO, use case, port, adapter, mapper, output DTO, presenter — সহজেই ৮টি file, যেখানে সরল নকশায় ২টি। প্রতিটি সীমানায় field-গুলো আবার লেখা।
- CRUD app-এ over-engineering: যদি app-এর কাজ শুধু form থেকে পড়ে table-এ লেখা আর table থেকে পড়ে দেখানো (CRUD), তাহলে কেন্দ্রে রক্ষা করার মতো নিয়ম প্রায় নেই। চার বৃত্ত তখন শুধু একই data-কে চারবার হাতবদল করে।
| পোষায় যখন… | সাধারণত পোষায় না যখন… |
|---|---|
| Business-এর নিয়ম জটিল আর বদলায় (দাম, ছাড়, credit, ফলাফল হিসাব) | App প্রায় পুরোটাই CRUD — form আর table |
| System বহু বছর চলবে; framework বা database বদলানোর বাস্তব সম্ভাবনা আছে | Prototype, এক সপ্তাহের demo, বা একবার চালানোর script |
| একই কাজ কয়েকটি পথে আসে: web, mobile API, batch, message queue | একটিই পথ, একটিই database, আর ছোট দল |
| নিয়মের দ্রুত test দরকার (হাজারো ক্ষেত্র, database ছাড়া) | Framework নিজেই যথেষ্ট test-সুবিধা দেয় আর নিয়ম সামান্য |
একটি বাস্তব মাঝামাঝি পথ: সব জায়গায় চার বৃত্ত নয়, বরং ঝুঁকির জায়গায় একটি port. যেমন payment gateway বাইরের, অস্থির আর test-এ বিপজ্জনক — শুধু তার জন্য একটি PaymentPort আর দুটি adapter. বাকি CRUD অংশ framework-এর সাধারণ পথে। নিয়ম জটিল হতে থাকলে, অংশটি ধীরে ধীরে core-এ সরানো যায় — পর্ব ৪৩-এর পুরোনো কোড সামলানোর কৌশলের মতো ছোট ধাপে। পরিচ্ছন্ন নাম আর ছোট function (পর্ব ৪১) যেকোনো architecture-এ লাগে; architecture সেগুলোর বিকল্প নয়।
আরও গভীরে: Martin-এর Clean Architecture (২০১৭) বইয়ে component-এর নীতি (stable dependencies, stable abstractions — পর্ব ২৫) আর “Humble Object” pattern আছে, যা দিয়ে test করা কঠিন অংশ (UI, database) পাতলা রেখে বাকিটা test-যোগ্য করা হয়। Evans-এর বইয়ের দ্বিতীয়ার্ধ — strategic design, context map, core domain — অনেকের মতে প্রথমার্ধের building block-এর চেয়েও গুরুত্বপূর্ণ; Vernon-এর ২০১৩ সালের বই aggregate-এর নকশার নিয়ম (ছোট aggregate, id দিয়ে reference, এক transaction-এ এক aggregate) বিস্তারিত বলে। Cockburn পরে hexagonal architecture নিয়ে আরও বিস্তারিত লেখা প্রকাশ করেছেন বলে জানা যায়; মূল ধারণার জন্য তাঁর প্রথম প্রবন্ধই যথেষ্ট।
পরীক্ষা ও ইন্টারভিউতে:
- Dependency rule কী? — Source code-এর নির্ভরতা শুধু ভেতরের (বেশি নীতিনির্ভর) বৃত্তের দিকে; ভেতরের কোড বাইরের কিছুর নাম জানে না।
- Driving আর driven adapter-এর পার্থক্য? — Driving adapter core-কে ডাকে (web controller, test); driven adapter core-এর ঘোষণা করা port বাস্তবায়ন করে আর core তাকে ডাকে (database, payment).
- Entity আর value object-এর পার্থক্য? — Entity পরিচয় দিয়ে চেনা আর বদলায়; value object মান দিয়ে চেনা আর সাধারণত অপরিবর্তনীয়।
- Aggregate কেন? — একটি invariant রক্ষার একক; বাইরে থেকে শুধু root দিয়ে বদল, আর সাধারণত এক transaction-এ একটি aggregate.
- Hexagonal আর Clean কি আলাদা জিনিস? — মূল ধারণা একই (নির্ভরতা কেন্দ্রের দিকে); hexagonal সীমানা আর port-এ জোর দেয়, Clean ভেতরের স্তর আর একটি স্পষ্ট নিয়মে।
৯ · AI-এর যুগে architecture আর DDD
AI-কে “এই project Clean Architecture-এ সাজিয়ে দাও” বললে সে প্রায়ই চমৎকার দেখতে একটি folder-গাছ দেয় — আর ভুলগুলো থাকে ভেতরে। কয়েকটি সাধারণ ভুল আর ধরার উপায়:
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
Folder-এর নাম domain/, application/, infrastructure/ — কিন্তু domain-এর file-এ ORM-এর decorator | Import-এর তালিকা দেখুন, বা একটি import-নিয়মের checker চালান; নাম নয়, নির্ভরতাই architecture |
| Port-এর interface adapter-এর folder-এ রাখা (মালিকানা উল্টো) | জিজ্ঞেস করুন: interface-টি কার ভাষায় লেখা, আর কে কাকে import করে? |
| চার field-এর CRUD-এ প্রতিটি entity-র জন্য repository, use case, DTO, mapper, presenter | প্রতিটি স্তরের জন্য জিজ্ঞেস করুন: এখানে কোন নিয়ম রক্ষা হচ্ছে? দ্বিতীয় adapter কোথায়? |
| Setter-ভরা “entity” আর সব নিয়ম service-এ — anemic model, নাম শুধু DDD-র | Invariant খুঁজুন: object-কে কি বাইরে থেকে অবৈধ অবস্থায় নেওয়া যায়? একটি test লিখে চেষ্টা করুন |
| Mock-ভরা test, যা শুধু যাচাই করে “repository.save একবার ডাকা হয়েছে” | Fake adapter দিয়ে ফল যাচাই করুন; নিয়মে ইচ্ছা করে ভুল ঢুকিয়ে দেখুন test ফেল করে কি না (mutation testing, পর্ব ৫০) |
| ভুল ইতিহাস বা উদ্ধৃতি: “Evans hexagonal আবিষ্কার করেন”, বানানো বছর বা বইয়ের অধ্যায় | মূল উৎস (Cockburn-এর প্রবন্ধ, Martin-এর ২০১২-র লেখা, Evans-এর বই) মিলিয়ে দেখুন |
AI-কে নকশার সমালোচক হিসেবে ব্যবহার করুন — লেখক হিসেবে নয়। কোড দেওয়ার আগে গোপন key, connection string আর গ্রাহকের তথ্য মুছে নিন:
AI-এর প্রস্তাব হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Cockburn ছয় কোণা আকার বেছে নিলেন কেন? আর “ভেতর-বাইরে” ভাবনা “ওপর-নিচ” ভাবনার চেয়ে কোথায় আলাদা ফল দেয়?
Cockburn নিজেই বলেছেন, ছয় সংখ্যাটির কোনো বিশেষ অর্থ নেই। তিনি চেয়েছিলেন এমন একটি ছবি, যাতে চারপাশে কয়েকটি port আঁকার জায়গা থাকে, আর যা দেখতে প্রচলিত layer-এর স্তূপের মতো নয়। আয়তক্ষেত্রের স্তূপ চোখে একটি ক্রম চাপিয়ে দেয় — UI ওপরে, database নিচে, আর “নিচের” জিনিস যেন ভিত্তি। এই ভিত্তির ধারণাটিই সমস্যা: database-কে ভিত্তি ভাবলে নকশা শুরু হয় table থেকে, আর ব্যবসার নিয়ম সেই table-এর ওপর দাঁড়িয়ে থাকে।
তাঁর মূল পর্যবেক্ষণ ছিল প্রায় সব প্রতিষ্ঠানের একটি পুনরাবৃত্ত কষ্ট: business logic UI-র কোডে মিশে যাওয়া। একটি screen-এর button handler-এর ভেতরেই দাম হিসাব, ছাড়ের নিয়ম আর database-এ লেখা। তখন একই নিয়ম একটি batch job থেকে চালাতে হলে কোড কপি করতে হয়, আর automated test লিখতে হলে UI চালাতে হয়। Hexagon-এর ছবি বলে: UI আর database দুটিই “বাইরে”, দুটিই একটি port-এর পেছনে বদলানো যায় এমন adapter. এই প্রতিসাম্য থেকেই আসে তাঁর বিখ্যাত দাবি — application-কে মানুষ, program, test আর script সমানভাবে চালাতে পারবে।
ব্যবহারিক ফলও আলাদা। “ওপর-নিচ” ভাবনায় দল জিজ্ঞেস করে, “business layer data layer-এর কোন method ডাকবে?” — আর data layer-এর interface ঠিক হয় database-এর সুবিধামতো। “ভেতর-বাইরে” ভাবনায় প্রশ্ন হয়, “core-এর কী দরকার?” — আর port-এর interface ঠিক হয় core-এর ভাষায় (save(order), charge(customer, amount)). Database তখন সেই চাহিদা পূরণের একটি উপায় মাত্র; memory-র dictionary আরেকটি উপায়। এজন্য hexagonal নকশায় test adapter কোনো বাড়তি কৌশল নয়, বরং নকশার স্বাভাবিক ফল।
শেষে একটি সতর্কতা: আকার নিয়ে বেশি ভাবা নিজেই একটি ফাঁদ। কোনো দলের ছবিতে ছয়টি port না থাকলে বা বৃত্ত আঁকলে কিছু ভুল হয় না। মাপকাঠি একটিই — import-এর তীর কোন দিকে।
মূল উপলব্ধি: Hexagon একটি ভাবনার সরঞ্জাম; এর আসল শিক্ষা UI আর database-কে সমান মর্যাদার “বাইরের জগৎ” হিসেবে দেখা, আর port-কে core-এর চাহিদার ভাষায় লেখা।
প্র ০২Clean Architecture কি আসলে শুধু DIP, পুরো system-এর মাপে? তাহলে এত সমালোচনা কেন?
অনেকটা তাই। পর্ব ২৮-এর DIP বলেছিল: policy detail-এর ওপর নির্ভর করবে না, দুটিই abstraction-এর ওপর নির্ভর করবে, আর abstraction-এর মালিক policy. Clean Architecture এই কথাকে বৃত্তে সাজিয়ে একটি পুনরাবৃত্ত নিয়ম বানায়: প্রতিটি সীমানায় ভেতরের বৃত্ত abstraction ঘোষণা করে, বাইরের বৃত্ত তা বাস্তবায়ন করে। Martin-এর অবদান মূলত সংশ্লেষণে — তিনি ২০১২ সালের লেখায় স্পষ্টই বলেছেন hexagonal, onion, BCE, DCI-এর মতো ধারণাগুলো একই দিকে ইঙ্গিত করে, আর তিনি সেগুলোকে একটি নামে, একটি নিয়মে বাঁধছেন। বইয়ে এর সাথে আসে component-এর নীতি আর “screaming architecture”-র ধারণা — project-এর folder দেখেই যেন বোঝা যায় এটি কী নিয়ে (মুদি-বাজার, হাসপাতাল), কোন framework দিয়ে তা নয়।
সমালোচনা আসে কয়েক দিক থেকে। প্রথমত, আক্ষরিক প্রয়োগ: চারটি বৃত্তকে চারটি বাধ্যতামূলক project বা package বানিয়ে, প্রতিটি use case-এর জন্য input port, output port, interactor, presenter, request model, response model — একটি সাধারণ “সব order দেখাও” কাজেও। Martin নিজে বলেছেন বৃত্তের সংখ্যা নমুনা মাত্র, কিন্তু বহু tutorial আর template ছবিটিকে folder-কাঠামো হিসেবে নকল করে। দ্বিতীয়ত, framework-এর সুবিধা হারানো: Rails বা Django-র মতো framework-এর শক্তি তার database-ঘনিষ্ঠ model-এ; সেগুলো পুরোপুরি বাইরের বৃত্তে ঠেলে দিলে framework-এর অনেক সুবিধা আর নিজের হাতে লিখতে হয়। তৃতীয়ত, প্রমাণের প্রশ্ন: “database পরে বদলানো যাবে” — বাস্তবে অনেক system-এর database জীবনে একবারও বদলায় না, তাই সেই নমনীয়তার দাম সবসময় উসুল হয় না।
রক্ষাকারীদের পাল্টা যুক্তি: আসল লাভ database বদলানো নয়, বরং test — ব্যবসার নিয়ম database ছাড়া মিলিসেকেন্ডে test করা যায়। আর framework-এর বড় version বদল (যা বাস্তবে ঘটে) তখন বাইরের বৃত্তেই সীমাবদ্ধ থাকে। দুই পক্ষের মাঝখানে একটি পরিণত অবস্থান দাঁড়িয়েছে: নিয়মটি (নির্ভরতা ভেতরের দিকে) রাখুন, কিন্তু স্তরের সংখ্যা আর DTO-র পরিমাণ ঠিক করুন domain-এর জটিলতা দেখে।
মূল উপলব্ধি: Clean Architecture হলো DIP-কে system-এর মাপে বারবার প্রয়োগ; সমালোচনা সাধারণত নিয়মটির বিরুদ্ধে নয়, ছবিটিকে আক্ষরিক folder-কাঠামো বানানোর বিরুদ্ধে।
প্র ০৩Active Record ধাঁচের framework (যেখানে model class নিজেই database-এ নিজেকে save করে) কি Clean Architecture-এর বিরোধী? তবু এগুলো এত সফল কেন?
Fowler-এর Patterns of Enterprise Application Architecture (২০০২) বইয়ে দুটি pattern পাশাপাশি আছে। Active Record: একটি object একটি table-এর একটি row, আর সেই object-এর ভেতরেই save(), find() — data, ব্যবসার আচরণ আর database-এর কাজ এক class-এ। Data Mapper: domain object database সম্পর্কে কিছুই জানে না; আলাদা একটি mapper দুইয়ের মাঝে অনুবাদ করে। প্রথমটি সরল আর দ্রুত; দ্বিতীয়টি domain-কে database থেকে আলাদা রাখে, কিন্তু বেশি কোড লাগে। Ruby on Rails তার ORM-এর নামই রেখেছে ActiveRecord, আর Django-র model-ও মোটামুটি এই ধাঁচের।
Dependency rule-এর আক্ষরিক বিচারে Active Record model একটি নিয়মভঙ্গ: entity (ভেতরের বৃত্ত) সরাসরি ORM framework-এর base class থেকে আসে, অর্থাৎ ভেতর বাইরের দিকে নির্ভর করে। ফলে model-এর নিয়ম test করতে প্রায়ই test database লাগে, আর framework-এর বড় version বদলে model-এর কোডও ছুঁতে হতে পারে।
তবু এগুলো সফল, কারণ বিপুল সংখ্যক web app-এর আসল আকার CRUD-এর কাছাকাছি: form, validation, table, কয়েকটি নিয়ম। সেখানে Active Record প্রায় শূন্য অনুষ্ঠানে কাজ করে — একটি class লিখলেই table, form আর admin screen. শুরুর গতি বিশাল, আর অনেক app কখনো এমন জটিলতায় পৌঁছায় না যেখানে বিচ্ছিন্নতার দাম উসুল হয়। এটি “ভুল নকশা” নয়, বরং একটি ভিন্ন trade-off: framework-এর সাথে গভীর বন্ধন মেনে নিয়ে গতি কেনা।
সমস্যা দেখা দেয় যখন app বড় হয় আর নিয়ম জটিল হয়: “fat model” — হাজার লাইনের model class, যার ভেতরে ব্যবসার নিয়ম, callback আর query মিশে আছে। তখন দলগুলো সাধারণত ধাপে ধাপে কিছু জটিল নিয়ম সাধারণ object বা service-এ সরায়, model-কে persistence-এর জন্য রেখে — অর্থাৎ যেখানে দরকার সেখানে hexagonal ধারণা আনে, সব জায়গায় নয়। ORM আর migration নিয়ে বিস্তারিত দেখুন DBMS কোর্সের ORM পর্বে।
মূল উপলব্ধি: Active Record dependency rule ভাঙে, কিন্তু CRUD-প্রধান app-এ এটি একটি সচেতন ও প্রায়ই সঠিক trade-off; জটিলতা বাড়লে জটিল অংশটুকু core-এ সরানোই বাস্তব পথ।
প্র ০৪DDD-তে কেন বলা হয় “এক transaction-এ একটি aggregate”? আর aggregate-এর মাপ ভুল হলে কী হয়?
Aggregate হলো consistency-র সীমানা: এর ভেতরের invariant প্রতিটি transaction-এর শেষে সত্য থাকতেই হবে। Order আর তার line-গুলো একটি aggregate, কারণ “মোট ≤ credit limit” যাচাই করতে সব line একসাথে লাগে। এর বাইরের নিয়ম — যেমন “গুদামের মজুত শূন্যের নিচে যাবে না” — আরেকটি aggregate-এর (Stock) দায়। Vernon-এর Implementing Domain-Driven Design (২০১৩)-এ এই চিন্তা কয়েকটি নিয়মে বলা হয়েছে: aggregate ছোট রাখুন, অন্য aggregate-কে id দিয়ে চিনুন, এক transaction-এ একটিই aggregate বদলান, আর aggregate-এর মধ্যে consistency আনুন পরে, domain event দিয়ে (eventual consistency).
কেন? প্রথম কারণ concurrency. দুই ক্রেতা একই সময়ে order দিলে database-এ lock বা version-চেক লাগে (DBMS কোর্সের transaction পর্ব)। Aggregate বড় হলে — ধরুন পুরো “Shop” একটি aggregate, যার ভেতরে সব order আর সব stock — তাহলে প্রতিটি order পুরো দোকান lock করে, আর ঈদের আগে বাসের টিকিটের ভিড়ের মতো সবাই এক লাইনে দাঁড়িয়ে থাকে। দ্বিতীয় কারণ load: বড় aggregate পড়তে প্রতিবার হাজারটি line memory-তে আনতে হয়। তৃতীয় কারণ সীমানার স্পষ্টতা: একটি transaction-এ কয়েকটি aggregate বদলালে কোন নিয়ম কার দায়, তা ঝাপসা হয়।
উল্টো ভুলও আছে: aggregate খুব ছোট করলে — যেমন order line আলাদা aggregate — তখন “মোট ≤ limit” কেউ একা রক্ষা করতে পারে না, কারণ কোনো একটি object সব line দেখে না। তখন নিয়মটি service-এ চলে যায়, আর anemic model-এর ঝুঁকি ফিরে আসে।
একটি ব্যবহারিক প্রশ্ন সাহায্য করে: “এই দুটি জিনিস কি সত্যিই একই মুহূর্তে সুসংগত থাকতে হবে, নাকি কয়েক সেকেন্ড পরে হলেও চলে?” Order দেওয়ার সাথে সাথে stock কমাতেই হবে, নাকি order গৃহীত হলে একটি OrderPlaced event পেয়ে stock পরে কমলেও ব্যবসা মেনে নেয়? প্রায়ই domain expert-রা দ্বিতীয়টি মেনে নেন — আর সেটি জানতে হলে তাঁদের সাথে কথা বলতে হয়, যা DDD-র মূল কথা। Service-এর মাপে এই সীমানা কীভাবে microservice-এর সীমানা হয়, তা দেখুন System Design-এর microservices পর্বে।
মূল উপলব্ধি: Aggregate-এর মাপ ঠিক হয় invariant দিয়ে — যতটুকু একসাথে সত্য রাখতে হবে, ততটুকুই; বাকি সব id আর event দিয়ে যুক্ত।
প্র ০৫Fowler anemic domain model-কে anti-pattern বলেছেন, অথচ functional programming-এ data আর function আলাদা রাখাই রীতি। দুটি অবস্থান কি পরস্পরবিরোধী?
পুরোপুরি নয়, যদিও প্রথম দেখায় তাই মনে হয়। Fowler-এর ২০০৩ সালের আপত্তির কেন্দ্রে ছিল একটি নির্দিষ্ট পরিস্থিতি: দল object-oriented domain model-এর সব খরচ দিচ্ছে — entity class, ORM mapping, স্তর — কিন্তু object-গুলো শুধু getter-setter-এর ব্যাগ, আর সব নিয়ম “service”-এ procedural ধাঁচে লেখা। অর্থাৎ নাম object-oriented, কাজ procedural, আর দুইয়ের সুবিধা কোনোটিই পুরো পাওয়া যায় না। সবচেয়ে বড় ক্ষতি: invariant রক্ষার দায় কারও নয়; যেকোনো কোড setter ডেকে object-কে অবৈধ অবস্থায় নিতে পারে, যেমন এই পর্বের JavaScript উদাহরণে মোট ৩৫০০ হয়ে গেল।
Functional ধারায় data আর function আলাদা, কিন্তু সেখানে দুটি জিনিস পরিস্থিতি বদলে দেয়। প্রথমত, data অপরিবর্তনীয়: কেউ “পরে” একটি field বদলাতে পারে না, নতুন মান বানাতে হয়। দ্বিতীয়ত, বৈধ মান বানানোর পথ নিয়ন্ত্রিত: একটি “smart constructor” function, যা অবৈধ input পেলে ভুল ফেরত দেয় — ফলে type-টির কোনো অবৈধ মান program-এ থাকতেই পারে না। এই কৌশলকে কেউ কেউ “অবৈধ অবস্থা প্রকাশযোগ্যই না রাখা” বলেন। Invariant তখন object-এর method দিয়ে নয়, type আর constructor দিয়ে রক্ষা পায়। সমস্যার সমাধান একই — শুধু অস্ত্র ভিন্ন।
Architecture-এর মাপেও একটি মিল আছে। Gary Bernhardt-এর ২০১২ সালের দিকের আলোচনা থেকে জনপ্রিয় হওয়া “functional core, imperative shell” ধারণা বলে: কেন্দ্রে বিশুদ্ধ function (input থেকে output, কোনো I/O নেই), আর বাইরে একটি পাতলা খোলস, যা database, network আর ঘড়ির সাথে কথা বলে। এটি hexagonal-এর একটি functional রূপ — core-এ নিয়ম, বাইরে adapter, আর core test করা সবচেয়ে সহজ।
তাই প্রশ্নটি “data আর আচরণ একসাথে, না আলাদা?” নয়। প্রশ্ন হলো: “নিয়মটি কি এমন জায়গায় আছে, যা এড়িয়ে যাওয়া অসম্ভব?” Rich object-এ private field আর guarded method দিয়ে, functional কোডে immutable data আর smart constructor দিয়ে — দুটিই সেই শর্ত পূরণ করে। খোলা, বদলানো যায় এমন data আর ঐচ্ছিক service — কোনোটিই করে না।
মূল উপলব্ধি: Anemic model-এর আসল দোষ data আর function-এর বিচ্ছেদ নয়, বরং এমন নিয়ম যা এড়িয়ে যাওয়া যায়; immutability আর নিয়ন্ত্রিত constructor সেই দোষ অন্য পথে সারায়।
অনুশীলন
আউটপুট অনুমান, import-এর হিসাব, port আঁকা আর সমালোচনা — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।
-
আউটপুট অনুমান করুন (fake adapter দিয়ে use case): নিচের কোডে
placeএকটি use case,InMemRepoআরFakeSmsদুটি test adapter. দুটিprint-এর আউটপুট লিখুন, আর ব্যাখ্যা করুন কেন repository-তে একটিই order.class FakeSms: def __init__(self): self.sent = [] def send(self, to, text): self.sent.append(to) class InMemRepo(dict): def save(self, o): self[o["id"]] = o def place(repo, sms, oid, phone, amount, limit=1000): # the use case if amount > limit: return "REJECTED" repo.save({"id": oid, "amount": amount}) sms.send(phone, f"order {oid} ok") return "OK" repo, sms = InMemRepo(), FakeSms() print(place(repo, sms, 1, "017", 400), place(repo, sms, 2, "018", 1200), place(repo, sms, 1, "019", 900)) print(len(repo), repo[1]["amount"], sms.sent)আসল আউটপুট (চালিয়ে মেলানো):
OK REJECTED OK 1 900 ['017', '019']- প্রথম ডাক: ৪০০ ≤ ১০০০, তাই order 1 জমা আর 017-এ SMS — “OK”.
- দ্বিতীয় ডাক: ১২০০ > ১০০০, তাই শুরুতেই “REJECTED”; কিছু জমা হয়নি, কোনো SMS যায়নি।
- তৃতীয় ডাক: ৯০০ ≤ ১০০০ — “OK”. কিন্তু id আবার 1!
InMemRepoএকটি dictionary, তাই একই key-তে নতুন order আগেরটিকে মুছে বসে। ফলেlen(repo)১, আরrepo[1]["amount"]৯০০। - SMS: শুধু সফল দুটি ডাকে —
['017', '019'].
এখান থেকে একটি নকশার প্রশ্ন ওঠে: একই id-তে দ্বিতীয় order নীরবে আগেরটি মুছে দেওয়া কি ব্যবসার নিয়ম? প্রায় নিশ্চয়ই নয়। নিয়মটি (“id অনন্য”) কোথায় থাকবে — repository-তে, না use case-এ? একটি ভালো উত্তর: id বানানোর দায় repository-র (
next_id()), আর আসল database adapter-এ unique constraint. Fake adapter-কেও আসলটির মতো আচরণ করতে হবে — নইলে test সবুজ, production লাল। -
হিসাব (dependency rule): একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালে module আর ring:
grade(০),gpa(০),publishResult(১),resultPorts(১),resultController(২),pgResultRepo(২),smsGateway(২),server(৩). Import:gpa → grade;grade → pgResultRepo;publishResult → gpa, resultPorts, smsGateway;resultController → publishResult;pgResultRepo → resultPorts, grade;smsGateway → resultPorts;server → resultController, pgResultRepo. (ক) মোট কয়টি import, কয়টি নিয়মভঙ্গ? (খ) প্রতিটি ভঙ্গ DIP দিয়ে কীভাবে সারাবেন?(ক) গোনা: gpa ১, grade ১, publishResult ৩, resultController ১, pgResultRepo ২, smsGateway ১, server ২ = ১১টি import. প্রতিটির ring পরীক্ষা:
Import Ring রায় gpa → grade ০ → ০ ✓ grade → pgResultRepo ০ → ২ ✗ বাইরের দিকে publishResult → gpa ১ → ০ ✓ publishResult → resultPorts ১ → ১ ✓ publishResult → smsGateway ১ → ২ ✗ বাইরের দিকে resultController → publishResult ২ → ১ ✓ pgResultRepo → resultPorts, grade ২ → ১, ২ → ০ ✓ ✓ smsGateway → resultPorts ২ → ১ ✓ server → resultController, pgResultRepo ৩ → ২, ৩ → ২ ✓ ✓ মোট ২টি নিয়মভঙ্গ, ৯টি অনুমোদিত।
(খ) সমাধান:
grade(entity) database adapter-কে চেনে — সম্ভবত নিজেকে নিজে save করতে। Entity থেকে persistence-এর কোড সরিয়ে দিন; সংরক্ষণ হবে use case থেকেresultPorts-এর একটিResultRepositoryinterface দিয়ে, আরpgResultRepoসেটি বাস্তবায়ন করবে (এটি আগে থেকেইresultPortsimport করে)।publishResult → smsGateway: use case-এর বৃত্তে একটিNotifierport (notify(student_id, text)) ঘোষণা করুন;smsGatewayসেটি বাস্তবায়ন করবে, আরserver-এর composition root আসল gateway বানিয়ে use case-কে দেবে। সারানোর পর ২টি ভাঙা import-এর জায়গায় আসেpublishResult → resultPorts(আগেই ছিল) — মোট import ৯টি (জোড়া দিতেserver → smsGatewayযোগ হলে ১০টি), সব ভেতরের দিকে বা একই ring-এ। -
নকশা — এই feature-এর port আঁকুন: একই কাল্পনিক ফলাফল পোর্টালে নতুন feature: “পরীক্ষা নিয়ন্ত্রক ফলাফল প্রকাশ করলে প্রতিটি শিক্ষার্থীর GPA হিসাব হবে, ফল database-এ জমা হবে, শিক্ষার্থীকে SMS যাবে, আর শিক্ষার্থী পরে website থেকে PDF transcript নামাতে পারবেন। একই প্রকাশের কাজটি রাতের একটি batch script থেকেও চালানো যাবে।” Driving port আর adapter, driven port আর adapter, আর test adapter-এর তালিকা করুন। কোন নিয়ম কোন entity বা value object-এ যাবে, সেটিও বলুন।
Driving দিক (যে core-কে চালায়):
- Driving port
PublishResults.run(exam_id)— adapter: নিয়ন্ত্রকের web screen-এর controller, আর রাতের batch script (দুটিই একই port ডাকে)। - Driving port
GetTranscript.run(student_id)— adapter: শিক্ষার্থীর website-এর controller. - Test adapter: test script দুটি port সরাসরি ডাকে।
Driven দিক (যাকে core চালায়) — সব interface core-এর বৃত্তে:
ResultRepository(marks_for(exam_id),save(result),get(student_id)) — adapter: আসল database-এর SQL repo; test-এ InMemoryResults.Notifier(notify(student_id, text)) — adapter: SMS gateway; test-এ FakeNotifier, যা পাঠানো বার্তা তালিকায় রাখে। নাম “SmsPort” নয়, কারণ core-এর চাহিদা “জানানো”; কাল email হতে পারে।DocumentRenderer(render_transcript(transcript)→ bytes) — adapter: একটি PDF library; test-এ একটি fake, যা সরল লেখা ফেরত দেয়।- ঐচ্ছিক
Clock(today()) — প্রকাশের তারিখ test-এ স্থির রাখতে।
Core-এর ভেতরে:
GradeআরGpavalue object (যেমন GPA ০.০০ থেকে ৪.০০-এর বাইরে বানানোই যাবে না);Resultentity বা aggregate (শিক্ষার্থী আর পরীক্ষার id দিয়ে পরিচিত), যার invariant: প্রকাশিত ফল আর বদলানো যাবে না, শুধু সংশোধনী হিসেবে নতুন version. GPA হিসাবের নিয়ম (credit-ভারিত গড়)Result-এর ভেতরে বা একটি domain service-এ, যদি কয়েকটি entity লাগে। “ফল প্রকাশিত হলো” একটি domain event —ResultsPublished— যা পেয়ে notifier SMS পাঠাতে পারে, যাতে use case SMS-এর ব্যর্থতায় আটকে না যায়।যাচাই: HTTP, SQL, SMS gateway বা PDF library-র কোনো নাম কি core-এর কোনো file-এ আছে? না থাকলে নকশা ঠিক পথে।
- Driving port
-
সমালোচনা — একটি over-engineered CRUD app: একটি কাল্পনিক পাড়ার ক্রিকেট ক্লাবের সদস্য-তালিকা app: field মাত্র চারটি (নাম, ফোন, ঠিকানা, যোগদানের তারিখ), কাজ শুধু যোগ, দেখা, বদলানো আর মোছা; ব্যবহারকারী একজন সম্পাদক, ব্যবসার নিয়ম নেই বললেই চলে। Developer Clean Architecture-এ বানিয়েছেন: চারটি project, প্রতিটি কাজের জন্য input port, output port, interactor, presenter, request model আর response model;
Memberentity,MemberOrmModel, আর দুইয়ের মাঝে mapper; repository interface-এর একটিই বাস্তবায়ন, কোনো test নেই। মোট প্রায় ৪০টি file. এই নকশার সমালোচনা করুন আর একটি বিকল্প প্রস্তাব করুন।- রক্ষা করার মতো নিয়ম নেই: Clean Architecture-এর মূল্য আসে কেন্দ্রের নিয়ম রক্ষা থেকে। এখানে কেন্দ্র খালি — প্রতিটি interactor শুধু request model থেকে entity, entity থেকে ORM model-এ field কপি করে। একই চারটি field অন্তত চারবার লেখা (request, entity, ORM, response). একটি নতুন field (ধরুন রক্তের গ্রুপ) যোগ করতে ৬–৮টি file বদলাতে হবে।
- বিমূর্ততার কোনো দ্বিতীয় ব্যবহার নেই: Repository interface-এর একটিই বাস্তবায়ন আর কোনো test নেই — অর্থাৎ port-এর প্রধান লাভ (test adapter বসানো) একবারও নেওয়া হয়নি। এটি পর্ব ২৬-এর YAGNI-র সরাসরি লঙ্ঘন: প্রমাণহীন ভবিষ্যতের জন্য স্তর।
- Indirection-এর দাম: নতুন একজন স্বেচ্ছাসেবক developer “সদস্য যোগ” কোথায় হয় খুঁজতে চারটি project পেরোবেন। ক্লাবের app-এ এই সময়ই সবচেয়ে দামি।
- যা ভালো: Developer-এর উদ্দেশ্য (framework থেকে আলাদা থাকা) খারাপ নয়, আর ভবিষ্যতে নিয়ম এলে (চাঁদা বকেয়া হলে খেলার তালিকা থেকে বাদ) কাঠামো তৈরি আছে। কিন্তু সেই দিনের জন্য আজ দাম দেওয়ার দরকার নেই।
বিকল্প: একটি project; framework-এর সাধারণ CRUD পথ (Active Record ধাঁচের model, framework-এর form validation, প্রয়োজনে auto-generated admin screen); ফোন নম্বরের format যাচাই model-এর validation-এ। কয়েকটি end-to-end বা integration test (পর্ব ৫১)। মোট হয়তো ৫–৮টি file. যেদিন সত্যিকারের নিয়ম আসবে (চাঁদা, ম্যাচের দল বাছাই), সেদিন শুধু সেই অংশের জন্য একটি use case আর একটি port, test-সহ। সিদ্ধান্তটি একটি ছোট ADR-এ লিখে রাখুন (পর্ব ৩৮): “Clean Architecture নয়, কারণ domain-এ নিয়ম নেই; নিয়ম এলে পুনর্বিবেচনা।”
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি database-কেন্দ্রিক পুরোনো feature-কে কিছু না ভেঙে ধাপে ধাপে port আর adapter-এ নেওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🧪 বর্তমান আচরণ ধরে রাখতে feature-টির চারপাশে characterization test লেখা
- ✂️ Controller-এর ভেতরের নিয়ম একটি আলাদা use case class-এ সরানো
- 🚪 Use case-এর বৃত্তে database-এর কাজের জন্য একটি port interface ঘোষণা করা
- 🔌 পুরোনো SQL কোডকে সেই port বাস্তবায়ন করা একটি adapter-এ সরানো
- 🧷 Composition root-এ আসল adapter জোড়া দিয়ে পুরোনো test আবার চালানো
- ⚡ একটি in-memory adapter বানিয়ে database ছাড়া দ্রুত নতুন test যোগ করা
Hexagonal architecture-এ একটি অনলাইন দোকানের core-এ PaymentPort নামের interface আছে, যা একটি WalletApi class বাস্তবায়ন করে আর use case charge() ডাকে। WalletApi কোন ধরনের?
- Driving (primary) adapter
- Driven (secondary) adapter
- Driving port
- Entity
Clean Architecture-এর বৃত্ত ভেতর থেকে বাইরে: entities (০), use cases (১), interface adapters (২), frameworks (৩)। নিচের কোন import dependency rule ভাঙে?
- OrderController (২) → PlaceOrder use case (১)
- SqlOrderRepo (২) → OrderRepository port (১)
- Order entity (০) → Money value object (০)
- PlaceOrder use case (১) → SmsGateway adapter (২)
Python-এ Money একটি @dataclass(frozen=True), যার দুটি field: amount: int আর currency: str. Money(500, "BDT") == Money(500, "BDT")-এর মান কী, আর কেন?
True— dataclass field-এর মান তুলনা করেFalse— দুটি আলাদা object- FrozenInstanceError
- TypeError — Money-তে == সংজ্ঞায়িত নেই
একটি Order aggregate-এর invariant: মোট দাম ≤ credit limit (২০০০ টাকা)। বর্তমান মোট ১৩০০। একটি command ৯০০ টাকার নতুন line যোগ করতে চায়। ভালো নকশার aggregate কী করবে?
- Line যোগ করবে, তারপর মোট ২২০০ দেখে line-টি মুছে দেবে
- Line যোগ করবে; limit যাচাই করা OrderService-এর কাজ
- বদলের আগেই ২২০০ হিসাব করে command প্রত্যাখ্যান করবে, অবস্থা অপরিবর্তিত রেখে
- Credit limit নিজে থেকে ২২০০ করে দেবে
একটি অনলাইন বাজারে বিক্রয় বিভাগের কাছে “product” মানে ছবি, দাম আর বিবরণ; গুদামের কাছে মানে তাক নম্বর, ওজন আর মজুত। DDD অনুযায়ী সবচেয়ে উপযুক্ত নকশা কোনটি?
- সব field-সহ একটি সাধারণ Product class, যা দুই বিভাগই ব্যবহার করে
- দুটি bounded context — Sales-এ একটি Product model, Warehouse-এ আরেকটি — একই id দিয়ে যুক্ত
- একটি Product base class আর তার দুটি subclass
- একটি Product class, কিন্তু দুটি আলাদা database table
কোন পরিস্থিতিতে পূর্ণ Clean Architecture (চার বৃত্ত, প্রতিটি কাজে port আর DTO) সবচেয়ে কম পোষায়?
- একটি ঋণ-অনুমোদন system, যার নিয়ম জটিল আর প্রতি ত্রৈমাসিকে বদলায়
- একটি system, যার একই কাজ web, mobile API আর রাতের batch — তিন পথেই চলে
- একটি system, যা ১৫ বছর চলবে আর মাঝে framework বদলানোর পরিকল্পনা আছে
- একটি ছোট ক্লাবের সদস্য-তালিকা, যেখানে শুধু যোগ, দেখা, বদল আর মোছা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- সমস্যা: database-কেন্দ্রিক layering-এ business-এর নিয়ম framework আর database-এর ওপর নির্ভর করে — test-এ database লাগে, framework বদলালে নিয়ম ভাঙে। প্রশ্ন “কে কাকে ডাকে” নয়, “কে কাকে চেনে”।
- Hexagonal (Cockburn): core-এর নিজের ভাষায় port; driving adapter core-কে ডাকে, driven adapter core-এর port বাস্তবায়ন করে। দুই দিকেই test adapter — core না বদলে।
- Clean (Martin) আর onion (Palermo): entities, use cases, interface adapters, frameworks & drivers; source নির্ভরতা শুধু ভেতরের দিকে। Boundary পার হয় DIP-এর interface আর সরল DTO দিয়ে; composition root সবচেয়ে বাইরে। তিনটি নাম, একটি মূল ধারণা।
- DDD (Evans, ২০০৩): ubiquitous language; entity পরিচয়ে, value object মানে; aggregate root invariant রক্ষা করে আর বদলের আগেই অবৈধ command প্রত্যাখ্যান করে; aggregate-প্রতি repository; domain service, domain event; bounded context আর context map.
- দাম আর বিচার: indirection, বেশি file, mapping. জটিল আর দীর্ঘজীবী domain-এ পোষায়; নিয়মহীন CRUD-এ over-engineering. ঝুঁকির জায়গায় একটি port দিয়ে শুরু করুন। নিয়ম এমন জায়গায় রাখুন, যা এড়ানো যায় না — নইলে anemic model.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪১ · Clean code — নাম, function, comment, formatting আর error handlingপরবর্তী পর্বলাইন আর function-এর মাপে পরিচ্ছন্নতা।
- পর্ব ৩৯ · MVC, MVP আর MVVM — UI-র কোড সাজানোআগের পর্বDriving adapter-এর ভেতরটা — UI-র কোড কীভাবে ভাগ হয়।
- পর্ব ৩৮ · Software architecture — layered, client-server, pipe-filter, event-driven আর ADRভিত্তিLayered style আর architecture-এর সিদ্ধান্ত লিখে রাখা।
- পর্ব ২৮ · SOLID ২ — Liskov substitution, interface segregation আর dependency inversionভিত্তিDIP — এই পর্বের সব architecture-এর মূল নীতি।
- পর্ব ৩৭ · Dependency injection আর composition — inversion of control আর inheritance-এর বদলে compositionএকই ধারাComposition root-এ adapter জোড়া দেওয়া।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- System Design · Microservicesপাশের ট্র্যাকBounded context আর aggregate দিয়ে service-এর সীমানা।
- DBMS ও SQL · ORM আর migrationপাশের ট্র্যাকActive Record আর data mapper-এর বাস্তব রূপ।