Part 01 · Low Level Design

Design Principles: DRY, KISS, YAGNI

DRYKISSYAGNISeparation of concernsLaw of Demeter

Copy-paste, দরকারের বেশি engineering আর a.b().c().d() এর মতো long chain: কোন principle কোন ব্যথা সারায়, আর কখন নিয়ম ভাঙাটাই ঠিক।

⏱ 1-2 ঘণ্টা · Station 5 / 80

তিনটা screen, তিন রকম delivery fee

একটা food delivery app তিন জায়গায় delivery fee দেখায়: checkout screen এ, receipt এ, আর refund job এ, যেটা order fail হলে customer কে টাকা ফেরত দেয়। তিনটা লিখেছে তিনজন developer, আর প্রত্যেকে fee এর rule টা নিজে নিজে লিখেছে: "প্রথম ৩ km এ ৪০ টাকা, তারপর প্রতি km এ ১০ টাকা, ঢাকায় ৫০০ টাকার উপরে free"।

Rule বদলে base হলো ৫০ টাকা। কেউ একজন checkout আর receipt update করলো। Refund job এ এখনো ৪০। দুই সপ্তাহ ধরে refund এ ১০ টাকা কম যাচ্ছে, আর customer রা অভিযোগ করছে app টাকা "মেরে দিচ্ছে"।

এদিকে আরেকজন developer কে fee "configurable" করতে বলা হলো, আর সে বানালো একটা FeeStrategyFactory, সাথে plugin system, একটা YAML file আর চারটা interface। অথচ business এর rule কখনো একটার বেশি ছিল না।

দুইটা সমস্যার মূল একই: কতটুকু structure দরকার, সেটা ঠিক করা। Engineer রা এই প্রশ্নের উত্তর দিতে যে কয়েকটা চলতি নিয়ম মেনে চলে, এই chapter সেগুলো নিয়ে।

সমস্যা: structure খুব কম, নয়তো খুব বেশি

খারাপ design দুই রকম হয়:

খুব কমখুব বেশি
একই rule কয়েক জায়গায় copyএকটাই implementation এর জন্য interface
একটা function parse, হিসাব, format, save সব করেদুই field এর object এর জন্য factory
a.b().c().d() এর মতো লম্বা হাত বাড়ানো codeকখনো বদলায় না এমন value এর জন্য config file
একটা change এ অনেক file এ editএকটা change এ অনেক layer এ edit

দুইটাই change কে দামি করে। এই chapter এর নিয়ম গুলো দুই দিক থেকে ঠেলে মাঝখানে আনে: প্রতিটা জ্ঞানের একটাই ঘর, আর আজকের requirement এর চেয়ে বেশি কলকব্জা না।

একটা রান্নার বইয়ের কথা ভাবো

ভালো একটা restaurant তার biryani র জন্য একটাই recipe রাখে। সব রাঁধুনি সেটা মেনে চলে। মালিক মশলার মাপ বদলালে এক পাতা বদলায়, আর সব plate একসাথে বদলে যায়। এটা DRY।

Recipe তে লেখা থাকে "দুই কাপ চাল", "একটা configurable পরিমাণের grain-type abstraction" না। এটা KISS।

Menu তে নেই এমন খাবারের recipe কেউ লিখে রাখে না "কোনোদিন কেউ চাইতে পারে" ভেবে। এটা YAGNI।

আর waiter রান্নাঘরে জিজ্ঞেস করে "biryani হয়েছে?", "পেছনের দ্বিতীয় চুলার তিন নম্বর হাঁড়িটা কি যথেষ্ট গরম?" না। এটা Law of Demeter।

