Part 01 · Low Level Design
Design Principles: DRY, KISS, YAGNI
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।
তবে একটা জায়গায় মেলে না। রান্নার বই একবার লেখা হয়, কালেভদ্রে বদলায়। Code বদলায় প্রতি সপ্তাহে, আর ঠিক এই কারণেই code এ duplication বেশি ভোগায়: কোনো কিছু যত ঘনঘন বদলায়, তার প্রতিটা বাড়তি copy তত বেশি খরচ করায়।
Delivery fee এর rule থাকে একটা function এ, deliveryFee()। Checkout, receipt আর refund job সবাই ওটাকে ডাকে। Base fee ৪০ থেকে ৫০ টাকা হলো। কয় জায়গায় বদলাতে হবে?
পাঁচটা চলতি নিয়ম
- DRY (Don't Repeat Yourself) এর বিষয় হলো জ্ঞান, লেখা না। প্রতিটা business rule এর একটাই আসল ঘর থাকা উচিত। দেখতে একরকম কিন্তু মানে আলাদা এমন দুইটা code (delivery fee আর service fee, আজ দুটোই কাকতালীয়ভাবে ৪০ টাকা) duplication না; এদের এক করলে এমন দুইটা rule জোড়া লেগে যায় যারা আলাদা আলাদা বদলাবে।
- KISS (Keep It Simple): আজকের requirement পূরণ করে এমন সবচেয়ে সহজ design বাছো। পরিষ্কার input ওয়ালা একটা সাধারণ function একটা framework এর চেয়ে ভালো। সহজ code পড়া, test করা আর বদলানো সহজ, পরে জটিল কিছুতে বদলানোও।
- YAGNI (You Aren't Gonna Need It): কেউ চায়নি এমন feature এর জন্য কিছু বানিও না। আন্দাজে বানানো flexibility বেশিরভাগ সময় ভুল flexibility, আর কেউ সাহস করে মুছে না দেওয়া পর্যন্ত সেটা maintain করতে হয়।
- Separation of concerns: বদলানোর আলাদা আলাদা কারণ আলাদা জায়গায় রাখো। দাম হিসাব করা আর receipt সাজানো বদলায় আলাদা কারণে (business বনাম দেখানো), তাই এরা থাকে আলাদা function এ।
- Law of Demeter ("বন্ধুর সাথে কথা বলো, অচেনার সাথে না"): একটা method ডাকবে নিজের, নিজের field এর, নিজের parameter এর, আর নিজে বানানো object এর method; এদের কাছ থেকে পাওয়া object এর ভেতরে ঢুকে না।
order.customer.address.citycaller কে তিনটা class এর গঠনের সাথে বেঁধে দেয়।order.DeliveryCity()বাঁধে শুধু একটার সাথে। - এরা একে অপরের উল্টো দিকে টানে। DRY তোমাকে abstraction এর দিকে ঠেলতে পারে; KISS আর YAGNI ফিরিয়ে আনে। ভারসাম্যটা হলো: জ্ঞানের duplication দ্বিতীয় বা তৃতীয়বার দেখলেই সরাও, আর abstraction আনার আগে দ্বিতীয় একটা সত্যিকারের case আসা পর্যন্ত অপেক্ষা করো।
Fee টা refactor করা, ধাপে ধাপে
তিনটা copy থেকে একটা ঘর
ধাপ 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)
নিজে বলো: কোন নিয়মটা ভাঙছে?
নিয়মটার নাম বলো
price * 1.15 // checkout.go, invoice.go, refund.go ...
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")
}from dataclasses import dataclass
@dataclass(frozen=True)
class Address:
city: str
@dataclass(frozen=True)
class Customer:
name: str
address: Address
@dataclass(frozen=True)
class Order:
id: int
subtotal: int # taka
km: int
customer: Customer
def delivery_city(self) -> str:
"""Law of Demeter: callers ask the order instead of reaching through customer.address.city."""
return self.customer.address.city
def delivery_fee(subtotal: int, km: int, city: str) -> int:
"""DRY: the delivery fee rule lives in exactly one function.
KISS: a plain function with three inputs; no strategy, no factory, no config file."""
if city == "Dhaka" and subtotal >= 500:
return 0 # free delivery in Dhaka from 500 taka
return 40 + max(0, km - 3) * 10
def price(o: Order) -> tuple[int, int]:
"""Separation of concerns: pricing works out numbers..."""
fee = delivery_fee(o.subtotal, o.km, o.delivery_city())
return fee, o.subtotal + fee
def receipt(o: Order, fee: int, total: int) -> str:
"""...and formatting turns numbers into text."""
return f"order {o.id}: {o.subtotal} tk + delivery {fee} tk = {total} tk ({o.delivery_city()}, {o.km} km)"
dhaka, ctg = Address("Dhaka"), Address("Chattogram")
orders = [
Order(1, 350, 2, Customer("Rahim", dhaka)),
Order(2, 650, 6, Customer("Nadia", dhaka)),
Order(3, 450, 9, Customer("Tania", ctg)),
Order(4, 650, 6, Customer("Sabbir", ctg)),
]
for o in orders:
fee, total = price(o)
print(receipt(o, fee, total))
# The checkout screen and the refund job call the same rule, so they can't disagree.
print("refund job, order 3 delivery:", delivery_fee(450, 9, "Chattogram"), "tk")interface Address {
readonly city: string;
}
interface Customer {
readonly name: string;
readonly address: Address;
}
class Order {
readonly id: number;
readonly subtotal: number; // taka
readonly km: number;
readonly #customer: Customer;
constructor(id: number, subtotal: number, km: number, customer: Customer) {
this.id = id;
this.subtotal = subtotal;
this.km = km;
this.#customer = customer;
}
// Law of Demeter: callers ask the order instead of reaching through customer.address.city.
deliveryCity(): string {
return this.#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.
function deliveryFee(subtotal: number, km: number, city: string): number {
if (city === "Dhaka" && subtotal >= 500) return 0; // free delivery in Dhaka from 500 taka
return 40 + Math.max(0, km - 3) * 10;
}
// Separation of concerns: pricing works out numbers...
function price(o: Order): [fee: number, total: number] {
const fee = deliveryFee(o.subtotal, o.km, o.deliveryCity());
return [fee, o.subtotal + fee];
}
// ...and formatting turns numbers into text.
function receipt(o: Order, fee: number, total: number): string {
return `order ${o.id}: ${o.subtotal} tk + delivery ${fee} tk = ${total} tk (${o.deliveryCity()}, ${o.km} km)`;
}
const dhaka: Address = { city: "Dhaka" };
const ctg: Address = { city: "Chattogram" };
const orders = [
new Order(1, 350, 2, { name: "Rahim", address: dhaka }),
new Order(2, 650, 6, { name: "Nadia", address: dhaka }),
new Order(3, 450, 9, { name: "Tania", address: ctg }),
new Order(4, 650, 6, { name: "Sabbir", address: ctg }),
];
for (const o of orders) {
const [fee, total] = price(o);
console.log(receipt(o, fee, total));
}
// The checkout screen and the refund job call the same rule, so they can't disagree.
console.log("refund job, order 3 delivery:", deliveryFee(450, 9, "Chattogram"), "tk");struct Address { city: String }
struct Customer { _name: String, address: Address }
struct Order { id: u32, subtotal: u32, km: u32, customer: Customer }
impl Order {
// Law of Demeter: callers ask the order instead of reaching through customer.address.city.
fn delivery_city(&self) -> &str { &self.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.
fn delivery_fee(subtotal: u32, km: u32, city: &str) -> u32 {
if city == "Dhaka" && subtotal >= 500 {
return 0; // free delivery in Dhaka from 500 taka
}
40 + km.saturating_sub(3) * 10
}
// Separation of concerns: pricing works out numbers...
fn price(o: &Order) -> (u32, u32) {
let fee = delivery_fee(o.subtotal, o.km, o.delivery_city());
(fee, o.subtotal + fee)
}
// ...and formatting turns numbers into text.
fn receipt(o: &Order, fee: u32, total: u32) -> String {
format!("order {}: {} tk + delivery {fee} tk = {total} tk ({}, {} km)", o.id, o.subtotal, o.delivery_city(), o.km)
}
fn main() {
let order = |id, subtotal, km, name: &str, city: &str| Order {
id, subtotal, km,
customer: Customer { _name: name.into(), address: Address { city: city.into() } },
};
let orders = [
order(1, 350, 2, "Rahim", "Dhaka"),
order(2, 650, 6, "Nadia", "Dhaka"),
order(3, 450, 9, "Tania", "Chattogram"),
order(4, 650, 6, "Sabbir", "Chattogram"),
];
for o in &orders {
let (fee, total) = price(o);
println!("{}", receipt(o, fee, total));
}
// The checkout screen and the refund job call the same rule, so they can't disagree.
println!("refund job, order 3 delivery: {} tk", delivery_fee(450, 9, "Chattogram"));
}import java.util.List;
public class Main {
record Address(String city) {}
record Customer(String name, Address address) {}
record Order(int id, int subtotal, int km, Customer customer) {
// Law of Demeter: callers ask the order instead of reaching through customer().address().city().
String deliveryCity() { return customer.address().city(); }
}
// DRY: the delivery fee rule lives in exactly one method.
// KISS: a plain method with three inputs; no strategy, no factory, no config file.
static int deliveryFee(int subtotal, int km, String city) {
if (city.equals("Dhaka") && subtotal >= 500) return 0; // free delivery in Dhaka from 500 taka
return 40 + Math.max(0, km - 3) * 10;
}
record Price(int fee, int total) {}
// Separation of concerns: pricing works out numbers...
static Price price(Order o) {
int fee = deliveryFee(o.subtotal(), o.km(), o.deliveryCity());
return new Price(fee, o.subtotal() + fee);
}
// ...and formatting turns numbers into text.
static String receipt(Order o, Price p) {
return String.format("order %d: %d tk + delivery %d tk = %d tk (%s, %d km)",
o.id(), o.subtotal(), p.fee(), p.total(), o.deliveryCity(), o.km());
}
public static void main(String[] args) {
Address dhaka = new Address("Dhaka"), ctg = new Address("Chattogram");
List<Order> orders = List.of(
new Order(1, 350, 2, new Customer("Rahim", dhaka)),
new Order(2, 650, 6, new Customer("Nadia", dhaka)),
new Order(3, 450, 9, new Customer("Tania", ctg)),
new Order(4, 650, 6, new Customer("Sabbir", ctg)));
for (Order o : orders) System.out.println(receipt(o, price(o)));
// The checkout screen and the refund job call the same rule, so they can't disagree.
System.out.println("refund job, order 3 delivery: " + deliveryFee(450, 9, "Chattogram") + " tk");
}
}#include <algorithm>
#include <cstdio>
#include <string>
#include <utility>
#include <vector>
struct Address {
std::string city;
};
struct Customer {
std::string name;
Address address;
};
class Order {
public:
Order(int id, int subtotal, int km, Customer c) : id(id), subtotal(subtotal), km(km), customer_(std::move(c)) {}
int id, subtotal, km; // subtotal in taka
// Law of Demeter: callers ask the order instead of reaching through customer.address.city.
const std::string& deliveryCity() const { return customer_.address.city; }
private:
Customer customer_;
};
// DRY: the delivery fee rule lives in exactly one function.
// KISS: a plain function with three inputs; no strategy, no factory, no config file.
int deliveryFee(int subtotal, int km, const std::string& city) {
if (city == "Dhaka" && subtotal >= 500) return 0; // free delivery in Dhaka from 500 taka
return 40 + std::max(0, km - 3) * 10;
}
// Separation of concerns: pricing works out numbers...
std::pair<int, int> price(const Order& o) {
int fee = deliveryFee(o.subtotal, o.km, o.deliveryCity());
return {fee, o.subtotal + fee};
}
// ...and formatting turns numbers into text.
std::string receipt(const Order& o, int fee, int total) {
char buf[128];
std::snprintf(buf, sizeof buf, "order %d: %d tk + delivery %d tk = %d tk (%s, %d km)", o.id, o.subtotal, fee,
total, o.deliveryCity().c_str(), o.km);
return buf;
}
int main() {
Address dhaka{"Dhaka"}, ctg{"Chattogram"};
std::vector<Order> orders{
{1, 350, 2, {"Rahim", dhaka}},
{2, 650, 6, {"Nadia", dhaka}},
{3, 450, 9, {"Tania", ctg}},
{4, 650, 6, {"Sabbir", ctg}},
};
for (const auto& o : orders) {
auto [fee, total] = price(o);
std::printf("%s\n", receipt(o, fee, total).c_str());
}
// The checkout screen and the refund job call the same rule, so they can't disagree.
std::printf("refund job, order 3 delivery: %d tk\n", deliveryFee(450, 9, "Chattogram"));
}ছয়টাই হুবহু এটা 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 করে
যে design টা পরে ভোগাবে
fee := 40 + max(0, km-3)*10 // partner_api.go
একটু থেমে ভাবো
তোমার 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 পাও →