Speculative Generality: যে সুইমিং পুলের জন্য পাইপ বসালে, পুলটাই হলো না
বাড়ি বানানোর গল্প দিয়ে Speculative Generality smell বোঝো। YAGNI কী, ভবিষ্যতের অনুমানে কোড লেখা কেন ক্ষতিকর, আর অব্যবহৃত abstraction কীভাবে সরাতে হয় — সব পরিষ্কার হয়ে যাবে।
কল্পিত সুইমিং পুলের জন্য পাইপ বসানো বাড়ি
ধরো জামাল সাহেব ঢাকায় একটা বাড়ি বানাচ্ছেন। সারাজীবনের সঞ্চয় — সরকারি চাকরি, মিতব্যয়ী মানুষ, দুই ছেলে। architect নাসরিন টেবিলে নকশা মেলে ধরে জিজ্ঞেস করলেন: "দুই তলা, তাই না?"
"চার তলার ভিত দাও," জামাল সাহেব গর্বের সাথে বললেন। "একদিন আমার ছেলেরা পরিবার নিয়ে উপরে থাকবে। চার তলার ভিত দাও। ছাদে পাইপ বসাও — একদিন হয়তো সুইমিং পুল বানাবো, বাচ্চারা মজা পাবে। সব দেওয়ালে বাড়তি electrical conduit দাও — একদিন home automation করবো। আর রান্নাঘরের দেওয়াল সরানোর ব্যবস্থা রাখো — একদিন রান্নাঘর বড় করবো।"
নাসরিন থামলেন। "স্যার, প্রতিটা 'একদিন'-এর জন্য আজকেই টাকা লাগবে। আমরা কি—"
"ভবিষ্যতের জন্য বানাও!" জামাল সাহেব বললেন। "এটাই বুদ্ধিমানের কাজ।"
তাই নাসরিন মেনে নিলেন। ভিত হলো চার তলার: বাড়তি রড, বাড়তি সিমেন্ট, বাড়তি কয়েক মাসের কাজ। কল্পিত পুলের পাইপ গেল আসল শোবার ঘরের দেওয়ালের ভেতর দিয়ে। কল্পিত automation-এর conduit বিছানো হলো প্রতিটা মেঝের টাইলের নিচে। সরানো যাবে এমন রান্নাঘরের দেওয়ালের জন্য অন্য জেলা থেকে আনতে হলো বিশেষ স্টিলের বিম।
বিল এলো: একটা সাধারণ দুই তলা বাড়ির প্রায় দ্বিগুণ খরচ। পরিবারের সঞ্চয় শেষ, তাই বছরের পর বছর ভেতরের কাজ অর্ধেক হয়ে রইল। ফাতেমা বেগম খালি প্লাস্টারের রান্নাঘরে রান্না করলেন — দেওয়াল যেটা অন্তত তাত্ত্বিকভাবে সরানো যায়।
আর সেই ভবিষ্যৎ? দুই ছেলে চাকরি পেল চট্টগ্রাম আর দুবাইতে। তৃতীয় তলা আর হলো না। পুল? বিশ বছর পরে ছাদের পাইপ মরিচা ধরে বন্ধ — আর ফাতেমা বেগম যখন ছাদে বাগান করতে চাইলেন, প্লাম্বার মাথা নাড়লেন: "আগে এই পুলের পাইপ সব তুলতে হবে ম্যাডাম। ঠিক ভুল জায়গায় আছে।" সরানো যাবে এমন রান্নাঘরের দেওয়ালে ফাটল ধরল, কারণ বিশেষ বিমের বিশেষ রক্ষণাবেক্ষণ দরকার ছিল — যেটা কেউ মনে রাখেনি।
ভবিষ্যৎ নিয়ে প্রতিটা অনুমানের জন্য প্রথম দিনেই বাস্তব টাকা গেছে, তারপর প্রতিদিন সেটা সমস্যা করেছে, আর শেষে যখন আসল ভবিষ্যৎ ভিন্ন আকারে এলো তখন সেটা ভাঙতে হয়েছে।
কোডে এটাই হলো Speculative Generality: অনুমানিত ভবিষ্যতের জন্য বানানো কাঠামো, যার মূল্য দিতে হয় বর্তমানের জটিলতা দিয়ে।
এই smell-টা কী?
Speculative Generality হলো বাড়তি কাঠামো — interface, abstract base class, hook method, parameter, generic type, factory, plugin system — যেটা বর্তমানের কোনো প্রয়োজনে নয়, বরং কেউ অনুমান করা ভবিষ্যতের প্রয়োজনের জন্য বানিয়েছে। অনুমান কমই সেই কল্পিত আকারে সত্যি হয়, কিন্তু কাঠামো থেকে যায় — আর প্রতিটা পাঠক ও পরিবর্তন তার মূল্য দেয়।
এই smell-এর একটা বিখ্যাত counter-principle আছে: YAGNI — "You Aren't Gonna Need It" — Extreme Programming-এর একটা যুদ্ধধ্বনি। Martin Fowler-এর bliki essay Yagni হলো এর সবচেয়ে ভালো ব্যাখ্যা, যেখানে খরচগুলোর সুনির্দিষ্ট নাম দেওয়া হয়েছে:
- বানানোর খরচ (Cost of building) — অনুমানিত feature বানাতে যে সময় গেল, সেটা এখন দরকার এমন feature থেকে নেওয়া হলো।
- দেরির খরচ (Cost of delay) — আসল মূল্যবান feature পরে গেল কারণ অনুমানে সময় নষ্ট হয়েছে।
- বহনের খরচ (Cost of carry) — speculative কাঠামো codebase ভারী করে; প্রতিটা ভবিষ্যৎ পরিবর্তনকে এটা এড়িয়ে যেতে হয়।
- মেরামতের খরচ (Cost of repair) — যখন আসল প্রয়োজন ভিন্ন আকারে আসে (প্রায়ই আসে), আগে ভুল abstraction ভাঙতে হয়।
Fowler-এর সবচেয়ে তীক্ষ্ণ কথা: এমনকি যদি অনুমানিত feature কোনোদিন সত্যিই দরকার হয়, আগেভাগে বানানো সাধারণত তখনো ভুল — পুরো সময়টা তার ভার বহন করতে হয়, আর বানিয়েছিলে সবচেয়ে কম তথ্য দিয়ে।
যেকোনো abstraction-এর জন্য সৎ পরীক্ষা: "আমার কাছে কি দুটো বাস্তব case আছে, নাকি একটা বাস্তব আর একটা কল্পিত?" দুটো বাস্তব case থাকলে abstraction পাওয়ার যোগ্যতা আছে — এরা তার আসল আকার দেখায়। একটা বাস্তব case আর একটা অনুমান থাকলে পাওনা শুধু সহজ, সরল কোড। দ্বিতীয় case যখন সত্যিই আসবে, তখন সে বলবে abstraction কেমন হওয়া উচিত।
একটু deeper ভাবো: YAGNI-র একটা পরিষ্কার economics-এর ব্যাখ্যা আছে। আগেভাগে flexibility বানানো যেন একটা option কেনার মতো — ভবিষ্যতে সুবিধা পাওয়ার অধিকারের জন্য এখনই premium দিচ্ছো। সমস্যা হলো software option সাধারণত ভুল দামে বিক্রি হয়: premium নিশ্চিতভাবে দিতে হয়, কিন্তু লাভ নির্ভর করে প্রয়োজন আর তার আকার দুটো নিয়েই কম সম্ভাবনার অনুমানের উপর। Lean software development-এ এর বিকল্পকে বলে last responsible moment পর্যন্ত সিদ্ধান্ত পিছিয়ে রাখা। এর সাথে Kent Beck-এর কথা মেলাও — ভালোভাবে সাজানো সহজ কোড পরিবর্তনের খরচ সমান রাখে। তাহলে YAGNI-র মূলকথা দাঁড়ায়: যখন refactoring সহজ, পরে abstract করার option প্রায় বিনামূল্যে — তাই আজকে নিশ্চিত premium দেওয়া আসলে একটা খারাপ বিনিয়োগ।
এক মানচিত্রে পুরো বিষয়টা:
কীভাবে চিনবে
চেকলিস্ট — দেখো তোমার project-এ কয়টা আছে:
- একটা
interfaceবা abstract base class যার ঠিক একটাই implementation আছে, আর মাস বা বছর পরেও দ্বিতীয়টার দেখা নেই। - Hook method যেটা কোনো subclass কখনো override করেনি।
- Parameter, option, বা "strategy" slot যেখানে প্রতিটা caller একই value পাঠায়।
- Generic type parameter যেটা সবসময় একটাই concrete type দিয়ে ব্যবহার হয়।
- "Manager", "Handler", "Provider", "Factory" নামের layer যেটা লেখা হয়নি এমন extension সাপোর্ট করার জন্য বসানো।
- Plugin system-এ zero plugin; callback registry-তে zero callback; event bus-এ একটাই publisher আর subscriber একই module-এ বসা।
- কোড যার একমাত্র যুক্তি জিজ্ঞেস করলে হয়: "পরে কেউ হয়তো এটা লাগতে পারে।"
| লক্ষণ | জিজ্ঞাসা করার প্রশ্ন | Speculative যদি... |
|---|---|---|
| Interface + ১টা implementation | আজকে কি সত্যিকারের দ্বিতীয় implementation বা test seam আছে? | না, "পরে হবে" |
| Config option | কোনো environment কি ভিন্ন value সেট করে? | সব একই value দেয় |
| Hook method | কোনো subclass কি override করে? | কখনো করেনি |
Generic <T> | একের বেশি type দিয়ে ব্যবহার হয়? | শুধু <Invoice> |
| Factory | কি কখনো ভিন্ন product return করতে পারে? | সবসময় একই class |
| "Extensible" plugin API | কয়টা plugin আছে? | শূন্য, সবসময় |
আর দল যখন সত্যিকারভাবে তাদের পুরনো "future-proofing" সিদ্ধান্ত পর্যালোচনা করে, স্কোরবোর্ড সাধারণত এরকম দেখায়:
দশটা অনুমানের মধ্যে মোটামুটি একটা কল্পিত আকারে ফলে। প্রতিটা speculative layer দিয়ে তুমি এই odds কিনছো।
কেন এটা সমস্যা
খরচ ১: কোনো লাভ ছাড়া indirection। একটা সহজ operation বুঝতে এখন পাঠককে লাফাতে হয়: interface থেকে factory, factory থেকে abstract base, তারপর একমাত্র concrete class — আর শেষে দেখা যায় পুরো যাত্রাটা কিছুই বোঝায়নি, কারণ সবসময় একটাই case ছিল। চারটা file পড়তে হলো যা একটা function বলে দিত।
একজন নতুন teammate ঠিক এই যাত্রাটা করতে দেখো:
খরচ ২: design-এ মিথ্যা প্রতিশ্রুতি। একটা abstraction সাইনবোর্ডের মতো — বলছে "এখানে variation আছে!" পাঠক সেটা বিশ্বাস করে অন্য implementation খুঁজতে যায়... যেগুলো নেই। Speculative কাঠামো design-কে মিথ্যা বানায়, আর পাঠকরা বারবার সেই মিথ্যায় সময় নষ্ট করে।
খরচ ৩: অনুমান প্রায়ই ভুল আকারের হয়। যখন সত্যিকারের দ্বিতীয় case আসে, সেটা কখনোই তার জন্য বানানো slot-এ ঠিকঠাক ফিট করে না। পুলের পাইপ ঠিক সেখানে আছে যেখানে বাগানের মাটি দরকার। এখন দ্বিগুণ ভাঙার কাজ: ভুল abstraction সরাও, তারপর সঠিকটা বানাও। সহজ সরল কোড সস্তার শুরু হতো।
খরচ ৪: এটা অন্য Dispensables-দের জন্ম দেয়। পিছনে ফেলে যাওয়া ফাঁকা class হয় Lazy Classes। কেউ ডাকে না এমন hook হয় Dead Code। একটা speculative module পুরো একটা smell পরিবার তৈরি করতে পারে।
বহনের খরচ বোঝা কঠিন কারণ এটা প্রতিদিন, প্রত্যেকের কাছ থেকে ছোট ছোট করে নেয়:
আর এটা হলো একটা সাধারণ speculative abstraction-এর জীবন কাহিনী — দেখো vindicated path কতটা বিরল:
দুটো পথের শেষ পর্যন্ত দেখো। Speculative পথ প্রতিটা branch-এ হারে — এমনকি যখন অনুমান সত্যি হয়, বেশিরভাগ সময় ভুল আকারে হয়। YAGNI পথ কখনো হারে না: সবচেয়ে খারাপ অবস্থায় দ্বিতীয় case আসলে refactor করো, পূর্ণ তথ্য হাতে নিয়ে।
বাস্তব কোডের উদাহরণ
জামাল সাহেবের বাড়ি, এবার billing feature হিসেবে। requirement ছিল এক লাইন: "উৎসবের দিনে ১০% ছাড় দাও।" কিন্তু বানানো হলো এটা:
// Smelly version: a framework for a one-line requirement
interface DiscountStrategy {
apply(amount: number): number;
}
interface DiscountStrategyProvider {
provide(): DiscountStrategy;
}
// the only strategy that has ever existed
class FestivalDiscount implements DiscountStrategy {
apply(amount: number): number {
return amount * 0.9;
}
}
// the only provider that has ever existed
class DefaultDiscountStrategyProvider implements DiscountStrategyProvider {
provide(): DiscountStrategy {
return new FestivalDiscount(); // always. only. forever.
}
}
class DiscountEngine<TContext> { // TContext: only ever 'Order'
constructor(
private readonly provider: DiscountStrategyProvider,
private readonly roundingMode: string = "standard", // every caller: "standard"
) {}
// hook for subclasses... that were never written
protected beforeApply(_context: TContext): void {}
run(context: TContext, amount: number): number {
this.beforeApply(context);
return this.provider.provide().apply(amount);
}
}
// caller — four floors of foundation for a one-floor house:
const engine = new DiscountEngine<Order>(new DefaultDiscountStrategyProvider());
const price = engine.run(order, amount);একটু খুঁটিয়ে দেখো:
- দুটো interface, প্রতিটার একটা করে implementation। পুলের পাইপ।
- একটা provider যেটা সবসময় একই জিনিস দেয়। সরানো যায় কিন্তু কখনো সরানো হয়নি এমন দেওয়াল।
roundingMode— এমন parameter যেখানে প্রতিটা caller একই value পাঠায়। তার ছাড়া conduit।TContext— generic যেটা ঠিক একটা type দিয়ে ব্যবহার হয়। কখনো বানানো হয়নি এমন তলার ভিত।beforeApply— এমন hook যেটা কোনো subclass override করেনি। নির্মাণের পর থেকে ইটে বন্ধ দরজা।
এই পুরো কলকব্জা আসলে কী করে? ০.৯ দিয়ে গুণ করে। এক লাইনের business logic, পাঁচ স্তরের বর্মে ঢাকা — শত্রুর বিরুদ্ধে যে কখনো আসেনি। নির্মাণ সাইটের blueprint:
ধাপে ধাপে পরিষ্কার করা
ভাঙার দিন। বাইরে থেকে ভেতরে কাজ করো।
ধাপ ১: কেউ পরিবর্তন করে না এমন parameter সরাও। প্রতিটা caller roundingMode = "standard" পাঠায়। Remove Parameter apply করো আর সরাসরি standard behavior ব্যবহার করো।
ধাপ ২: কেউ override করে না এমন hook inline করো। beforeApply কিছুই করে না, কোনো subclass সেটা বদলায়নি। Inline Method apply করো — কোনো চিহ্ন ছাড়াই মিলিয়ে যায়।
ধাপ ৩: Provider আর engine ভেতরে ঢোকাও। DefaultDiscountStrategyProvider সবসময় FestivalDiscount return করে; engine শুধু সেটায় forward করে। দুবার Inline Class apply করো — factory তার caller-এ ঢুকে যায়, wrapper মিলিয়ে যায়।
ধাপ ৪: এক-implementation interface গুটিয়ে নাও। DiscountStrategy-এর নিচে শুধু FestivalDiscount বাকি থাকায়, Collapse Hierarchy apply করো: concrete জিনিসটা রাখো, abstract প্রতিশ্রুতি মুছে দাও।
ধাপ ৫: যা বাকি থাকে দেখো।
// Clean version: the requirement, stated directly
const FESTIVAL_DISCOUNT_RATE = 0.1;
function festivalPrice(amount: number): number {
return amount * (1 - FESTIVAL_DISCOUNT_RATE);
}
// caller:
const price = festivalPrice(amount);পঁচিশ লাইনের framework হলো তিন লাইনের সত্য। একজন নতুন teammate পাঁচ সেকেন্ডে বোঝে। আর সুন্দর ব্যাপার হলো: এই সহজ version হলো আসল ভবিষ্যতের জন্য সবচেয়ে ভালো শুরু। পরের বছর যদি marketing সত্যিই "loyalty discount" আর "clearance discount" যোগ করে, তোমার কাছে দুটো বা তিনটা বাস্তব case থাকবে — আর তখন strategy interface বের করতে বিশ মিনিট লাগবে আর সেটা সঠিক আকারের হবে, কল্পনা নয় তথ্য দিয়ে তৈরি।
C#-এ একই smell
একটা report exporter যেটা কখনো আসেনি এমন format-এর জন্য "তৈরি":
// Before: an export framework with exactly one export
public interface IReportExporter
{
byte[] Export(Report report);
}
public abstract class ReportExporterBase : IReportExporter
{
public byte[] Export(Report report)
{
OnBeforeExport(report); // never overridden
return DoExport(report);
}
protected virtual void OnBeforeExport(Report report) { }
protected abstract byte[] DoExport(Report report);
}
public class PdfReportExporter : ReportExporterBase // the only child, ever
{
protected override byte[] DoExport(Report report)
=> PdfWriter.Write(report);
}
public static class ReportExporterFactory
{
public static IReportExporter Create(string format = "pdf") // only "pdf" is ever passed
=> new PdfReportExporter();
}চারটা type আর একটা factory — শুধুমাত্র একটা library method call করার জন্য। ভাঙার পরে:
// After: the one real capability, stated plainly
public class ReportExporter
{
public byte[] ExportPdf(Report report) => PdfWriter.Write(report);
}যখন Excel export একটা confirmed, scheduled requirement হবে — হলওয়েতে বলা "একদিন হবে" নয় — সেই মুহূর্তটাই interface introduce করার সময়, দুটো বাস্তব format দিয়ে আকৃতি দিয়ে।
Python-এ একটা উদাহরণ, যেখানে speculation প্রায়ই **kwargs আর base class-এ লুকিয়ে থাকে:
# Before: a base class and options built for imaginary subclasses
class NotificationSenderBase:
def send(self, message, priority="normal", retry_policy=None, **kwargs):
self.before_send(message) # no subclass overrides this
self._do_send(message)
def before_send(self, message):
pass
# After: the one thing the app actually does
def send_notification(message: str) -> None:
sms_gateway.send(message)প্রতিটা caller priority="normal" পাঠাতো, কেউ retry_policy দেয়নি, আর **kwargs ধুলো জমাতো। তিনটা সৎ লাইন কল্পিত framework-এর জায়গা নিল।
বাস্তব project-এ এই smell কোথায় লুকিয়ে থাকে
- প্রতিটা
FooService-এর জন্য স্বয়ংক্রিয়IFooService। কিছু codebase reflexively প্রতিটা class-এর সাথে interface pair করে। যেখানে কোনো seam ব্যবহার হয় না, সেখানে প্রতিটা pair একটা speculative layer। আধুনিক DI container আর mocking tool অনেক ক্ষেত্রে concrete class দিয়েও ভালো কাজ করে। - "হয়তো কোনোদিন database বদলাবো" abstraction layer। ORM swappable রাখার জন্য ভারী repository-on-repository wrapper — যে swap বেশিরভাগ কোম্পানিতে কখনো হয় না। এদিকে প্রতিটা query wrapper-এর কর দেয়।
- Framework-এর উপর নিজের framework। HTTP client-এর wrapper, logger-এর wrapper, message bus-এর wrapper — "vendor সহজে বদলাতে পারবো" — প্রতিটা wrapper যেটাকে wrap করছে তার চেয়ে feature কম আর documentation দুর্বল।
- কেউ set করে না এমন config option।
appsettings-এ ডজন ডজন knob যেটা প্রতিটা environment default-এ ছেড়ে দেয়। প্রতিটা knob একটা code path যেটা test করতে হয় আর মাথায় রাখতে হয়। - Premature microservices আর plugin architecture। একটা ছোট product-কে service বা plugin slot-এ ভাগ করা "পরে যে scale লাগবে" তার জন্য — আজকেই distributed-system জটিলতার মূল্য দেওয়া হচ্ছে এমন traffic-এর জন্য যা হয়তো কখনো আসবে না।
- এক ব্যবহারকারীর generic utility type।
Result<T, TError, TContext>machinery যেটা ঠিক একটা call site থেকে ঠিক একটা আকারে ব্যবহার হয়। - Subclass-দের কাছে পাঠানো field আর method যেটা কখনো ব্যবহার হয় না। "child class পরে ব্যবহার করতে পারবে" বলে বানানো — কখনো করেনি।
কখন উপেক্ষা করা ঠিক আছে
YAGNI একটা নীতি, ধর্ম না। কিছু up-front generality অর্জিত — বিচারটা প্রমাণ বনাম অনুমান নিয়ে।
| পরিস্থিতি | এখনই abstraction বানাবে? | কেন |
|---|---|---|
| বাইরের user-দের সাথে published API / library boundary | হ্যাঁ | বাইরের মানুষ contract-এর উপর নির্ভর করে; পরে বদলালে তারা ক্ষতিগ্রস্ত হবে |
| Roadmap-এ ইতোমধ্যে committed দ্বিতীয় implementation | হ্যাঁ | দুটো বাস্তব case আছে — একটা একটু ভবিষ্যতে |
| কঠিন dependency isolate করতে আজকেই দরকার এমন test seam | হ্যাঁ | এটা বর্তমান প্রয়োজন, speculation নয় |
| সমর্থন করতেই হবে এমন regulatory বা platform boundary | হ্যাঁ | requirement বাস্তব আর বাহ্যিক |
| অন্য team-এ পাঠানো framework-এ extension point | হ্যাঁ | "ভবিষ্যৎ ব্যবহারকারীরা" API-এর আসল customer |
| "নিশ্চয়ই একাধিক payment provider লাগবে" (একটাই আছে) | না | একটা বাস্তব case + একটা কল্পিত = speculation |
| "Generic বানাও, কেউ হয়তো reuse করবে" | না | সেই কাউকে অপেক্ষা করো; তাকে আকৃতি দিতে দাও |
| "পরে caller ছুঁতে না হওয়ার জন্য parameter এখনই যোগ করো" | না | পরে caller ছোঁয়া সবসময় knob বহন করার চেয়ে সস্তা |
যেকোনো প্রস্তাবিত abstraction বানানোর আগে এই chart-এ রাখতে পারো। শুধু top-right কোণটাই up-front কাঠামো পাওয়ার যোগ্য:
Fowler-এর Yagni essay থেকে আরেকটা গুরুত্বপূর্ণ কথা: YAGNI feature আর flexibility-তে প্রযোজ্য, quality-তে নয়। "YAGNI!" বলে test বাদ দেওয়া, naming উপেক্ষা করা, বা জটিল কোড লেখার অজুহাত হিসেবে ব্যবহার করো না — কোডকে পরে পরিবর্তন করা সহজ রাখাটাই YAGNI-কে নিরাপদ করে। ঠিক এই কারণেই তুমি শুধু আজকের প্রয়োজন বানাতে পারো — কারণ পরিষ্কার, ভালো-tested কোড কাল extend করা সস্তা।
কোন refactoring-গুলো এটা সারায়
| Speculative কাঠামো | সারানোর refactoring |
|---|---|
| Abstract class / subclass যে variation কখনো আসেনি | Collapse Hierarchy |
| একটা product-এর factory, provider, বা wrapper | Inline Class |
| কেউ customize করে না এমন hook বা delegating method | Inline Method |
| প্রতিটা caller identically পাঠায় এমন parameter | Remove Parameter |
| একটা implementation-এর interface, কোনো real seam নেই | Interface সরাও; class সরাসরি ব্যবহার করো |
| অব্যবহৃত "ভবিষ্যৎ" field আর method | মুছে দাও — দেখো Dead Code |
দ্রুত revision বক্স
+--------------------------------------------------------------+
| SPECULATIVE GENERALITY — QUICK REVISION |
+--------------------------------------------------------------+
| Story : A 2-floor family building 4-floor foundations |
| and pool plumbing for a pool that never comes. |
| Smell : Interfaces, hooks, factories, parameters, and |
| generics built for GUESSED future needs. |
| Why bad : Pay 4 times — build cost, delay cost, carry |
| cost, and repair cost when the real future |
| arrives in a different shape. |
| YAGNI : "You Aren't Gonna Need It" (XP; Fowler's bliki). |
| Build for today; extract abstractions from REAL |
| cases, not imagined ones. |
| Test : "Two real cases, or one real + one imagined?" |
| Two real -> abstract. One + hunch -> stay simple. |
| Cures : Collapse Hierarchy, Inline Class, Inline Method, |
| Remove Parameter, plain deletion. |
| Caution : YAGNI is about features, never about quality. |
+--------------------------------------------------------------+Practice exercise
ধরো একটা school noticeboard app-এর ঠিক একটাই কাজ: আজকের নোটিশ দেখাও, নতুনটা আগে। একজন অতি উৎসাহী developer এটা দিল:
interface NoticeSource<TFilter> {
fetch(filter: TFilter): Notice[];
}
interface NoticeRanker {
rank(notices: Notice[]): Notice[];
}
class DatabaseNoticeSource implements NoticeSource<DateFilter> {
fetch(filter: DateFilter): Notice[] {
return db.notices.where("date", filter.date);
}
}
class NewestFirstRanker implements NoticeRanker {
rank(notices: Notice[]): Notice[] {
return [...notices].sort((a, b) => b.time - a.time);
}
}
class NoticeBoardEngine<TFilter> {
constructor(
private source: NoticeSource<TFilter>,
private ranker: NoticeRanker,
private maxItems = 100, // every caller: 100
private theme = "default", // every caller: "default"; unused inside
) {}
protected onBeforeRender(): void {} // no subclass exists
display(filter: TFilter): Notice[] {
this.onBeforeRender();
const all = this.source.fetch(filter);
return this.ranker.rank(all).slice(0, this.maxItems);
}
}তোমার কাজ:
- একটা speculation তালিকা বানাও: কল্পিত ভবিষ্যতের জন্য থাকা প্রতিটা কলকব্জা চিহ্নিত করো। interface, generic, অব্যবহৃত parameter, constant-valued parameter, আর hook গুনে দেখো — অন্তত ছয়টা আইটেম।
- প্রতিটা আইটেমের জন্য, কোন ভাঙার refactoring ব্যবহার করবে তার নাম লেখো: Collapse Hierarchy, Inline Class, Inline Method, Remove Parameter, বা সরাসরি মুছে ফেলা।
- পরিষ্কার version লেখো। লক্ষ্য: একটা ছোট function বা ছোট class, দশ লাইনের মধ্যে, যেটা আজকের নোটিশ fetch করে নতুনটা আগে sort করে।
- এখন plot twist: ছয় মাস পরে, school সত্যিই দ্বিতীয় source চাইল — অভিভাবকদের WhatsApp group export থেকে নোটিশ। তিন বা চার লাইনে sketch করো যে abstraction তুমি এখন introduce করবে, দুটো বাস্তব source দিয়ে আকৃতি দিয়ে। লক্ষ্য করো এই design আসল অনুমানের চেয়ে কতটা better-informed।
- আসল
NoticeBoardEngineআর তোমার ছয় মাসের abstraction চিত্র ১০-এর quadrant chart-এ রাখো। প্রতিটা কোন quadrant-এ পড়ে আর কেন? - তোমার নিজের বর্তমান project-এ সৎ পরীক্ষা apply করো: একটা single implementation-এর interface খুঁজে বের করো। এক বাক্যে বিচার করো — আসল seam, নাকি পুলের পাইপ?
- Bonus: দুই বাক্যে ব্যাখ্যা করো কেন পরিষ্কার দশ লাইনের version WhatsApp feature-এর জন্য আসল "extensible" engine-এর চেয়ে ভালো শুরু ছিল। Hint: কোন version reshape করা সহজ ছিল?
যখন তোমার noticeboard দশটা সৎ লাইনে আজকের কাজ করে — আর তুমি ঠিক কোন প্রমাণ কোন abstraction justify করবে তা ব্যাখ্যা করতে পারো — তখন তুমি YAGNI সেভাবে বুঝেছো যেভাবে Fowler বলেছিলেন। আর নাসরিন architect অবশেষে একটা বুদ্ধিমানের বাড়ি বানাতে পারবেন।
সচরাচর জিজ্ঞাসা
- Speculative Generality মানে কী সহজ ভাষায়?
- এটা হলো বাড়তি কাঠামো — interface, base class, parameter, hook, plugin system — যেটা আজকে বানানো হয় ভবিষ্যতের একটা অনুমানিত প্রয়োজনের জন্য। সেই অনুমান সাধারণত ভুল হয় বা কোনোদিন আসেই না, কিন্তু বাড়তি জটিলতা থেকে যায় আর প্রতিটা পাঠক ও পরিবর্তন তার মূল্য দেয়।
- YAGNI মানে কী?
- YAGNI মানে 'You Aren't Gonna Need It' — অর্থাৎ 'তোমার এটা লাগবে না'। এটা Extreme Programming-এর একটা নীতি, Martin Fowler-এর bliki-তে বিস্তারিত আছে: অনুমানিত ভবিষ্যৎ প্রয়োজনের জন্য কিছু বানাবে না; বাস্তব দরকার হলে তখন বানাও। এতে বানানোর খরচ, বহন করার খরচ, আর ভুল অনুমান ভাঙার খরচ — তিনটাই বাঁচে।
- ভবিষ্যতের কথা ভেবে কোড লেখা কেন খারাপ? পরিকল্পনা করা কি ভালো না?
- পরিকল্পনা ভালো; কোডে অনুমান করা ভালো না। ভবিষ্যতের requirement প্রায় কখনোই সেই আকারে আসে না যেটা ভাবা হয়েছিল। তাই speculative abstraction আগে ভাঙতে হয়, তারপর সঠিকটা বানাতে হয় — মোট কাজ কমে না, বরং বাড়ে। সহজ সরল কোডই যেকোনো ভবিষ্যতের জন্য সবচেয়ে ভালো শুরু।
- Speculative Generality, Lazy Class আর Dead Code-এর মধ্যে পার্থক্য কী?
- এরা ঘনিষ্ঠ আত্মীয়। Speculative Generality হলো অপ্রয়োজনীয় কাঠামো বানানোর কাজটা; এর ফলে যে ফাঁকা class পড়ে থাকে সেটা Lazy Class, আর যে hook কেউ কখনো ডাকে না সেটা Dead Code। একটা smell বাকি দুটো তৈরি করে।
- কখন আগে থেকে abstraction বানানো আসলেই যুক্তিসঙ্গত?
- যখন প্রমাণ আছে, অনুমান নয়: বাইরের user-রা ইতোমধ্যে যে published API-এর উপর নির্ভর করছে, roadmap-এ confirm করা দ্বিতীয় implementation, আজকেই দরকার এমন test seam, অথবা support করতেই হবে এমন regulatory বা platform boundary। সৎ পরীক্ষা: আমার কাছে কি দুটো বাস্তব case আছে, নাকি একটা বাস্তব আর একটা কল্পিত?
আরো দেখো
সম্পর্কিত পাঠ
Lazy Class: যে চাকরির কাজ শুধু একটা বাটন চাপা
Lazy Class code smell শিখো একটা মজার গল্পের মাধ্যমে। কোন class-গুলো টিকে থাকার যোগ্যতা রাখে না সেটা বুঝতে পারবে, আর Inline Class দিয়ে সেগুলো ঠিক করতে পারবে।
Dead Code: গুদামঘরে পুরনো জিনিস 'যদি লাগে' বলে আটকে রাখা
Dead Code smell শেখো একটা গুদামঘরের গল্প দিয়ে। দেখো কেন না-চলা কোড আসলে টাকা আর সময় নষ্ট করে, Knight Capital-এর ৪৪০ মিলিয়ন ডলারের ঘটনা থেকে শিখো, আর সহজভাবে সমাধান করো।
Shotgun Surgery: এক জায়গায় পরিবর্তন, দশ জায়গায় দৌড়াদৌড়ি
Shotgun Surgery code smell শিখবে রুবেলের বাসা বদলের গল্পের মাধ্যমে — সহজ সংজ্ঞা, TypeScript আর C# এর example, Divergent Change এর সাথে পার্থক্য, আর practice সহ।
Collapse Hierarchy: যখন Parent আর Child Class একই হয়ে যায়
একটা মহল্লার কমিটির গল্পের মাধ্যমে Collapse Hierarchy refactoring শেখো — TypeScript আর C#-এ superclass আর subclass মার্জ করার ধাপে ধাপে পদ্ধতি, আর কখন বুঝবে একটা hierarchy আর কাজে আসছে না।