checkoutreceiptrefund jobfee rule copy40 tk + 10/kmfee rule copy40 tk + 10/kmfee rule copy50 tk + 10/kmdeliveryFee()the only copythree copies, one has already driftedfee?fee?fee?one rule, one placeordercustomeraddresscity.customer.address.cityorder.DeliveryCity()
Play চাপো। উপরে: তিনটা caller প্রত্যেকে fee rule এর নিজের copy নিয়ে বসে আছে, আর একটা এর মধ্যেই ৫০ টাকায় সরে গেছে; তারপর তিনজনই একটা deliveryFee() কে ডাকে। নিচে: একটা call city পেতে order, customer আর address এর ভেতর দিয়ে হাত বাড়ায়, তারপর সোজা order.DeliveryCity() কে জিজ্ঞেস করে।

তবে একটা জায়গায় মেলে না। রান্নার বই একবার লেখা হয়, কালেভদ্রে বদলায়। Code বদলায় প্রতি সপ্তাহে, আর ঠিক এই কারণেই code এ duplication বেশি ভোগায়: কোনো কিছু যত ঘনঘন বদলায়, তার প্রতিটা বাড়তি copy তত বেশি খরচ করায়।

আগে অনুমান করো

Delivery fee এর rule থাকে একটা function এ, deliveryFee()। Checkout, receipt আর refund job সবাই ওটাকে ডাকে। Base fee ৪০ থেকে ৫০ টাকা হলো। কয় জায়গায় বদলাতে হবে?

