আমার ক্যারিয়ারের একটা লম্বা অংশ কেটেছে একটা পণ্যের চাকচিক্যহীন অর্ধেক তৈরিতে: অভ্যন্তরীণ অ্যাডমিন প্ল্যাটফর্ম। যে অংশ গ্রাহকরা কখনো দেখে না, যেখানে operations, finance ও support আসলে ব্যবসা চালায়। সময়ের সঙ্গে এটা আশিটা মডিউল ছাড়িয়ে গেল — Product, Category, Order, Inventory, Vendor, Warehouse, Ledger, HR ও লম্বা লেজের আরও অনেক।
এখানে যে জিনিসটা কেউ সতর্ক করে না। প্রথম পাঁচটা মডিউল সত্যিই আলাদা ও সেগুলো বানানো মজার। ছয় থেকে আশি নম্বর মডিউল, প্রায় ব্যতিক্রম ছাড়াই, আলাদা পোশাক পরা একই আকার: এমন একটা তালিকা যা filter ও paginate করা যায়, একটা detail view, একটা create/edit form, কিছু permission, কিছু validation। আর ওই একঘেয়েমিতেই বিপদ থাকে।
যে পচন মডিউল ৩০ পর্যন্ত টের পান না
দ্বিতীয় মডিউল বানানোর সুস্পষ্ট উপায় হলো প্রথমটা কপি করা আর field বদলানো। এটা কাজ করে। দ্রুত। উৎপাদনশীল মনে হয়। তাই তৃতীয়টার জন্য আবার করেন ও চতুর্থটার জন্য ও তাকিয়ে দেখার আগেই আপনার হাতে ত্রিশটা ফাইল যা ৮০% অভিন্ন আর ২০% সূক্ষ্মভাবে ভিন্ন।
খরচটা duplication নিজে নয় — ডিস্ক সস্তা। খরচটা হলো একটা বাগের এখন ত্রিশটা ঘর। কেউ Orders তালিকায় pagination ঠিক করে। কারও মনে নেই Products তালিকার নিজের কপিতে একই off-by-one আছে। একটা permission চেক এক জায়গায় কড়া করা হয় আর নীরবে বারো জায়গায় ভুলে যাওয়া হয়। প্রতিটি উন্নতি হাতে প্রয়োগ করতে হয়, সর্বত্র ও "সর্বত্র" এমন একটা সংখ্যা যা আপনি আর মাথায় রাখতে পারেন না।
ওই মাপে copy-paste আর শর্টকাট নয়। এটা একটা ধীরগতির outage।
তিনটা স্ক্রিনে copy-paste ঠিক আছে। আশিটায় এটা একটা ধীরগতির outage: একটা বাগের এখন ত্রিশটা ঘর আর "সর্বত্র" এমন সংখ্যা যা মাথায় রাখতে পারেন না।
মডিউলকে code নয়, data হিসেবে দেখা
যে পদক্ষেপ প্ল্যাটফর্ম বাঁচিয়েছে তা হলো প্রতিটি মডিউলকে একটা component হিসেবে ভাবা বন্ধ করে একটা বর্ণনা হিসেবে ভাবতে শুরু করা। একটা মডিউল code নয় — এটা একটা ছোট config: কী কী field আছে, তালিকায় কোন column দেখায়, কীভাবে validate করে, কে ছুঁতে পারে। মানসম্মত list/detail/edit যন্ত্রপাতি একবার লেখা হয় আর সেই বর্ণনা পড়ে।
export const orderModule = defineModule({
name: "order",
label: "Orders",
fields: {
code: { type: "string", readonly: true },
customer: { type: "reference", to: "user" },
total: { type: "currency" },
status: {
type: "enum",
options: ["pending", "packed", "shipped", "delivered"],
},
},
list: {
columns: ["code", "customer", "total", "status"],
filters: ["status", "customer"],
},
permissions: { view: "order.read", edit: "order.write" },
});একটা মডিউল যোগ করা হয়ে ওঠে বর্ণনার কাজ, নির্মাণের নয়। কোনো নতুন list component নেই, কোনো নতুন form wiring নেই, pagination বাগের কোনো নতুন কপি নেই। generic renderer আগেই জানে কীভাবে ওই object-কে একটা কার্যকর স্ক্রিনে পরিণত করতে হয়।
লাভটা চক্রবৃদ্ধি হারে বাড়ে। এখন যখন pagination ঠিক করেন, এক জায়গায় ঠিক করেন আর আশিটা মডিউলই সেটা পায়। যখন একটা global ফিচার যোগ করেন — column visibility, CSV export, edit-এ audit trail — এঞ্জিনে একবার যোগ করেন ও প্রতিটি মডিউল জ্বলে ওঠে। এটাই সেই লিভারেজ যা আশিটা মডিউলকে একটা বড় দলের বদলে একটা ছোট দলের রক্ষণাবেক্ষণযোগ্য করে তোলে।
যে অংশ সবাই এড়িয়ে যায়: escape hatch
এখানেই বেশিরভাগ "শুধু config-চালিত করো" পরামর্শ চুপচাপ ভেঙে পড়ে। এটা সত্য ঠিক সেই মডিউল পর্যন্ত যেটা খাপ খায় না — আর আপনি সবসময় সেটায় পৌঁছান।
একটা মডিউলের একটা custom action button দরকার যা একটা background job শুরু করে। আরেকটার একটা field আছে যার option অন্য একটা field-এর মানের উপর নির্ভর করে। তৃতীয়টার একটা সম্পূর্ণ নিজস্ব detail view দরকার কারণ এটা আসলে একটা dashboard, একটা record নয়। আপনার abstraction-এর যদি এসবের উত্তর না থাকে, দুটি খারাপ জিনিসের একটা ঘটে: হয় আপনি config-কে এমন কিছুতে মোচড়ান যা অপাঠ্য যাতে ব্যতিক্রমটা জোর করে ঢোকানো যায়, নয়তো বেরিয়ে এসে পুরো মডিউল হাতে লেখেন — আর এখন আপনি সেই copy-paste দুনিয়ায় ফিরে গেছেন যেটা থেকে পালাচ্ছিলেন।
abstraction তার escape hatch যতটা ভালো ততটাই ভালো। আমি যে নিয়মে পৌঁছেছি: config সাধারণ ৯০% সামলায় ও প্রতিটি extension point একটা নামযুক্ত override, একটা fork নয়।
export const shipmentModule = defineModule({
name: "shipment",
// ...standard field + list config...
overrides: {
// Replace only the detail view; list and edit stay generic.
DetailView: ShipmentTrackingBoard,
// Add a custom action without touching the toolbar internals.
rowActions: [{ label: "Reprint label", run: reprintLabel }],
},
});এই পার্থক্য দেখতে যতটা মনে হয় তার চেয়ে বেশি গুরুত্বপূর্ণ। একটা override বলে "এই মডিউল ৯০% মানসম্মত আর ১০% বিশেষ ও এই যে ঠিক কোন ১০%।" একটা fork বলে "এই মডিউল এখন তার নিজের মহাবিশ্ব।" প্রথমটা শেয়ারড ফিক্সগুলো ভেতরে আসতে দেয়। দ্বিতীয়টা মডিউলকে সেগুলো থেকে চিরকালের জন্য কেটে দেয়। config পড়া একজন নতুন লোক এক নজরে দেখতে পারে কোথায় একটা মডিউল নিয়ম বাঁকায়, কারণ বাঁকানোটা স্পষ্ট আর স্থানীয়, একটা হাতে-লেখা ফাইল জুড়ে ছড়ানো নয়।
কোথায় আমি ইচ্ছে করে abstract করিনি
উল্টো ভুলটা হলো অতি-উৎসাহী একীকরণ ও আমি সেটাও করেছি। দুটি মডিউল অভিন্ন দেখাত, তাই এদের একটা শেয়ারড সংজ্ঞায় merge করলাম একটা flag দিয়ে আলাদা করার জন্য। প্রায় এক মাস চতুর মনে হলো।
তারপর তাদের প্রয়োজন সরে গেল, যেভাবে বাস্তব ব্যবসায়িক প্রয়োজন সবসময় যায়। প্রতিটি নতুন পার্থক্য মানে শেয়ারড কোডের ভেতর আরেকটা branch — if (variant === 'a') জমতে জমতে "শেয়ারড" মডিউল দুটি আলাদার চেয়ে পড়া কঠিন হয়ে গেল। শিক্ষা: আজ যে দুটি জিনিস একই দেখায় সেগুলো অগত্যা একই জিনিস নয়। উপরিভাগের মিল couple করার কারণ নয়। এখন আমি একটা সত্যিকারের প্যাটার্নের তৃতীয় উপস্থিতি পর্যন্ত অপেক্ষা করি এঞ্জিনে টানার আগে ও একটা শেয়ারড abstraction তার দ্বিতীয় if variant গজানোর মুহূর্তেই আবার আলাদা করতে দ্রুত।
সৎ লেনদেন
Config-চালিত স্ক্রিন ফ্রি নয়। একজন একদম নতুন ইঞ্জিনিয়ার একটা হাতে-লেখা মডিউল উপর থেকে নিচ পড়ে এক বিকেলে বুঝতে পারে। একটা config-চালিত মডিউল বুঝতে তাকে আগে এঞ্জিন শিখতে হয় — indirection বাস্তব ও debug করা মানে মডিউলের নিজের code-এর বদলে একটা generic renderer-এর মধ্য দিয়ে হাঁটা। আপনি যেকোনো একটা স্ক্রিনের তাৎক্ষণিক পঠনযোগ্যতা বিনিময় করছেন সবগুলোর একসঙ্গে রক্ষণাবেক্ষণযোগ্যতার জন্য।
তিনটা স্ক্রিনে সেই লেনদেন খারাপ — শুধু তিনটা স্ক্রিন লিখুন। আশিটায় এটাই একমাত্র জিনিস যা প্ল্যাটফর্মকে বাঁচিয়ে রাখে। পুরো দক্ষতা হলো কপি জমার আগে খেয়াল করা যে আপনি সেই রেখার কোন পাশে আছেন, পরে নয়।
আপনি যদি আপনার পঞ্চম প্রায়-অভিন্ন CRUD স্ক্রিনের দিকে তাকিয়ে থাকেন আর চতুর্থটা কপি করার তাগিদ অনুভব করেন, সেটাই সংকেত। একটা framework বানানোর নয় — অনুগ্রহ করে পাঁচ নম্বর স্ক্রিনে framework বানাবেন না — কিন্তু আপনার স্ক্রিন নির্মাণের বদলে বর্ণনা করতে শুরু করার ও যেদিন এদের একটা মানতে অস্বীকার করবে তার জন্য নিজেকে পরিচ্ছন্ন, নামযুক্ত প্রস্থান রেখে দেওয়ার।