পাঁচটা চলতি নিয়ম

  1. DRY (Don't Repeat Yourself) এর বিষয় হলো জ্ঞান, লেখা না। প্রতিটা business rule এর একটাই আসল ঘর থাকা উচিত। দেখতে একরকম কিন্তু মানে আলাদা এমন দুইটা code (delivery fee আর service fee, আজ দুটোই কাকতালীয়ভাবে ৪০ টাকা) duplication না; এদের এক করলে এমন দুইটা rule জোড়া লেগে যায় যারা আলাদা আলাদা বদলাবে।
  2. KISS (Keep It Simple): আজকের requirement পূরণ করে এমন সবচেয়ে সহজ design বাছো। পরিষ্কার input ওয়ালা একটা সাধারণ function একটা framework এর চেয়ে ভালো। সহজ code পড়া, test করা আর বদলানো সহজ, পরে জটিল কিছুতে বদলানোও।
  3. YAGNI (You Aren't Gonna Need It): কেউ চায়নি এমন feature এর জন্য কিছু বানিও না। আন্দাজে বানানো flexibility বেশিরভাগ সময় ভুল flexibility, আর কেউ সাহস করে মুছে না দেওয়া পর্যন্ত সেটা maintain করতে হয়।
  4. Separation of concerns: বদলানোর আলাদা আলাদা কারণ আলাদা জায়গায় রাখো। দাম হিসাব করা আর receipt সাজানো বদলায় আলাদা কারণে (business বনাম দেখানো), তাই এরা থাকে আলাদা function এ।
  5. Law of Demeter ("বন্ধুর সাথে কথা বলো, অচেনার সাথে না"): একটা method ডাকবে নিজের, নিজের field এর, নিজের parameter এর, আর নিজে বানানো object এর method; এদের কাছ থেকে পাওয়া object এর ভেতরে ঢুকে না। order.customer.address.city caller কে তিনটা class এর গঠনের সাথে বেঁধে দেয়। order.DeliveryCity() বাঁধে শুধু একটার সাথে।
  6. এরা একে অপরের উল্টো দিকে টানে। DRY তোমাকে abstraction এর দিকে ঠেলতে পারে; KISS আর YAGNI ফিরিয়ে আনে। ভারসাম্যটা হলো: জ্ঞানের duplication দ্বিতীয় বা তৃতীয়বার দেখলেই সরাও, আর abstraction আনার আগে দ্বিতীয় একটা সত্যিকারের case আসা পর্যন্ত অপেক্ষা করো।

Fee টা refactor করা, ধাপে ধাপে

ধাপে ধাপে দেখো

তিনটা copy থেকে একটা ঘর

CheckoutReceiptRefund jobdeliveryFee()

ধাপ 1/5 লেখা না, জ্ঞানটা খোঁজো

তিন জায়গাতেই হিসাব হচ্ছে '৩ km পর্যন্ত base, তারপর প্রতি km, ঢাকায় ৫০০ থেকে free'। এটা একটাই business rule, তিনবার লেখা।

copies
3
refund job base
40 (stale)

Structure যোগ বা বাদ দেওয়ার আগে

তিনটা প্রশ্ন করো:

  • এই rule বদলালে কয় জায়গায় edit করতে হবে? একের বেশি হলে জ্ঞান duplicate হয়ে আছে (DRY)।
  • নতুন একজন teammate কে এই design দুই বাক্যে বোঝাতে পারবো? না পারলে সম্ভবত দরকারের চেয়ে বেশি চালাকি করা হয়েছে (KISS)।
  • এই flexibility র জন্য কি আজ সত্যিকারের কোনো requirement আছে? না থাকলে মুছে দাও, বা বানিও না (YAGNI)।

আর প্রতিটা method call এর সময়: আমি কি বন্ধুকে জিজ্ঞেস করছি, নাকি বন্ধুর ভেতর দিয়ে হাত বাড়িয়ে অচেনা কাউকে ধরছি? (Demeter)

নিজে বলো: কোন নিয়মটা ভাঙছে?

প্রতিটা case চালিয়ে দেখো

নিয়মটার নাম বলো

price * 1.15  // checkout.go, invoice.go, refund.go ...
ভাঙে: DRY। একটা জ্ঞানের (VAT এর হার) পাঁচটা ঘর। একটা function বা constant এ রাখো, সবাই সেটাই ব্যবহার করুক।

Code এ delivery fee

Fee এর rule এর একটাই ঘর (DRY, KISS), দাম হিসাব আলাদা আর formatting আলাদা (separation of concerns), আর DeliveryCity(), যাতে caller কে customer এর address এর ভেতরে হাত দিতে না হয় (Demeter)। চারটা order, আর refund job ও একই rule ডাকে।

package main

import "fmt"

type Address struct{ City string }

type Customer struct {
	Name    string
	address Address
}

type Order struct {
	ID       int
	Subtotal int // taka
	Km       int
	customer Customer
}

// Law of Demeter: callers ask the order, instead of reaching through
// order.customer.address.City themselves.
func (o Order) DeliveryCity() string { return o.customer.address.City }

// DRY: the delivery fee rule lives in exactly one function.
// KISS: a plain function with three inputs; no strategy, no factory, no config file.
func deliveryFee(subtotal, km int, city string) int {
	if city == "Dhaka" && subtotal >= 500 {
		return 0 // free delivery in Dhaka from 500 taka
	}
	fee := 40
	if km > 3 {
		fee += (km - 3) * 10
	}
	return fee
}

// Separation of concerns: pricing works out numbers...
func price(o Order) (fee, total int) {
	fee = deliveryFee(o.Subtotal, o.Km, o.DeliveryCity())
	return fee, o.Subtotal + fee
}

// ...and formatting turns numbers into text. Neither knows about the other's job.
func receipt(o Order, fee, total int) string {
	return fmt.Sprintf("order %d: %d tk + delivery %d tk = %d tk (%s, %d km)",
		o.ID, o.Subtotal, fee, total, o.DeliveryCity(), o.Km)
}

func main() {
	dhaka := Address{City: "Dhaka"}
	ctg := Address{City: "Chattogram"}
	orders := []Order{
		{ID: 1, Subtotal: 350, Km: 2, customer: Customer{"Rahim", dhaka}},
		{ID: 2, Subtotal: 650, Km: 6, customer: Customer{"Nadia", dhaka}},
		{ID: 3, Subtotal: 450, Km: 9, customer: Customer{"Tania", ctg}},
		{ID: 4, Subtotal: 650, Km: 6, customer: Customer{"Sabbir", ctg}},
	}
	for _, o := range orders {
		fee, total := price(o)
		fmt.Println(receipt(o, fee, total))
	}
	// The checkout screen and the refund job call the same rule, so they can't disagree.
	fmt.Println("refund job, order 3 delivery:", deliveryFee(450, 9, "Chattogram"), "tk")
}

ছয়টাই হুবহু এটা print করে:

order 1: 350 tk + delivery 40 tk = 390 tk (Dhaka, 2 km)
order 2: 650 tk + delivery 0 tk = 650 tk (Dhaka, 6 km)
order 3: 450 tk + delivery 100 tk = 550 tk (Chattogram, 9 km)
order 4: 650 tk + delivery 70 tk = 720 tk (Chattogram, 6 km)
refund job, order 3 delivery: 100 tk

Output টা পড়ি:

  • Order 1: ২ km, প্রথম ৩ km এর ভেতরে, তাই base ৪০ টাকা।
  • Order 2: ঢাকা, আর ৬৫০ ≥ ৫০০, তাই ৬ km হলেও delivery free।
  • Order 3: চট্টগ্রাম, ৯ km: ৪০ + (৯ − ৩) × ১০ = ১০০।
  • Order 4: order 2 এর মতোই subtotal আর দূরত্ব, কিন্তু ঢাকার বাইরে, তাই ৪০ + ৩০ = ৭০। Free delivery র rule টা city র উপর নির্ভর করে, আর এই কারণেই fee function নিজে city খুঁজে না নিয়ে input হিসেবে নেয়।
  • Refund job order 3 এর জন্য ১০০ print করে, checkout এর মতোই। এর আর আলাদা হয়ে যাওয়ার উপায় নেই, কারণ আলাদা কোনো copy ই নেই।
  • Language ভেদে কী আলাদা: খুবই কম, আর এটাই আসল কথা: সাধারণ একটা function সব language এ একই idea। Java আর Python record/frozen dataclass ব্যবহার করে, যাতে ভুল করে order edit না হয়; TypeScript আর C++ customer কে private field এর পেছনে লুকায়, যাতে city পাওয়ার একমাত্র রাস্তা হয় DeliveryCity()।

Team এ কাজ করার সময় এই নিয়ম গুলো

  • তিনের নিয়ম: নিশ্চিত না হলে দ্বিতীয় copy টা মেনে নাও; তৃতীয়টা আসলে বের করে আনো। ততক্ষণে বুঝে যাবে আসলে কোনটা shared।
  • আলাদা service এর মধ্যে duplication কখনো কখনো ঠিক আছে। দুইটা team এর দুইটা microservice হয়তো একটা rule এর ছোট copy নিজের কাছে রাখবে, একটা shared library দিয়ে নিজেদের release জোড়া লাগানোর চেয়ে। DRY সবচেয়ে জরুরি একটা codebase এর ভেতরে।
  • আন্দাজে লেখা code মুছে দাও। কোনো "ভবিষ্যতের" extension point এ এক বছর ধরে একটাই implementation থাকলে inline করে ফেলো। Code কম মানে পড়ার জিনিসও কম।
  • Demeter মানে dot গোনা না। builder.WithA().WithB() আর list.Filter().Map() একই object এর উপর বা নতুন value এর উপর call করে; এগুলো ঠিক আছে। সমস্যা হলো অন্যের ভেতরের জিনিসে ঢুকে পড়া।
  • Fee কে pure function হিসেবে লেখো আর একটা table দিয়ে test করো। Value ঢোকে, value বের হয়, তাই কয়েক ডজন case millisecond এ চলে।

যে design গুলো পরে problem create করে

প্রতিটা case চালিয়ে দেখো

যে design টা পরে ভোগাবে

fee := 40 + max(0, km-3)*10  // partner_api.go
ভাঙে: চতুর্থ copy। পরের rule change এ এর একটা বাদ পড়বেই। তার বদলে deliveryFee ডাকো।

একটু থেমে ভাবো

তোমার lead বললেন: "deliveryFee কে এখনই pluggable বানাও। Marketing হয়তো কোনোদিন weekend pricing চাইবে।" Roadmap এ weekend pricing এর কোনো কথা নেই।

উত্তর দেখো

আপাতত সাধারণ function ই রাখো (YAGNI)। Weekend pricing যেদিন সত্যিকারের requirement হবে, সেদিন দিনটাকে একটা input হিসেবে যোগ করবে, অথবা সত্যিই কয়েক রকম pricing আসলে তখন function টাকে strategy বানাবে। Rule এর যেহেতু ঠিক একটাই ঘর আর input গুলো pure, তাই সেই পরিবর্তন হবে ছোট আর নিরাপদ। DRY আর KISS এর চুপচাপ লাভ এটাই: এরা পরের abstraction কে সস্তা করে দেয়, তাই আগেভাগে বানাতে হয় না।

ছোট্ট quiz

১. DRY ঠিক কোন জিনিস repeat না করার কথা বলে?

উত্তর

জ্ঞান: business rule আর সিদ্ধান্ত। দেখতে একরকম কিন্তু আলাদা rule বোঝায় এমন দুইটা code block duplication না, আর এদের এক করা উচিত না।

২. KISS আর YAGNI এর পার্থক্য কী?

উত্তর

KISS হলো যা দরকার সেটা কীভাবে বানাবে: যত সহজে সম্ভব। YAGNI হলো কী বানাবে: শুধু এখন যা দরকার, আন্দাজের ভবিষ্যৎ feature না।

৩. order.customer.address.city কেন order.DeliveryCity() এর চেয়ে খারাপ?

উত্তর

Caller তিনটা class এর ভেতরের গঠনের উপর নির্ভর করে। Customer বা Address বদলালে এমন প্রতিটা caller ভাঙে। DeliveryCity() থাকলে শুধু Order কে জানতে হয়।

৪. deliveryFee কেন নিজে city না পড়ে parameter হিসেবে নেয়?

উত্তর

এতে rule টা pure থাকে আর data কোথায় থাকে সেখান থেকে আলাদা থাকে (separation of concerns), তাই সাধারণ value দিয়ে test করা যায়, আর refund job সহ যেকোনো caller এটা ব্যবহার করতে পারে।

তোমার challenge: weekend এর rule, সহজভাবে

Marketing এবার সত্যিই চায়: শুক্রবার সব শহরে ৫০০ টাকা থেকে delivery free।

  • City র মতোই দিনটাকে deliveryFee এর একটা input হিসেবে যোগ করো। নতুন কোনো class বা interface আনবে না।
  • গল্পের শেষে চারটা order একটা শুক্রবারের জন্য আবার চালাও।

চালানোর আগে আন্দাজ করো শুক্রবারে order 4 এর কী হবে।

হয়েছে বুঝবে যখন শুক্রবারে order 4 print করবে order 4: 650 tk + delivery 0 tk = 650 tk (Chattogram, 6 km), আগের চারটা লাইন আর refund এর লাইন একই থাকবে, আর rule টা তখনো থাকবে শুধু deliveryFee এ।

এক কথায়

প্রতিটা জ্ঞানের ঠিক একটা ঘর দাও (DRY), আজ যা কাজ করে তার সবচেয়ে সহজ রূপটা বানাও (KISS), আর কাল্পনিক ভবিষ্যতের জন্য কিছুই না (YAGNI), বদলানোর আলাদা কারণ আলাদা রাখো, আর বন্ধুর ভেতর দিয়ে হাত না বাড়িয়ে সরাসরি বন্ধুকেই জিজ্ঞেস করো। পরিষ্কার, সহজ code ই কালকের abstraction কে সস্তা করে দেয়।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →