এই পর্বে যা শিখবেন
- একটি অস্পষ্ট function-কে ধাপে ধাপে rename করে উদ্দেশ্য-প্রকাশক নাম দেওয়া, আর আচরণ না বদলে তা প্রমাণ করা
- Function-কে এক স্তরের abstraction-এ আনা: extract function, flag argument সরানো, command–query আলাদা রাখা
- Nested
if-কে guard clause-এ বদলানো, আর nesting depth মেপে পার্থক্য দেখা - কখন comment লিখবেন আর কখন নয়, docstring/JSDoc, TODO আর commented-out কোডের নিয়ম; Git দিয়ে মুছে ফেলা কোড ফিরে পাওয়া
- Formatter, magic number, error handling-এর লেখার ধরন আর clean code-এর নিয়ম যখন একে অপরের বিরুদ্ধে যায়, তখন বিচার করা
১ · Clean code মানে কী — পড়া সহজ, বদলানো সহজ
আগের তিনটি পর্বে (পর্ব ৩৮ software architecture, পর্ব ৩৯ MVC আর MVVM, পর্ব ৪০ clean আর hexagonal architecture) বড় ছবিটা দেখেছেন — system-কে স্তরে আর অংশে ভাগ করা। এবার আমরা নামব একেবারে নিচে, প্রতিদিনের লেখা লাইনে। নাম মিলে গেলেও গুলিয়ে ফেলবেন না: clean architecture হলো অংশগুলোর নির্ভরতার দিক নিয়ে একটি নকশা, আর clean code হলো প্রতিটি function, নাম আর লাইনের পাঠযোগ্যতা নিয়ে অভ্যাসের সমষ্টি।
Clean code-এর কোনো একটি সর্বসম্মত সংজ্ঞা নেই, কিন্তু প্রায় সব সংজ্ঞায় তিনটি কথা ফিরে ফিরে আসে:
📖 পড়া যায়
নতুন সহকর্মী কয়েক মিনিটে বোঝেন কোডটি কী করে।
🎯 উদ্দেশ্য বলে
নাম আর গঠন দেখেই বোঝা যায় লেখক কী চেয়েছিলেন — শুধু কম্পিউটার কী করবে তা নয়।
🔧 বদলানো সহজ
একটি নিয়ম বদলাতে এক জায়গায় হাত দিতে হয়।
কেন এত জোর? কারণ কোড লেখার চেয়ে পড়া হয় অনেক বেশি। Robert C. Martin তাঁর বই Clean Code: A Handbook of Agile Software Craftsmanship-এ (২০০৮) অনুমান করেন যে পড়া আর লেখার সময়ের অনুপাত ১০:১-এরও বেশি। এটি তাঁর অভিজ্ঞতার আন্দাজ, কোনো মাপা গবেষণার ফল নয় — তবু দিকটা প্রায় সবাই স্বীকার করেন: একটি লাইন একবার লেখা হয়, তারপর review-তে, bug খোঁজায়, নতুন feature যোগের সময়, বছরের পর বছর পড়া হয়। আরও পুরোনো একটি বাক্য আছে Abelson ও Sussman-এর Structure and Interpretation of Computer Programs (১৯৮৫) বইয়ের ভূমিকায়: program লেখা উচিত মানুষের পড়ার জন্য, আর শুধু প্রসঙ্গক্রমে যন্ত্রের চালানোর জন্য।
পরীক্ষার খাতার খসড়া আর ফেয়ার কপির কথা ভাবুন। খসড়ায় আপনি নিজের জন্য লেখেন — কাটাকাটি, তীর, “ক”, “খ” দিয়ে নিজের সংক্ষেপ। কিন্তু ফেয়ার কপি লেখেন পরীক্ষকের জন্য: প্রতিটি ধাপের শিরোনাম, পরিষ্কার হাতের লেখা, একই রকম মার্জিন। উত্তর একই, কিন্তু পরীক্ষক দ্বিতীয়টি তিন মিনিটে বোঝেন, প্রথমটি বুঝতে আধঘণ্টা লাগে — আর হয়তো ভুল বোঝেন। Clean code হলো কোডের ফেয়ার কপি, আর পরীক্ষক হলেন ছয় মাস পরের আপনি বা সহকর্মী।
পর্ব ০৩-এ (software quality) দেখেছেন, ISO/IEC 25010-এ maintainability-র উপ-গুণের মধ্যে আছে analysability আর modifiability — অর্থাৎ বোঝা আর বদলানোর সহজতা। Clean code এই দুটি গুণের হাতে-কলমে রূপ; আর পর্ব ২৬-এর (নকশার নীতি) নীতিগুলো এখানে লাইনের স্তরে প্রয়োগ হবে।
বইটি সম্পর্কে একটি ভারসাম্যপূর্ণ কথা। Martin-এর Clean Code এই বিষয়ের সবচেয়ে বেশি পড়া বইগুলোর একটি, আর এই পর্বের অনেক শব্দ সেখান থেকে জনপ্রিয় হয়েছে। কিন্তু সব পরামর্শ সর্বসম্মত নয় — বিশেষ করে “function খুব ছোট হবে”। John Ousterhout-এর A Philosophy of Software Design (২০১৮) উল্টো দিক থেকে “deep module”-এর যুক্তি দেয় (§৮, ভাবনার প্রশ্ন ১)। তাই বইটি এখানে গুরুত্বপূর্ণ সূত্র, ধর্মগ্রন্থ নয়।
পুরো পর্ব জুড়ে একটি উদাহরণ চলবে: একটি কোচিং সেন্টারের ভর্তি আর ফি-হিসাবের app (কাল্পনিক). শিক্ষার্থী online বা offline batch-এ কয়েক মাসের জন্য ভর্তি হন; ভর্তি পরীক্ষায় ৮০ বা বেশি পেলে মেধা-ছাড়, ছয় মাসের বেশি কোর্সে এককালীন ছাড়, দেরিতে ফি দিলে জরিমানা, আর নির্দিষ্ট শর্তে টাকা ফেরত।
২ · নাম — কোডের সবচেয়ে বড় documentation
Program-এ variable, function, class, module — যা কিছুর নাম দিই, সবই identifier. একটি প্রচলিত রসিকতা (সাধারণত Phil Karlton-এর নামে): computer science-এ কঠিন কাজ মাত্র দুটি — cache invalidation আর নাম দেওয়া। ভালো নাম দিতে হলে আগে বুঝতে হয় জিনিসটা আসলে কী।
| নিয়ম | খারাপ | ভালো | কেন |
|---|---|---|---|
| উদ্দেশ্য প্রকাশ করুন | d, data, tmp | days_since_admission | নামেই প্রশ্নের উত্তর: কী, কোন একক, কিসের সাপেক্ষে |
| খোঁজা যায় | 7, e | REFUND_WINDOW_DAYS | পুরো project-এ search করলে ঠিক জায়গাগুলোই আসে |
| Type-এর সংকেত নামে নয় | strName, lstMonths, iCount | name, months, count | Type বদলালে নাম মিথ্যা বলে; editor নিজেই type দেখায় |
| এক ধারণা, এক শব্দ | fetch_student, retrieve_batch | get_student, get_batch | আলাদা শব্দ দেখলে পাঠক ভাবেন কাজও আলাদা |
| একক নামে রাখুন | timeout, fee | timeout_ms, fee_tk | সেকেন্ড না মিলিসেকেন্ড, টাকা না পয়সা — অনুমানের জায়গা থাকে না |
একটি উদ্দেশ্য-প্রকাশক নাম (intention-revealing name) তিনটি প্রশ্নের উত্তর দেয়: জিনিসটা কেন আছে, কী করে, আর কীভাবে ব্যবহার হয়। নাম পড়ার পরেও comment লাগলে নামটা কাজ শেষ করেনি। নামের দৈর্ঘ্য তার পরিধির সাথে মানানসই: তিন লাইনের loop-এ i ঠিক, কিন্তু পুরো module-এ ব্যবহৃত মানের নাম i হলে দুশো লাইন পরে কেউ বুঝবেন না। এককের গুরুত্ব মনে করিয়ে দেয় পর্ব ০২-এর (software-এর ইতিহাস) Mars Climate Orbiter-এর ঘটনা।
Hungarian notation আর নামকরণের রীতি
Hungarian notation হলো নামের শুরুতে type-এর সংকেত বসানো: strName (string), iCount (integer), bIsPaid (boolean). রীতিটি Charles Simonyi-র নামে পরিচিত (তিনি হাঙ্গেরীয় বংশোদ্ভূত)। তখনকার editor type দেখাত না, তাই এর প্রয়োজন ছিল। আজ editor নিজেই type দেখায়, আর type বদলালে নামটি মিথ্যা হয়ে যায়; তাই বেশিরভাগ আধুনিক style guide এটি নিরুৎসাহিত করে (ইতিহাস আর একটি “ভালো” রূপ ভাবনার প্রশ্ন ৩-এ)।
Type-এর সংকেত না দিলেও প্রতিটি ভাষার একটি নামকরণের রীতি আছে, আর দলের সবাই সেটি মানলে কোড এক হাতের লেখা মনে হয়। Python-এ PEP 8 অনুযায়ী variable ও function snake_case, class PascalCase, constant UPPER_SNAKE_CASE; JavaScript-এ প্রচলিত রীতি camelCase, class PascalCase. Python-এর রীতি নিয়ে বিস্তারিত দেখুন Python কোর্সের Pythonic কোড পর্বে।
Boolean-এর নাম — প্রশ্নের মতো পড়া যায় এমন
Boolean মান সত্য বা মিথ্যা — তাই তার নাম একটি হ্যাঁ/না প্রশ্নের মতো হওয়া উচিত: is_paid, has_merit_discount, can_refund, should_send_sms. তখন if is_paid: একটি বাক্যের মতো পড়া যায়। flag, status, ok কিছুই বলে না — কিসের ok? নামে নেতিবাচকতাও এড়িয়ে চলুন: if not is_not_unpaid পড়লে মাথা ঘোরে।
ইংরেজি নাকি বাংলায় নাম?
অনেকে অবাক হন যে Python 3 আর JavaScript দুটিই Unicode অক্ষরে নাম দিতে দেয় — Python-এ এটি PEP 3131 থেকে এসেছে। অর্থাৎ মোট_ফি = 1200 * 3 লিখলে Python ঠিকঠাক চলে, আর JavaScript-এ let মোট_ফি = 3600-ও বৈধ। তবে বাস্তবে বেশিরভাগ দল — সাধারণত বাংলাদেশেও — Unicode identifier নয়, ইংরেজি নাম রাখে: library আর documentation ইংরেজিতে, সব tool-এ বাংলা টাইপ সমান সহজ নয়, বাইরের সহকর্মী পড়তে পারেন না, আর দেখতে-একই Unicode অক্ষর নিরাপত্তা-ঝুঁকিও আনতে পারে (ভাবনার প্রশ্ন ৫)। প্রচলিত মাঝের পথ: নাম ইংরেজিতে (zakat_amount, eid_bonus), ব্যবহারকারীকে দেখানো লেখা বাংলায় — আর সিদ্ধান্তটি দলের style guide-এ লেখা।
একটি নাম ধাপে ধাপে
নিচের stepper-এ একটি পুরোনো function-এর নাম একটি একটি করে বদলানো হচ্ছে; আচরণ একই থাকে। খেয়াল করুন function যেখানে ডাকা হয় (call site), সেখানে কী বদলায়।
def calc(d, m, f):
...
total = calc(12, 90, True) # 12 কী? 90 কী? True কী?
def calc(months, m, f):
...
total = calc(12, 90, True)
def calc(months, exam_marks, f):
...
total = calc(12, 90, True)
def calc(months, exam_marks, is_online): ... total = calc(12, 90, True) # call site-এ এখনো True
def calc(months, exam_marks, mode): # "online" | "offline" ... total = calc(12, 90, "online")
def course_fee(months, exam_marks, mode): ... total = course_fee(12, 90, "online")
| অস্পষ্ট নাম | |
| Call site পড়ে বোঝা যায় | |
| Test (৫টি ইনপুট) |
- শুরু।
calc(12, 90, True)— ১২ কি মাস না দিন? ৯০ কি নম্বর?Trueকিসের? পাঠককে ভেতরে ঢুকতে হয়। শুরুর আগে আচরণ আটকানোর test সবুজ কি না দেখুন। d→months. ভেতর পড়ে বোঝা গেলdআসলে কোর্সের মাস। বদলাতে editor-এর Rename Symbol (VS Code-এ সাধারণত F2), find-and-replace নয় — নাহলেadd,mode-এর ভেতরের d-ও বদলাবে।m→exam_marks.mহতে পারত months, minutes, marks বা money — চারটিই এই app-এ আছে! এখন নামেই উৎস: ভর্তি পরীক্ষার নম্বর। প্রতিটি rename-এর পরে test চলল।f→is_online. Boolean-এর নাম এখন প্রশ্নের মতো। কিন্তু call site-এ এখনোTrue, নামটা সেখানে দেখা যায় না। এই সমস্যার নাম flag argument (§৩)।- Flag → স্পষ্ট mode. Boolean-এর বদলে নাম-ওয়ালা মান
"online"/"offline"(বড় project-এEnumআরও নিরাপদ)। এটি interface-এর বদল, তাই সব call site একই commit-এ বদলানো, অথবা কিছুদিন দুই রূপ পাশাপাশি (পর্ব ৩১-এর parallel change). calc→course_fee. Function একটি মান ফেরত দেয় আর কিছু বদলায় না (query), তাই নাম সেই মানের নাম। এখনcourse_fee(12, 90, "online")ভেতরে না ঢুকেই পড়া যায়: ১২ মাসের, ৯০ নম্বর পাওয়া online শিক্ষার্থীর কোর্স ফি।
নামের নিয়মগুলো কিছুটা যন্ত্র দিয়েও যাচাই করা যায়। নিচের প্রোগ্রামটি একটি খেলনা linter — এমন tool যা কোড না চালিয়েই সন্দেহজনক ধরন খোঁজে। এটি Python-এর নিজস্ব ast module দিয়ে কোডকে abstract syntax tree-এ ভেঙে তিনটি নিয়ম দেখে: loop-এর বাইরে এক-অক্ষরের নাম, Hungarian-ধাঁচের type সংকেত, আর is_/has_/can_ ছাড়া boolean. এটি শুধু শেখার জন্য; আসল কাজে Pylint-এর invalid-name বা Ruff-এর pep8-naming নিয়মের মতো tool ব্যবহার হয়, আর linter-এর পুরো আলোচনা পর্ব ৪৪-এ (code review).
# A toy naming linter (for teaching only, not a real tool) using Python's ast.
import ast, re
SOURCE = '''
def calc(d, m, flag=True):
strName = "fee"
ok = m >= 80
for i in range(3):
t = d * i
is_merit = m >= 80
'''
HUNGARIAN = re.compile(r"^(str|int|lst|arr|dict|obj|sz|b|n)[A-Z]")
BOOL_PREFIXES = ("is_", "has_", "can_", "should_")
def looks_boolean(value): # a comparison like m >= 80, or True/False
return isinstance(value, ast.Compare) or type(getattr(value, "value", None)) is bool
def named_things(tree): # yields (name, line, value or None)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
args, defaults = node.args.args, node.args.defaults
padded = [None] * (len(args) - len(defaults)) + defaults
yield from ((arg.arg, arg.lineno, default) for arg, default in zip(args, padded))
elif isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name):
yield node.targets[0].id, node.lineno, node.value
tree = ast.parse(SOURCE)
loop_vars = {node.target.id for node in ast.walk(tree) if isinstance(node, ast.For)}
warnings = []
for name, line, value in named_things(tree):
if len(name) == 1 and name not in loop_vars:
warnings.append((line, name, "single-letter name (not a loop variable)"))
if HUNGARIAN.match(name):
warnings.append((line, name, "Hungarian-style type prefix"))
if value is not None and looks_boolean(value) and not name.startswith(BOOL_PREFIXES):
warnings.append((line, name, "boolean without is_/has_/can_ prefix"))
for line, name, why in sorted(warnings):
print(f"line {line}: {name!r} - {why}")
print(f"{len(warnings)} warning(s)")আউটপুট:
line 2: 'd' - single-letter name (not a loop variable)
line 2: 'flag' - boolean without is_/has_/can_ prefix
line 2: 'm' - single-letter name (not a loop variable)
line 3: 'strName' - Hungarian-style type prefix
line 4: 'ok' - boolean without is_/has_/can_ prefix
line 6: 't' - single-letter name (not a loop variable)
6 warning(s)
SOURCE-এর প্রথম লাইনটি ফাঁকা, তাই def লাইন ২-এ। Parameter d আর m এক অক্ষরের; flag-এর default True, তাই এটি boolean অথচ নামে প্রশ্ন নেই। strName-এর শুরুতে type-সংকেত str আর তারপর বড় হাতের অক্ষর। ok = m >= 80 একটি তুলনা, অর্থাৎ boolean, কিন্তু “ok” কিসের? খেয়াল করুন i ধরা পড়েনি — এটি loop variable, তিন লাইনে শেষ, তাই এক অক্ষর ঠিক আছে। কিন্তু loop-এর ভেতরে assign হওয়া t loop variable নয়, তাই ধরা পড়েছে। is_merit সব নিয়ম মানে। সীমাও দেখুন: calc নামটি যাচাই হয়নি, আর নাম সত্যিই অর্থবহ কি না যন্ত্র বলতে পারে না — গণিতের কোডে x, y স্বাভাবিক। Linter মনে করিয়ে দেয়, বিচার করে মানুষ।
৩ · Function — এক কাজ, এক স্তর, কম parameter
Function হলো কোডের বাক্য: একটি ভালো function একটি কাজ করে। “একটি কাজ” চেনার পরীক্ষা: ভেতরের প্রতিটি লাইন যদি নামের ঠিক এক ধাপ নিচের কথা বলে, তবে সেটি একটি কাজ। এটিই এক স্তরের abstraction (one level of abstraction per function). “রসিদ ছাপানো” আর “৩২টি ড্যাশের লাইন আঁকা” এক function-এ থাকলে পাঠককে স্তরের মধ্যে ওঠানামা করতে হয়। এটি SRP থেকে আলাদা কথা: SRP প্রশ্ন করে কোন মানুষ বা দল-এর কারণে module বদলায় (পর্ব ২৭, single responsibility), আর এখানে প্রশ্ন হলো একটি function পড়তে কতটা সহজ।
বিয়েবাড়ির আয়োজনের তালিকা ভাবুন। প্রথম পাতায়: “হলুদ — বৃহস্পতিবার, বিয়ে — শুক্রবার, বৌভাত — রবিবার।” বিয়ের পাতায় “কাজি, বাবুর্চি, ডেকোরেটর”, আর বাবুর্চির পাতায় “চাল ৫০ কেজি”। প্রথম পাতাতেই “চাল ৫০ কেজি” আর ডেকোরেটরের ফোন নম্বর মেশালে তালিকা ঠিক থাকে, কিন্তু পুরো ছবি হারায়। Function-ও তেমন — উপরের function পড়ে পুরো গল্প, নিচের function-এ খুঁটিনাটি। Martin একে বলেন stepdown rule: file উপর থেকে নিচে পড়লে প্রতিটি function-এর পরে তার ডাকা function আসে, গল্প এক ধাপ করে নামে।
এক স্তরে আনার প্রধান হাতিয়ার extract function — কয়েকটি লাইন তুলে নিয়ে একটি নাম-ওয়ালা নতুন function বানানো, আর আগের জায়গায় সেই নাম ডাকা। Fowler-এর Refactoring (২য় সংস্করণ ২০১৮) বইয়ের সবচেয়ে বেশি ব্যবহৃত refactoring-গুলোর একটি এটি। নিচের stepper-এ কোচিং সেন্টারের ভর্তির রসিদ ছাপার function ধাপে ধাপে এক স্তরে আসছে।
def print_admission_slip(student):
if not re.fullmatch(r"01[3-9]\d{8}", student["phone"]):
raise ValueError(f"invalid phone: {student['phone']!r}")
fee = student["months"] * 1500
if student["marks"] >= 80:
fee -= fee * 0.10
line = "-" * 32
print(line)
print(student["name"].title().ljust(20) + f"{fee:>10.0f}")
print(line)
def print_admission_slip(student):
if not re.fullmatch(...): # ক: phone যাচাই
raise ValueError(...)
fee = student["months"] * 1500 # খ: ফি হিসাব
if student["marks"] >= 80:
fee -= fee * 0.10
line = "-" * 32 # গ: অক্ষর সাজানো
print(line) ... print(line)
def print_admission_slip(student):
require_valid_phone(student["phone"])
fee = student["months"] * 1500
if student["marks"] >= 80:
fee -= fee * 0.10
line = "-" * 32
print(line)
print(student["name"].title().ljust(20) + f"{fee:>10.0f}")
print(line)
def print_admission_slip(student):
require_valid_phone(student["phone"])
fee = tuition_fee(student["months"], student["marks"])
line = "-" * 32
print(line)
print(student["name"].title().ljust(20) + f"{fee:>10.0f}")
print(line)
def print_admission_slip(student):
require_valid_phone(student["phone"])
fee = tuition_fee(student["months"], student["marks"])
print(format_slip(student["name"], fee))
def require_valid_phone(phone): ... # নিচে, ব্যবহারের ক্রমে
def tuition_fee(months, exam_marks): ...
def format_slip(name, fee): ... # লেখা ফেরত দেয়, ছাপে না
| ভেতরের লাইন | |
| মিশে থাকা স্তর | |
| আলাদাভাবে test করা যায় |
- তিন স্তর মিশে আছে। নয় লাইনের function-এ তিনটি আলাদা কাজ: phone নম্বরের নিয়ম (বাংলাদেশি মোবাইল নম্বরের মতো ১১ অঙ্কের একটি সরল ছাঁচ), ফি-এর হিসাব, আর অক্ষর সাজিয়ে ছাপা। কোনোটিই print ছাড়া আলাদাভাবে test করা যায় না; তার ওপর নামহীন সংখ্যা (§৭)।
- স্তর চিহ্নিত করা। কিছু বদলানোর আগে শুধু পড়ে দাগ দেওয়া: ক অংশ যাচাই, খ অংশ হিসাব, গ অংশ উপস্থাপন। নাম খুঁজে না পেলে অংশটি হয়তো ঠিকভাবে ভাগ হয়নি।
- phone যাচাই বের করা। দুটি লাইন গেল
require_valid_phone(phone)-এ; নামটি বলে “বৈধ না হলে থামবে” (fail fast, পর্ব ২৬)। মূল function-এ এখন ৮ লাইন। এর জন্য এখন আলাদা ছোট test লেখা যায়। - ফি হিসাব বের করা। তিনটি লাইন গেল
tuition_fee(months, exam_marks)-এ। মূল কোড শুধু ১৫০০ টাকার হার আর ১০% ছাড় জানত, তাই নতুন function-ও তাই করে — refactoring-এর সময় নতুন feature নয়। মূল function এখন ৬ লাইন। - এক স্তর আর stepdown. অক্ষর সাজানোর চারটি লাইন গেল
format_slip(name, fee)-এ, যা একটি string ফেরত দেয়, নিজে ছাপে না — তাই test-এ ফেরত লেখা মিলিয়ে দেখা যায় (command আর query আলাদা)। মূল function এখন তিন লাইনের একটি সূচিপত্র: যাচাই, হিসাব, ছাপা। সাহায্যকারী function-গুলো নিচে, ডাকার ক্রমে। এখানে থামাই যুক্তিসঙ্গত (§৮)।
Parameter — যত কম, তত ভালো; আর flag argument নয়
প্রতিটি parameter পাঠকের মাথায় একটি বাড়তি জিনিস। দুটি পর্যন্ত সহজ; তিনটিতে ক্রম মনে রাখা কঠিন হয়; চার-পাঁচটি সাধারণত বোঝায় কয়েকটি মিলে একটি ধারণা — তখন একটি object বা dataclass-এ জুড়ে দিন (Enrollment(months, exam_marks, mode)), অথবা Python-এ keyword argument বাধ্যতামূলক করুন (def course_fee(*, months, exam_marks, mode)), যাতে call site-এ নাম লিখতেই হয়।
সবচেয়ে খারাপ parameter হলো flag argument — একটি boolean, যা function-কে দুই রকম কাজ করতে বলে। render(report, True) পড়ে কেউ বলতে পারেন না True মানে কী। আর ঘোষণাতেই স্বীকার করা হলো যে function একাধিক কাজ করে: “flag সত্য হলে এটা, মিথ্যা হলে ওটা”। প্রতিকার: দুটি function (render_pdf, render_html), একটি Enum (render(report, Format.PDF)), অথবা অন্তত keyword argument (as_pdf=True) — Fowler-এর ক্যাটালগে “Remove Flag Argument”.
লুকানো side effect আর command–query separation
পর্ব ২৬-এ (নকশার নীতি) command–query separation দেখেছেন: method হয় অবস্থা বদলাবে, নয়তো প্রশ্নের উত্তর দেবে। Clean code-এর দিক থেকে এর সবচেয়ে বড় শত্রু হলো নামে লুকানো side effect. is_valid_pin(pin) নাম শুনে সবাই ধরে নেন এটি শুধু হ্যাঁ/না বলবে; ভেতরে এটি চেষ্টাও গুনলে log-এ একবার ডাকলেই আচরণ বদলাবে (অনুশীলন ১)। নিয়ম: নাম যা বলে, function ঠিক তা-ই করবে।
এবার সব মিলিয়ে: একটি পুরোনো ফি-হিসাবের function (অস্পষ্ট নাম, flag argument, nested if, নামহীন সংখ্যা) আর তার পরিষ্কার রূপ, পাঁচটি ইনপুটে পাশাপাশি — refactoring মানে আচরণ একই রেখে গঠন বদলানো।
# Fictional coaching-centre course fee. BEFORE: unclear names, a flag
# argument, nested ifs and magic numbers. AFTER: the same rules, cleaned.
def calc(d, m, f):
t = 0
if d > 0:
if f:
t = d * 1200
if m >= 80:
t = t - t * 0.15
else:
t = d * 1500
if m >= 80:
t = t - t * 0.10
if d > 6:
t = t - 500
return round(t)
MONTHLY_FEE_TK = {"online": 1200, "offline": 1500}
MERIT_DISCOUNT = {"online": 0.15, "offline": 0.10}
MERIT_MIN_MARKS, LONG_COURSE_MONTHS, LONG_COURSE_REBATE_TK = 80, 6, 500
def course_fee(months, exam_marks, mode):
if months <= 0:
return 0
fee = months * MONTHLY_FEE_TK[mode]
if exam_marks >= MERIT_MIN_MARKS:
fee -= fee * MERIT_DISCOUNT[mode]
if months > LONG_COURSE_MONTHS:
fee -= LONG_COURSE_REBATE_TK
return round(fee)
CASES = [(3, 70, True), (3, 85, False), (12, 90, True), (0, 95, False), (8, 60, False)]
for months, marks, online in CASES:
mode = "online" if online else "offline"
old, new = calc(months, marks, online), course_fee(months, marks, mode)
print(f"{months:>2} mo, {marks} marks, {mode:<7}: old {old:>5}, new {new:>5}",
"same" if old == new else "DIFFERENT")আউটপুট:
3 mo, 70 marks, online : old 3600, new 3600 same
3 mo, 85 marks, offline: old 4050, new 4050 same
12 mo, 90 marks, online : old 11740, new 11740 same
0 mo, 95 marks, offline: old 0, new 0 same
8 mo, 60 marks, offline: old 11500, new 11500 same
পাঁচটি ইনপুটেই দুটি function একই মান দেয়। হাতে মিলিয়ে নিন: ৩ মাস online, ৭০ নম্বর — ৩ × ১২০০ = ৩৬০০, মেধা-ছাড় নেই (৭০ < ৮০), দীর্ঘ কোর্স নয়। ৩ মাস offline, ৮৫ নম্বর — ৪৫০০ থেকে ১০% বাদ, ৪০৫০. ১২ মাস online, ৯০ নম্বর — ১৪৪০০ থেকে ১৫% (২১৬০) বাদে ১২২৪০, তারপর ছয় মাসের বেশি বলে আরও ৫০০ বাদ, ১১৭৪০. ০ মাস — ০. ৮ মাস offline, ৬০ নম্বর — ১২০০০ − ৫০০ = ১১৫০০. কী বদলেছে? নাম নিয়মের ভাষায়; online/offline-এর দুটি প্রায়-একই শাখা মিলে একটি পথ, পার্থক্য শুধু দুটি টেবিলে; প্রতিটি সংখ্যার নাম; আর months <= 0 শুরুতেই একটি guard clause (§৪)। একটি সূক্ষ্ম বোনাস: অজানা mode (যেমন "onlnie") দিলে নতুন কোড KeyError তুলে থামে, অথচ পুরোনো কোডে যেকোনো falsy মান চুপচাপ offline ধরা হতো।
৪ · Nesting কমানো — guard clause আর early return
একটি if-এর ভেতরে আরেকটি if, তার ভেতরে আরেকটি — কোড ক্রমশ ডানে সরে যায়, পাশ থেকে দেখতে তীরের মাথার মতো। একে প্রায়ই বলা হয় arrow code. কতগুলো ব্লক একটির ভেতরে আরেকটি আছে, তার মাপ হলো nesting depth. Depth যত বেশি, পাঠককে তত বেশি শর্ত একসাথে মাথায় রাখতে হয়, আর else তার if থেকে দূরে সরে যায়।
সমাধান হলো guard clause: যে অবস্থায় কাজ করা যাবে না, function-এর শুরুতেই সেটা যাচাই করে বেরিয়ে যান (early return). সব পাহারা পার হলে বাকি কোড মূল কাজ — happy path — সোজা বাঁ দিক ঘেঁষে লেখা থাকে। Fowler-এর ক্যাটালগে এর নাম “Replace Nested Conditional with Guard Clauses”.
একই নিয়ম, দুই আকার — nested বনাম guard clause
Nesting depth: arrow-shaped nested ifs vs a flat list of guard clauses
if-এ: সুখী পথটি (সবুজ) সবচেয়ে গভীরে, আর প্রত্যাখ্যানগুলো উল্টো ক্রমে নিচে জমা হয় — প্রথম শর্তের else সবার শেষে। ডান দিকে একই নিয়ম চারটি guard clause-এ: প্রতিটি শর্ত আর তার ফল এক লাইনে, আর সুখী পথ শেষে, কোনো ব্লকের ভেতরে নয়।নিচের প্রোগ্রামে ঠিক এই দুটি রূপ। একটি ছোট function নিজের source লেখা পড়ে { আর } গুনে nesting depth মাপে, আর পাঁচটি অনুরোধে দুটি রূপের উত্তর মিলিয়ে দেখে।
// Same refund rules, two shapes. Fictional coaching centre.
function refundNested(req) {
if (req) {
if (req.paid) {
if (req.days <= 7) {
if (req.classes < 3) {
return "full refund";
} else return "rejected: attended " + req.classes + " classes";
} else return "rejected: asked after 7 days";
} else return "rejected: nothing paid";
} else return "rejected: no request";
}
function refundGuarded(req) {
if (!req) return "rejected: no request";
if (!req.paid) return "rejected: nothing paid";
if (req.days > 7) return "rejected: asked after 7 days";
if (req.classes >= 3) return "rejected: attended " + req.classes + " classes";
return "full refund";
}
function nestingDepth(fn) { // deepest { } block inside the body
let depth = 0, deepest = 0;
for (const ch of fn.toString()) {
if (ch === "{") deepest = Math.max(deepest, ++depth);
if (ch === "}") depth--;
}
return deepest - 1; // the body's own { } does not count
}
const requests = [null, { paid: false, days: 1, classes: 0 },
{ paid: true, days: 10, classes: 0 }, { paid: true, days: 3, classes: 5 },
{ paid: true, days: 2, classes: 1 }];
for (const r of requests)
console.log((refundNested(r) === refundGuarded(r) ? "same " : "DIFF ") + refundGuarded(r));
for (const fn of [refundNested, refundGuarded])
console.log(`${fn.name}: depth ${nestingDepth(fn)}, ${fn.toString().split("\n").length} lines`);আউটপুট:
same rejected: no request
same rejected: nothing paid
same rejected: asked after 7 days
same rejected: attended 5 classes
same full refund
refundNested: depth 4, 11 lines
refundGuarded: depth 0, 7 lines
পাঁচটি অনুরোধে প্রতিটি শাখা অন্তত একবার চলে, আর দুই রূপ একই উত্তর দেয়। Nested রূপে সবচেয়ে ভেতরের return "full refund" চারটি if-ব্লকের ভেতরে, তাই depth ৪; guard রূপে কোনো ব্লকই নেই, depth ০, আর লাইন ১১ থেকে ৭. শেষ guard-এ শর্ত উল্টেছে (<-এর উল্টো >=, > নয়) — এই ধাপটিই সবচেয়ে ভুল-প্রবণ, তাই সীমার মান (ঠিক ৩টি ক্লাস, ঠিক ৭ দিন) দিয়ে test রাখুন। Brace-গোনা মাপটি খেলনা: string-এর ভেতরে { থাকলে ভুল গুনবে। আসল tool-এ nesting-কে গুরুত্ব দেওয়া মাপ হলো cognitive complexity; আর শাখার সংখ্যা মাপে cyclomatic complexity — দুটি রূপেই এখানে সমান, কারণ শর্তের সংখ্যা একই। পার্থক্যটা পড়ার কষ্টে, শাখায় নয়; এই মাপগুলোর হিসাব পর্ব ৫০-এ।
এক লাফে পুরো কাঠামো বদলানো ঝুঁকিপূর্ণ। নিচের stepper-এ একই refactoring ছোট ধাপে — প্রতি ধাপে একটি স্তর সরে, আর প্রতি ধাপে একই পাঁচটি অনুরোধে test চলে।
if (req) {
if (req.paid) {
if (req.days <= 7) {
if (req.classes < 3) {
return "full refund";
} else return "rejected: attended …";
} else return "rejected: asked after 7 days";
} else return "rejected: nothing paid";
} else return "rejected: no request";
if (!req) return "rejected: no request";
if (req.paid) {
if (req.days <= 7) {
if (req.classes < 3) {
return "full refund";
} else return "rejected: attended …";
} else return "rejected: asked after 7 days";
} else return "rejected: nothing paid";
if (!req) return "rejected: no request";
if (!req.paid) return "rejected: nothing paid";
if (req.days <= 7) {
if (req.classes < 3) {
return "full refund";
} else return "rejected: attended …";
} else return "rejected: asked after 7 days";
if (!req) return "rejected: no request";
if (!req.paid) return "rejected: nothing paid";
if (req.days > 7) return "rejected: asked after 7 days";
if (req.classes < 3) {
return "full refund";
} else return "rejected: attended …";
if (!req) return "rejected: no request"; if (!req.paid) return "rejected: nothing paid"; if (req.days > 7) return "rejected: asked after 7 days"; if (req.classes >= 3) return "rejected: attended …"; return "full refund";
| Nesting depth | |
| লাইন | |
| ৫টি অনুরোধে আগের মতো উত্তর |
- শুরু। চারটি nested
if, depth ৪, ১১ লাইন। প্রথম শর্তের ফল (no request) একেবারে শেষ লাইনে। পাঁচটি অনুরোধের test সবুজ। - বাইরের শর্ত উল্টে guard.
if (req) {…} else return X-কে লেখা হলোif (!req) return X;আর বাকি অংশ এক ধাপ বাঁয়ে সরল। শর্ত উল্টানোর সময়else-এর ফলটি guard-এর সাথে যায়। Depth ৩, লাইন ১০, test সবুজ। - paid-এর guard. একই কৌশল:
if (!req.paid) return "rejected: nothing paid";. Depth ২, লাইন ৯. Guard-গুলোর ক্রমই নিয়মের ক্রম। - সময়ের guard.
days <= 7-এর উল্টোdays > 7(≤-এর উল্টো >, ≥ নয়)। ঠিক ৭ দিনের একটি test থাকলে এখানে ভুল করলে সঙ্গে সঙ্গে ধরা পড়ত। Depth ১, লাইন ৮। - শেষ শর্ত আর happy path. শেষ
if/else-ও guard হলো, শর্ত উল্টে:classes >= 3হলে প্রত্যাখ্যান। এখন সুখী পথ —return "full refund"— শেষ লাইনে, কোনো ব্লকের ভেতরে নয়। Depth ০, লাইন ৭, পাঁচটি test সবুজ। প্রতিটি ধাপ আলাদা commit হলে কিছু ভাঙলে ঠিক কোন ধাপে, তা বোঝা যায়।
“একটি function-এ একটিই return” নিয়মের কী হলো? Structured programming-এর যুগে (পর্ব ০২) “single entry, single exit” জনপ্রিয় ছিল, আর কিছু কঠোর standard (যেমন embedded C-এর জন্য MISRA C) আজও এমন নিয়ম রাখে, কারণ C-তে মাঝপথে return করলে memory বা file ছাড়ার কাজ বাদ পড়তে পারে। যেসব ভাষায় try/finally, with, defer বা স্বয়ংক্রিয় memory ব্যবস্থাপনা আছে, সেখানে guard clause সাধারণত বেশি পরিষ্কার। কিন্তু function-এর মাঝখানে লুকানো দশটি return পাঠকের জন্য আবার কষ্টকর — guard শুরুতে, মূল কাজ পরে, এই ছাঁচটি রাখুন।
৫ · Comment — কোড বলে “কী”, comment বলে “কেন”
Martin Clean Code-এ প্রায় এভাবে লেখেন: comment হলো কোডে নিজের কথা প্রকাশ করতে না পারার ক্ষতিপূরণ। Ousterhout-এর মতো অন্যরা বলেন, কোড কখনো সব বলতে পারে না — কেন এই সিদ্ধান্ত, interface-এর প্রতিশ্রুতি কী — এগুলো লেখাই নকশার অংশ। দুই মতের সাধারণ ভিত্তি: যে comment কোডের কথা আবার বলে, সেটি অপ্রয়োজনীয়; যে comment কোডে যা বলা যায় না তা বলে, সেটি মূল্যবান।
| ধরন | উদাহরণ | সিদ্ধান্ত |
|---|---|---|
| কোডের প্রতিধ্বনি | i += 1 # i এক বাড়াও | মুছে ফেলুন — কিছুই যোগ করে না |
| “কেন” — সিদ্ধান্তের কারণ | # ২০২৬-এর বোর্ড সিদ্ধান্ত: ছাড় পুরো টাকায় নিচে গোল করা | রাখুন — কোড এটা বলতে পারে না |
| সতর্কতা | # এই ক্রম বদলাবেন না: ব্যাংক আগে fee, পরে VAT চায় | রাখুন |
| Commented-out কোড | # if marks >= 70: ... | মুছে ফেলুন — Git মনে রাখে (নিচে) |
| পুরোনো, মিথ্যা comment | # ১০% ছাড় অথচ কোডে 0.12 | সবচেয়ে বিপজ্জনক; কোড বদলালে comment-ও একই commit-এ |
শেষ সারিটি মনে রাখুন: compiler আর test comment যাচাই করে না, তাই কোড বদলায়, comment পড়ে থাকে, একদিন মিথ্যা বলে।
Docstring আর JSDoc — interface-এর লিখিত প্রতিশ্রুতি
একটি ব্যতিক্রম আছে যেখানে প্রায় সবাই একমত: public function, class আর module-এর documentation. Python-এ এটি docstring — function-এর প্রথম লাইনে একটি string, যা help() আর editor দেখায় (রীতিনীতি PEP 257-এ)। JavaScript-এ প্রচলিত রূপ JSDoc — /** … */ comment, যা editor আর TypeScript-এর type-checker-ও পড়তে পারে। এগুলো “কীভাবে” নয়, “কী প্রতিশ্রুতি” বলে: কী নেয়, কী ফেরত দেয়, কখন কোন error তোলে — অর্থাৎ পর্ব ৩১-এর (interface আর contract) contract-এর মানুষের-পড়ার-মতো রূপ।
def course_fee(months: int, exam_marks: int, mode: str) -> int:
"""Return the total course fee in taka, rounded to whole taka.
Merit discount applies at 80 marks or more; courses longer than
6 months get a one-time 500 tk rebate. Raises KeyError for an
unknown mode ("online" and "offline" are valid).
>>> course_fee(3, 85, "offline")
4050
"""/**
* Decide a refund request (fictional coaching centre).
* @param {{paid: boolean, days: number, classes: number} | null} req
* @returns {string} "full refund" or a reason starting with "rejected:"
*/
function refundGuarded(req) { /* … */ }Docstring-এর >>> লাইনগুলো শুধু উদাহরণ নয় — Python-এর doctest module এগুলো চালিয়ে মিলিয়ে দেখতে পারে। অর্থাৎ documentation-এর একটি অংশ মিথ্যা হলে test লাল হয়। Comment-কে সত্য রাখার এটিই সবচেয়ে ভালো উপায়: যতটা সম্ভব comment-কে যাচাইযোগ্য করা — type hint, doctest, assertion.
TODO-র নিয়ম
TODO comment (# TODO: …) হলো ভবিষ্যতের কাজের চিরকুট। সমস্যা হলো, চিরকুট জমতে জমতে কোড-ভিত্তিতে শত শত TODO পড়ে থাকে, যার মালিক নেই, তারিখ নেই, আর কেউ জানে না এখনো প্রাসঙ্গিক কি না — এটি নীরব technical debt (পর্ব ৪৩, technical debt আর legacy code). অভ্যাস: TODO-তে issue নম্বর বা মালিক রাখুন (# TODO(#142): …), নিয়মিত তালিকা দেখুন, আর যে TODO কখনো হবে না তা মুছুন। অনেক দল linter দিয়ে issue নম্বর ছাড়া TODO আটকায়।
Commented-out কোড — Git মনে রাখে
“পরে লাগতে পারে” ভেবে কোড # দিয়ে ঢেকে রাখা খুব সাধারণ। ক্ষতিটা নীরব: পরের পাঠক জানেন না এটি কেন আছে, ভয়ে কেউ মোছেন না, search-এ আসল কোডের সাথে মিশে আসে, আর চারপাশ বদলে যাওয়ায় uncomment করলে চলেও না। পুরোনো কোড রাখার আসল জায়গা হলো version control. পর্ব ৯-এ (history আর পার্থক্য দেখা) log আর show দেখেছেন; এখানে একটি নতুন option: git log -S "লেখা", যা দেখায় কোন commit-এ ওই লেখাটি যোগ হয়েছে বা মুছেছে (একে বলে pickaxe খোঁজ; বিস্তারিত পর্ব ৫৫-এ)। নিচে একটি কাল্পনিক repository-তে পুরোনো ঈদ-অফারের commented-out কোড প্রথমে রাখা, তারপর মুছে ফেলা, আর শেষে খুঁজে বের করা হয়েছে:
$ git log --oneline
6851b01 refactor: remove commented-out Eid offer
314837b chore: park the old Eid offer
8fd7f26 feat: add merit discount
$ git log -S "old_eid_offer" --oneline
6851b01 refactor: remove commented-out Eid offer
314837b chore: park the old Eid offer
$ git show 314837b:fee.py
MERIT_DISCOUNT = 0.10
def merit_discount(fee, exam_marks):
# old_eid_offer: Eid offer, keep just in case
# if exam_marks >= 70:
# return fee * 0.20
return fee * MERIT_DISCOUNT if exam_marks >= 80 else 0Repository-টি এই পর্বের জন্য নির্দিষ্ট লেখক (Rahim) আর নির্দিষ্ট তারিখ দিয়ে বানানো, তাই একই ধাপে আপনার hash-ও হুবহু এক হবে। git log -S "old_eid_offer" শুধু সেই commit দেখায় যেখানে লেখাটির সংখ্যা বদলেছে: 314837b-এ যোগ হয়েছিল, 6851b01-এ মুছেছে; প্রথম commit 8fd7f26 তালিকায় নেই, কারণ সেখানে লেখাটি ছিলই না। তারপর git show 314837b:fee.py ওই commit-এর সময়ের পুরো file দেখায়, working tree না বদলিয়ে। অর্থাৎ কোড মুছে ফেললেও হারায় না — শর্ত একটাই, সেটি কখনো commit হয়েছিল। ভালো commit message-ও (পর্ব ১৮, ভালো commit) এমন খোঁজ সহজ করে।
৬ · Formatting — তর্ক নয়, স্বয়ংক্রিয় formatter
Formatting মানে কোডের চেহারা: indentation, space, বন্ধনী, লাইনের দৈর্ঘ্য। আচরণে প্রভাব নেই (Python-এর indentation বাদে), কিন্তু পড়ার গতিতে বিশাল প্রভাব — আর ভিন্ন হাতের ছাপে প্রতিটি diff-এ space-এর বদল মিশে যায়।
মুদি দোকানের হিসাবের খাতা ভাবুন: তিন কর্মচারী তিনভাবে তারিখ আর টাকা লেখেন। হিসাব ঠিক, কিন্তু মাস শেষে মালিক প্রতি পাতায় খুঁজে বেড়ান কোথায় কী। ছক-কাটা ছাপানো খাতা এই সমস্যা মুছে দেয়। স্বয়ংক্রিয় formatter হলো কোডের ছাপানো খাতা।
Code formatter এমন tool, যা কোড পড়ে একটি নির্দিষ্ট নিয়মে আবার লিখে দেয় — আচরণ না বদলে। Style guide হলো মানুষের জন্য লেখা নিয়ম; formatter সেই নিয়মের যে অংশ যন্ত্রে প্রয়োগ করা যায়, তা করে ফেলে।
| Tool | ভাষা | লক্ষণীয় |
|---|---|---|
gofmt | Go | ভাষার সাথেই আসে (Go প্রকাশিত হয় ২০০৯ সালে); প্রায় কোনো option নেই, তাই Go-র প্রায় সব কোড দেখতে এক রকম |
| Black | Python | ২০১৮ সালের দিকে প্রকাশিত, নিজেকে “uncompromising” বলে; লেখার সময়ে ডিফল্ট লাইন-দৈর্ঘ্য ৮৮ |
| Prettier | JavaScript, TypeScript, CSS, HTML, JSON… | ২০১৭ সালের দিকে জনপ্রিয় হয়; লেখার সময়ে ডিফল্ট লাইন-প্রস্থ ৮০, কয়েকটি মাত্র option |
rustfmt, clang-format | Rust; C/C++ | একই ধারণা; clang-format-এ অনেক option, তাই দলকে একটি config বেছে নিতে হয় |
Go সম্প্রদায়ের একটি প্রচলিত প্রবাদ (সাধারণত Rob Pike-এর নামে) এই ধারণার সারকথা: gofmt-এর style কারও প্রিয় নয়, তবু gofmt সবার প্রিয়। কারণ formatter-এর আসল লাভ সুন্দর চেহারা নয় — তর্ক শেষ হওয়া. Review-তে space নিয়ে সময় নষ্ট হয় না, diff-এ শুধু আসল পরিবর্তন থাকে। Formatter save-এর সময় চলুক, আর CI-তেও (পর্ব ৫৮) যাচাই হোক।
Formatter যা পারে না — লাইন-দৈর্ঘ্য আর উল্লম্ব খোলামেলা ভাব
- লাইনের দৈর্ঘ্য: PEP 8 ৭৯ অক্ষর বলে, Black ৮৮, অনেক দল ১০০ বা ১২০. একটি সংখ্যা বেছে সবাই মানাই আসল; খুব লম্বা লাইন প্রায়ই বোঝায় লাইনটি বেশি কাজ করছে।
- উল্লম্ব খোলামেলা ভাব (vertical openness): একটি ধারণা শেষ হলে একটি ফাঁকা লাইন — অনুচ্ছেদের মতো। যাচাই, হিসাব, ফল ছাপা — তিনটি অংশ তিনটি “অনুচ্ছেদ”।
- উল্লম্ব ঘনত্ব: যে লাইনগুলো একসাথে কাজ করে, সেগুলো কাছাকাছি; variable ঘোষণা করুন যেখানে প্রথম লাগে, file-এর মাথায় সব জমিয়ে নয়। সম্পর্কিত function কাছাকাছি, ডাকার ক্রমে (stepdown rule, §৩)।
- File-এর আকার: একটি file একটি বিষয়। কয়েক হাজার লাইনের file প্রায় সবসময় কয়েকটি আলাদা ধারণা একসাথে বহন করে (§৮)।
দলে প্রথমবার formatter চালু করছেন? পুরো code-ভিত্তি একবারে format করুন, একটি আলাদা commit-এ, কোনো আচরণ-পরিবর্তন ছাড়া — feature-এর pull request-এর ভেতরে নয় (পর্ব ১৬, pull request). নাহলে reviewer হাজার লাইনের space-পরিবর্তনের মধ্যে দুটি আসল লাইন খুঁজবেন। আর সেই বড় commit-এর কারণে git blame যাতে প্রতিটি লাইনের লেখক হিসেবে formatter-এর commit না দেখায়, তার উপায় ভাবনার প্রশ্ন ৪-এ।
৭ · Magic number আর error handling-এর লেখার ধরন
Magic number — নামহীন সংখ্যা
কোডের মাঝখানে হঠাৎ 1500, 0.10, 7 — এগুলোকে বলে magic number: সংখ্যাটি কেন এখানে, কী বোঝায়, তা কোথাও লেখা নেই। পাঠক জানেন না 7 মানে সাত দিন না সাতটি ক্লাস, আর নিয়ম বদলালে সংখ্যাটি খুঁজে বেড়াতে হয়। প্রতিকার হলো named constant: REFUND_WINDOW_DAYS = 7. সব সংখ্যা constant হবে না — 0, 1, শতাংশের 100, বা len(items) - 1-এর 1 নিজেই স্পষ্ট। প্রশ্ন একটাই: অন্য কেউ সংখ্যাটি দেখে কি এর অর্থ আর উৎস বলতে পারবেন? ESLint-এ no-magic-numbers নামে একটি নিয়ম আছে, যা চালু করলে এমন সংখ্যা ধরে।
নিচের প্রোগ্রামটি একটি বাস্তব ধাঁচের ভুল দেখায়। কোচিং সেন্টারের বোর্ড সিদ্ধান্ত নিল, দেরিতে ফি দেওয়ার জরিমানা ৫০০ থেকে কমে ৩০০ টাকা হবে। তাড়াহুড়োয় একজন developer পুরোনো কোডে find-and-replace দিয়ে সব “500” বদলে “300” করলেন। (প্রোগ্রামে eval শুধু “কোডের লেখা বদলানো” দেখানোর জন্য; বাস্তব কোডে eval এড়িয়ে চলুন।)
// Magic numbers vs named constants. Fictional coaching centre.
// Policy change: the late-payment fine drops from 500 tk to 300 tk.
const magicSource = `(months, daysLate) => {
let fee = months * 1500;
if (months > 6) fee -= 500;
if (daysLate > 10) fee += 500;
return fee;
}`;
// A hurried edit: find-and-replace every "500" with "300".
const hits = magicSource.split("500").length - 1;
const magicV1 = eval(magicSource);
const magicV2 = eval(magicSource.replaceAll("500", "300"));
// Named constants: each number has ONE name and ONE meaning.
const RULES_V1 = Object.freeze({
MONTHLY_FEE_TK: 1500, LONG_COURSE_MONTHS: 6, LONG_COURSE_REBATE_TK: 500,
LATE_AFTER_DAYS: 10, LATE_FINE_TK: 500,
});
const RULES_V2 = Object.freeze({ ...RULES_V1, LATE_FINE_TK: 300 }); // the one edit
const makeInvoice = (r) => (months, daysLate) => {
let fee = months * r.MONTHLY_FEE_TK;
if (months > r.LONG_COURSE_MONTHS) fee -= r.LONG_COURSE_REBATE_TK;
if (daysLate > r.LATE_AFTER_DAYS) fee += r.LATE_FINE_TK;
return fee;
};
const namedV1 = makeInvoice(RULES_V1), namedV2 = makeInvoice(RULES_V2);
console.log('"500" appears ' + hits + " times in the magic version");
for (const [m, late] of [[8, 15], [3, 0], [8, 0]]) {
console.log(`${m} months, ${late} days late: magic ${magicV1(m, late)} -> ${magicV2(m, late)}` +
`, named ${namedV1(m, late)} -> ${namedV2(m, late)}`);
}আউটপুট:
"500" appears 3 times in the magic version
8 months, 15 days late: magic 12000 -> 10400, named 12000 -> 11800
3 months, 0 days late: magic 4500 -> 3900, named 4500 -> 4500
8 months, 0 days late: magic 11500 -> 10100, named 11500 -> 11500
“500” লেখাটি তিনবার আছে, অথচ developer ভেবেছিলেন একবার। প্রথমটি 1500-এর ভেতরে লুকানো, দ্বিতীয়টি দীর্ঘ কোর্সের ছাড়, তৃতীয়টিই শুধু জরিমানা। ফলে find-and-replace-এর পরে মাসিক ফি ১৫০০ থেকে ১৩০০ আর ছাড়ও ৩০০ হয়ে গেল। ৮ মাস, ১৫ দিন দেরি: সঠিক ফল ৮ × ১৫০০ − ৫০০ + ৩০০ = ১১৮০০, কিন্তু magic সংস্করণ দেয় ৮ × ১৩০০ − ৩০০ + ৩০০ = ১০৪০০. সবচেয়ে ভয়ের সারিটি দ্বিতীয়: ৩ মাস, কোনো দেরি নেই — জরিমানার সাথে এর কোনো সম্পর্কই নেই, তবু ৪৫০০ থেকে ৩৯০০ হয়ে গেল। নামওয়ালা সংস্করণে বদল একটি জায়গায় (LATE_FINE_TK: 300), আর শুধু দেরির ক্ষেত্রেই ফল বদলায়। RULES_V1 আর RULES_V2 পাশাপাশি শুধু তুলনার জন্য; বাস্তবে একটি constant-এর মানই বদলাতেন।
Error handling-এর লেখার ধরন
পর্ব ৩১-এ (interface আর contract) error জানানোর তিন পথ আর পাঁচটি নিয়ম দেখেছেন; সেগুলো আবার বলব না। এখানে প্রশ্নটি হলো: error সামলানোর কোড কীভাবে লিখলে মূল কাজ চাপা পড়ে না।
- Error handling নিজেই “একটি কাজ”:
try-এর ভেতরে অল্প কোড, প্রায়ই একটি function-ডাক; পঞ্চাশ লাইনেরtry-ব্লকে কোন লাইন কোন error তোলে, কেউ বলতে পারেন না। - প্রসঙ্গসহ exception:
ValueError("invalid")নয়,FeeError(f"student {sid}: unknown mode {mode!r}"). নিচু স্তরের error ধরে উঁচু স্তরের অর্থ যোগ করলে মূল কারণ হারাবেন না — Python-এraise FeeError(...) from e, JavaScript-এnew Error(msg, { cause: e }). Log-এ পাসওয়ার্ড বা পুরো phone নম্বর নয়। - null ফেরত দেবেন না, null পাঠাবেন না: “কিছু নেই” বোঝাতে
None/nullফেরত দিলে প্রতিটি caller-কে মনে করে যাচাই করতে হয়, আর একজন ভুললেই crash (null reference). Collection হলে খালি list ফেরত দিন; একক মান হলে স্পষ্ট exception, optional type, বা Null Object — পর্ব ৩১-এর §৮-এ বিস্তারিত। - সীমানায় fail fast: বাইরে থেকে আসা মান (form, file, API) শুরুতেই guard দিয়ে যাচাই করুন (§৪)।
- স্বাভাবিক প্রবাহের জন্য exception নয়: loop থামাতে exception তুললে পাঠক ভাবেন কিছু ভুল হয়েছে।
# Before: the happy path is buried, the error loses its cause
def import_students(path):
try:
rows = open(path, encoding="utf-8").read().splitlines()
students = [parse_student(row) for row in rows[1:]]
save_all(students)
send_summary_sms(len(students))
except Exception:
print("error") # what? where? why?
# After: one job per function, context kept, nothing swallowed
def import_students(path):
students = read_students(path) # raises ImportError with row number
save_all(students)
send_summary_sms(len(students))
def read_students(path):
with open(path, encoding="utf-8") as file:
rows = file.read().splitlines()[1:]
try:
return [parse_student(row) for row in rows]
except ValueError as e:
raise ImportError(f"{path}: bad student row ({e})") from eপ্রথম রূপে চারটি আলাদা কাজ এক try-এর ভেতরে, আর যেকোনো ব্যর্থতা — file নেই, একটি সারি ভুল, database বন্ধ, SMS যায়নি — একই অর্থহীন “error” ছাপে, তারপর চুপচাপ এগিয়ে যায় (swallowed exception). দ্বিতীয় রূপে মূল function তিন লাইনের সুখী পথ; যে error সামলানোর মতো কিছু করা যায় না (database বন্ধ), সেটি স্বাভাবিকভাবে ওপরে ওঠে; আর সারি পড়ার ভুল একটি নির্দিষ্ট error-এ বদলায়, file-এর নাম আর মূল কারণসহ। আরও দেখুন Python কোর্সের exception পর্বে, JavaScript-এ error handling পর্বে।
৮ · ছোট, সুসংগত module, Boy Scout rule আর নিয়মের সীমা
Function থেকে এক ধাপ ওপরে উঠলে একই নীতি module আর file-এ খাটে। পর্ব ২৫-এ (coupling আর cohesion) দেখেছেন, একটি module-এর ভেতরের জিনিসগুলো একই কাজে লাগলে cohesion বেশি। Clean code-এর ভাষায়: একটি module-এর নাম পড়ে বলা যাবে ভেতরে কী আছে, আর ভেতরের সব কিছু সেই নামের সাথে মানায়। utils.py ধরনের নাম প্রায়ই আবর্জনার ঝুড়ি হয়ে ওঠে। আর সুসংগতি ব্যক্তিগত পছন্দের চেয়ে গুরুত্বপূর্ণ: একই সমস্যা সবখানে একই উপায়ে সমাধান হলে একটি অংশ বুঝলে বাকিগুলোও বোঝা যায়; অন্যের file-এ সেই file-এর রীতি মানুন।
Boy Scout rule
Martin Clean Code-এ আর 97 Things Every Programmer Should Know (২০১০) সংকলনে তাঁর একটি ছোট লেখায় স্কাউটদের একটি নিয়ম ধার করেন: ক্যাম্পের জায়গা যেমন পেয়েছিলেন, তার চেয়ে একটু পরিষ্কার করে রেখে যান। এই Boy Scout rule-এর কোডের রূপ: যে কোডে কাজ করতে ঢুকেছেন, বেরোনোর সময় সেটি একটু ভালো রেখে যান — একটি অস্পষ্ট নাম বদলানো, একটি magic number-কে নাম দেওয়া, একটি পুরোনো comment মোছা।
তিনটি সাবধানতা: (১) পরিধির মধ্যে — ছুঁয়ে যাওয়া function, পুরো module নয়; (২) আলাদা commit-এ — “rename: d → months” আর “feat: online fee” দুটি আলাদা atomic commit; (৩) test-এর পাহারায়. পরিকল্পিত বড় refactoring পর্ব ৪২-এর (code smell আর refactoring) বিষয়, জমা কোড-ঋণ পর্ব ৪৩-এর।
নিয়ম যখন একে অপরের বিরুদ্ধে
Clean code-এর নিয়মগুলো সবসময় একই দিকে টানে না। কয়েকটি পরিচিত টানাপোড়েন:
| টানাপোড়েন | এক দিকে | অন্য দিকে | সাধারণ বিচার |
|---|---|---|---|
| ছোট function বনাম কাছাকাছি পড়া | প্রতিটি function এক কাজ, ৫ লাইন | দশটি ৩-লাইনের function-এ লাফাতে লাফাতে গল্প হারায় | নাম যদি সত্যিকারের নতুন ধারণা দেয় তবেই বের করুন; শুধু লাইন কমাতে নয় |
| কম comment বনাম বেশি ব্যাখ্যা | কোড নিজেই বলবে | “কেন” আর interface-এর প্রতিশ্রুতি কোড বলতে পারে না | “কী” কোডে, “কেন” comment-এ, প্রতিশ্রুতি docstring-এ |
| DRY বনাম পড়ার সহজতা (বিশেষত test-এ) | একই setup এক helper-এ | Test পড়ে বোঝা দরকার, helper খুঁজতে হবে না | Test-এ কিছু পুনরাবৃত্তি সাধারণত মেনে নেওয়া হয় |
সবচেয়ে আলোচিত টানাপোড়েনটি প্রথমটি। Ousterhout-এর deep module-এর ধারণা বলে, একটি module বা function-এর মূল্য তার interface-এর জটিলতার তুলনায় সে কতটা কাজ লুকায় (পর্ব ২৫-এও ধারণাটি দেখেছেন)। খুব ছোট টুকরো করলে প্রতিটি টুকরোর interface (নাম, parameter, কী ফেরত দেয়) প্রায় তার কাজের সমান বড় হয়ে যায় — একে তিনি বলেন shallow module. তাঁর মতে এতে মোট জটিলতা কমে না, শুধু ছড়িয়ে পড়ে। Martin-এর উত্তর: ভালো নামের ছোট function-এর ভেতরে ঢুকতেই হয় না। দুই পক্ষের লক্ষ্য একই — পাঠকের চাপ কমানো। একটি ব্যবহারিক পরীক্ষা: নতুন function-এর নাম কি তার ভেতরের কোডের চেয়ে সহজে বোঝা যায়? হ্যাঁ হলে বের করুন; না হলে, যেমন add_one(x) যা x + 1 ফেরত দেয়, তবে বের করবেন না।
আরও গভীরে: এই পর্বের বেশিরভাগ ধারণার পুরোনো শিকড় আছে। Kent Beck-এর Smalltalk Best Practice Patterns (১৯৯৬) “Composed Method” নামে এক স্তরের function আর guard clause-এর কথা বলে; Steve McConnell-এর Code Complete (১৯৯৩; ২য় সংস্করণ ২০০৪) নাম আর formatting নিয়ে দীর্ঘ অধ্যায় রাখে। আর Martin-এর Clean Code (২০০৮) ও Ousterhout-এর A Philosophy of Software Design (২০১৮; ২য় সংস্করণ ২০২১) পাশাপাশি পড়লে একই লক্ষ্যে দুই ভিন্ন পথ দেখা যায়।
পরীক্ষা ও ইন্টারভিউতে: এই প্রশ্নগুলো প্রায়ই আসে —
- Clean code বলতে কী বোঝেন? — পড়তে আর বদলাতে সহজ কোড: স্পষ্ট নাম, এক কাজের function, কম nesting, “কেন”-র comment, সুসংগত format.
- Flag argument কেন খারাপ? — Call site-এ
True/False-এর অর্থ দেখা যায় না, আর এটি স্বীকার করে function দুটি কাজ করে; দুটি function বা Enum ব্যবহার করুন। - Guard clause কী? — Function-এর শুরুতে অবৈধ অবস্থা যাচাই করে সঙ্গে সঙ্গে return/throw; nesting কমে, সুখী পথ সোজা থাকে।
- Comment কখন লিখবেন? — “কেন”, সতর্কতা, জটিল ছাঁচের অর্থ আর public interface-এর প্রতিশ্রুতির জন্য; কোডের প্রতিধ্বনি বা commented-out কোড নয়।
৯ · AI-এর যুগে clean code
AI দ্রুত দেখতে-পরিষ্কার কোড লেখে, কিন্তু নাম ভুল জিনিস বোঝাতে পারে, comment কোডের সাথে না মিলতে পারে, আর “refactor” আচরণ বদলাতে পারে। Clean code-এর মাপকাঠি তাই AI-এর লেখা যাচাইয়ের চশমা।
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
প্রতিটি লাইনে প্রতিধ্বনি-comment (# increment counter), আর কোড বদলানোর পরে পুরোনো docstring রয়ে যাওয়া | প্রতিটি comment পড়ে জিজ্ঞেস করুন “এটা কি কোড যা বলে না, তা বলছে?”; docstring-এর উদাহরণ doctest দিয়ে চালান |
শুনতে-ভালো কিন্তু ভুল নাম: validate_user যা আসলে database-এ লেখে | নাম পড়ে আগে অনুমান করুন function কী করবে, তারপর কোড পড়ে মিলিয়ে দেখুন; side effect খুঁজুন |
“Refactor” করতে গিয়ে আচরণ বদল — বিশেষ করে শর্ত উল্টানোয় (<-এর উল্টো > লেখা) | আগে test, পরে refactor; সীমার মান (ঠিক ৭ দিন, ঠিক ৮০ নম্বর) দিয়ে test; diff লাইন ধরে পড়ুন; mutation testing (পর্ব ৫০) |
| বানানো নিয়ম আর তথ্য: “PEP 8 ১০০ অক্ষর বলে”, ভুল বই বা বছর, অস্তিত্বহীন linter option | মূল নথি (PEP, tool-এর documentation) খুলে মেলান; tool-এর --help চালিয়ে দেখুন |
পরিষ্কারের পরে history “গোছাতে” git push --force বা git reset --hard-এর পরামর্শ, সতর্কতা ছাড়া | শেয়ার করা branch-এ force push নয়; অনিশ্চিত হলে একটি throwaway clone-এ চেষ্টা করুন (পর্ব ১১, ভুল শোধরানো) |
AI দিয়ে কোড পরিষ্কার করাতে হলে তাকে বেঁধে দিন — আচরণ বদলানো নিষেধ, ছোট ধাপ, আর প্রতিটি বদলের কারণ। কোড দেওয়ার আগে ব্যক্তিগত তথ্য, phone নম্বর, API key আর password মুছে নিন:
AI-এর পরিষ্কার-করা কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১“Function খুব ছোট হবে” — Martin আর Ousterhout এখানে কেন একমত নন? দুই পক্ষের যুক্তি মিলিয়ে কীভাবে সিদ্ধান্ত নেবেন?
Robert C. Martin Clean Code-এ (২০০৮) লেখেন, function-এর প্রথম নিয়ম হলো ছোট হওয়া, আর দ্বিতীয় নিয়ম হলো আরও ছোট হওয়া; ২০ লাইনের বেশি প্রায় কখনো নয়, আর অনেক উদাহরণে তিন-চার লাইন। যুক্তিটি হলো: প্রতিটি ছোট function-এর একটি ভালো নাম থাকলে উঁচু স্তরের function একটি সূচিপত্রের মতো পড়া যায়, আর ভেতরে ঢোকার দরকারই পড়ে না।
John Ousterhout A Philosophy of Software Design-এ (২০১৮) জটিলতাকে সংজ্ঞা দেন এমনভাবে: যা কিছু system বোঝা আর বদলানো কঠিন করে। তাঁর মতে জটিলতার উৎস দুটি — নির্ভরতা আর অস্পষ্টতা — আর প্রতিকার হলো “deep” module: ছোট, সরল interface, পেছনে অনেক কাজ। তিনি সতর্ক করেন, খুব ছোট টুকরোয় ভাঙলে প্রতিটি টুকরোর নিজস্ব interface তৈরি হয়, আর অনেক সময় একটি টুকরো বুঝতে গেলে পাশের তিনটি পড়তে হয় — কারণ তারা আসলে একটি ধারণার ভাগ, স্বাধীন নয়। এতে জটিলতা কমে না, শুধু ছড়ায়। তিনি comment নিয়েও ভিন্নমত রাখেন: interface-এর comment-কে তিনি নকশার অপরিহার্য অংশ মনে করেন, “ব্যর্থতার ক্ষতিপূরণ” নয়।
সিদ্ধান্তের জন্য কয়েকটি ব্যবহারিক প্রশ্ন: (১) নতুন function-এর নাম কি তার ভেতরের কোডের চেয়ে সহজে বোঝা যায়? (২) Caller কি ভেতরে না ঢুকেই function-টি বিশ্বাস করে ব্যবহার করতে পারবেন, নাকি প্রতিবার ভেতরে গিয়ে দেখতে হবে? (৩) দুটি টুকরো কি সবসময় একসাথে বদলায়? তাহলে সম্ভবত তারা একটি ধারণা। (৪) Function কি একাধিক স্তরের কথা মেশাচ্ছে? তাহলে ভাঙুন, আকার যা-ই হোক। একটি সরল algorithm-এর ৪০ লাইন প্রায়ই দশটি টুকরোর চেয়ে সহজ; আবার যাচাই-হিসাব-ছাপা মেশানো ১৫ লাইন ভাঙা দরকার।
মূল উপলব্ধি: লাইন-সংখ্যা লক্ষ্য নয়, সংকেত। আসল মাপ হলো পাঠককে একসাথে কতটা মাথায় রাখতে হয় — আর দুই বই দুই দিক থেকে সেই একই মাপের কথা বলে।
প্র ০২২০১৪ সালের Apple-এর “goto fail” bug থেকে formatting, বন্ধনী আর review নিয়ে কী শেখার আছে?
২০১৪ সালের ফেব্রুয়ারিতে Apple iOS আর macOS-এর TLS/SSL যাচাইয়ের কোডে (SecureTransport) একটি গুরুতর ত্রুটি সংশোধন করে, যা CVE-2014-1266 নামে নথিভুক্ত। প্রকাশিত source অনুযায়ী, server-এর key যাচাইয়ের একটি C function-এ পরপর কয়েকটি যাচাই ছিল, প্রতিটি এই ছাঁচে: if ((err = …) != 0) goto fail;. কোথাও একটি লাইন দুবার লেখা হয়ে গিয়েছিল — দ্বিতীয় goto fail; কোনো if-এর অধীনে ছিল না, কিন্তু indentation-এর কারণে দেখতে মনে হতো আগের if-এর ভেতরে। C-তে বন্ধনী ছাড়া if শুধু পরের একটি statement নিয়ন্ত্রণ করে, তাই দ্বিতীয় goto সবসময় চলত, আর পরের আসল signature যাচাইটি কখনো হতো না। ফলে নির্দিষ্ট পরিস্থিতিতে ভুয়া server-কেও বৈধ মনে হতে পারত — একটি man-in-the-middle আক্রমণের সুযোগ।
কয়েকটি শিক্ষা, যা এই পর্বের সাথে মেলে: (১) Indentation মিথ্যা বলতে পারে। মানুষ indentation দেখে কাঠামো বোঝে, compiler দেখে বন্ধনী। স্বয়ংক্রিয় formatter কোডকে compiler-এর বোঝা কাঠামো অনুযায়ী সাজায়, তাই এমন লাইন বাঁ দিকে সরে চোখে পড়ত। (২) সবসময় বন্ধনী। অনেক style guide (যেমন অনেক C/C++ আর JavaScript দল) এক লাইনের if-এও বন্ধনী বাধ্যতামূলক করে ঠিক এই কারণে। (৩) Compiler warning আর static analysis. পৌঁছানো যায় না এমন কোড (unreachable code) সতর্ক করার মতো বিষয়; আধুনিক compiler আর static analysis tool এ ধরনের অনেক কিছু ধরতে পারে, যদি চালু থাকে। (৪) Test. “ভুল signature হলে সংযোগ প্রত্যাখ্যাত হবে” — এই একটি negative test থাকলে bug-টি ধরা পড়ত। (৫) Diff-এর review. দুটি একই লাইন পাশাপাশি — review-তে চোখে পড়ার মতো, যদি diff ছোট আর পরিষ্কার হয়।
কেউ কেউ goto-কেই দোষ দেন, কিন্তু C-তে resource ছাড়ার জন্য goto fail ছাঁচটি প্রচলিত আর যুক্তিসঙ্গত; সমস্যাটি ছিল বন্ধনী-ছাড়া if, ভুল-বোঝানো indentation আর যাচাইয়ের অভাবের যোগফল।
মূল উপলব্ধি: Formatting শুধু সৌন্দর্য নয় — কাঠামো চোখে দেখানোর উপায়। যন্ত্রে format, সবসময় বন্ধনী, warning চালু আর negative test — প্রতিটি আলাদা জাল, একসাথে অনেক বেশি নিরাপদ।
প্র ০৩Hungarian notation একসময় জনপ্রিয় ছিল, এখন প্রায় সবাই নিরুৎসাহিত করেন। তাহলে কি এর মূল চিন্তাটিই ভুল ছিল?
পুরোটা নয়। রীতিটি Charles Simonyi-র নামে পরিচিত, যিনি Xerox PARC আর পরে Microsoft-এ কাজ করেছেন। প্রচলিত বিবরণ অনুযায়ী তাঁর মূল ধারণা ছিল নামের শুরুতে ব্যবহারের ধরন লেখা — যেমন rw (row), col (column), us (unsafe string, ব্যবহারকারীর দেওয়া, এখনো যাচাই হয়নি)। পরে এটি “Apps Hungarian” নামে পরিচিত হয়। কিন্তু বহু জায়গায়, বিশেষ করে Windows API-র আশেপাশে, রীতিটি ছড়াল অন্য রূপে: নামের শুরুতে compiler-এর type — i (int), sz (zero-terminated string), dw (double word), lp (long pointer). একে বলা হয় “Systems Hungarian”, আর সমালোচনা মূলত এর বিরুদ্ধে।
Systems Hungarian-এর সমস্যা: type compiler আগেই জানে, editor দেখায়, তাই নামে তা পুনরাবৃত্তি; type বদলালে (int থেকে long) প্রতিটি নাম বদলাতে হয়, নাহলে নাম মিথ্যা বলে; আর নামের প্রথম অংশ পড়তে কষ্ট হয়। Linux kernel-এর coding style নথি এটিকে কড়া ভাষায় নিরুৎসাহিত করে, আর আধুনিক বেশিরভাগ style guide একই পথে।
অন্যদিকে Apps Hungarian-এর চিন্তাটি এখনো জীবিত। Joel Spolsky ২০০৫ সালের একটি পরিচিত প্রবন্ধে (“Making Wrong Code Look Wrong”) দেখান, us আর s (safe) উপসর্গ থাকলে write(usName) লাইনটিই ভুল দেখায় — যাচাই না-করা লেখা সরাসরি পাতায় লিখলে নিরাপত্তা-ঝুঁকি। এই পর্বে যে “একক নামে রাখুন” পরামর্শ দেখেছেন (timeout_ms, fee_tk), সেটিও একই চিন্তা: নাম বলে মানটি কী অর্থে ব্যবহার হবে। আধুনিক ভাষায় এর আরও শক্ত রূপ হলো আলাদা type — Milliseconds, Taka, UnsafeHtml — যাতে ভুল মিশ্রণ compiler-ই আটকায়।
এই পার্থক্যটিই আজকের নিয়মের ভিত্তি: আধুনিক style guide নামে type লিখতে নিষেধ করে, কিন্তু অর্থ লিখতে উৎসাহ দেয়। raw_input_html আর escaped_html — দুটিই string, তবু নাম দেখেই বোঝা যায় কোনটি পাতায় সরাসরি বসানো বিপজ্জনক।
মূল উপলব্ধি: নামে যন্ত্র যা আগেই জানে (type) তা লেখা অপচয়; যন্ত্র যা জানে না (একক, উৎস, নিরাপদ কি না) তা লেখা মূল্যবান — আর সম্ভব হলে সেটিকে type-এ তুলে দেওয়া আরও ভালো।
প্র ০৪পুরো code-ভিত্তি একবারে formatter দিয়ে সাজালে git blame অকেজো হয়ে যায় — এই দামের বিপরীতে লাভ কী, আর ক্ষতি কমানোর উপায় কী?
git blame file প্রতিটি লাইনের পাশে দেখায় শেষবার কোন commit সেই লাইনটি বদলেছে — bug খোঁজায় খুব কাজের (পর্ব ৫৫-এ বিস্তারিত)। কিন্তু একদিন যদি একটি commit সব file-এর indentation আর space বদলে দেয়, তাহলে প্রায় প্রতিটি লাইনের “শেষ বদল” হয়ে যায় সেই formatter-commit; আসল লেখক আর কারণ খুঁজতে আরও এক ধাপ পেছনে যেতে হয়। এটাই অনেক দলের formatter চালু করতে দ্বিধার কারণ।
লাভের দিকটা বড়: Go-তে gofmt ভাষার শুরু থেকে থাকায় Go-র প্রায় সব কোড একই রকম দেখায়, আর style নিয়ে তর্ক কার্যত নেই — এই অভিজ্ঞতাই Black, Prettier-এর মতো “কম option” formatter-কে অনুপ্রাণিত করেছে বলে প্রায়ই বলা হয়। একবার সাজানো হয়ে গেলে প্রতিটি পরের diff-এ শুধু আসল পরিবর্তন থাকে, review দ্রুত হয়, merge conflict-এর একটি বড় উৎস (দুজন দুভাবে space দেওয়া) চলে যায়।
ক্ষতি কমানোর উপায়: (১) একটি আলাদা commit, শুধু formatting, অন্য কোনো বদল ছাড়া, একটি স্পষ্ট message-সহ (style: format whole codebase with black). (২) Git ২.২৩ (২০১৯) থেকে git blame-এ --ignore-rev আর --ignore-revs-file option আছে; দলগুলো প্রায়ই .git-blame-ignore-revs নামে একটি file-এ এমন commit-এর hash রাখে আর git config blame.ignoreRevsFile .git-blame-ignore-revs দিয়ে চালু করে। তখন blame সেই commit পেরিয়ে আগের আসল লেখককে দেখায়। কিছু hosting platform-ও লেখার সময়ে এই file-টি চেনে, তবে platform-ভেদে আলাদা। (৩) সব খোলা branch-এর মালিকদের আগে জানানো, কারণ বড় format-commit-এর পরে পুরোনো branch merge করলে conflict হবে; অনেক দল সবাই merge করার পরে একটি শান্ত দিনে কাজটি করে। (৪) এরপর CI-তে format যাচাই, যাতে আবার এলোমেলো না হয়।
মূল উপলব্ধি: একবারের দাম, স্থায়ী লাভ — যদি দামটা পরিকল্পনা করে, আলাদা commit-এ আর সঠিক Git tool দিয়ে দেওয়া হয়।
প্র ০৫Python আর JavaScript বাংলায় নাম দিতে দেয় — তাহলে বাংলাদেশি দল কেন সাধারণত ইংরেজি নাম রাখে? Unicode নামের কোনো নিরাপত্তা-ঝুঁকি আছে কি?
ভাষাগতভাবে কোনো বাধা নেই। Python 3 থেকে (PEP 3131) Unicode-এর “অক্ষর” শ্রেণির চিহ্ন দিয়ে identifier বানানো যায়, তাই মোট_ফি = 3600 বৈধ; ECMAScript-ও Unicode-ভিত্তিক identifier নিয়ম মানে। শিক্ষার শুরুতে, বা শুধু বাংলাভাষী একটি ছোট দলের অভ্যন্তরীণ script-এ, এটি বোঝার সুবিধা দিতে পারে — ব্যবসার শব্দ (যাকাত, ঈদ বোনাস, মোহরানা) ঠিক সেই নামেই লেখা যায়।
তবু বেশিরভাগ দল ইংরেজিতে থাকে, কয়েকটি ব্যবহারিক কারণে: library, framework, error message আর documentation ইংরেজিতে, তাই মিশ্র ভাষার কোড পড়তে বারবার মোড় নিতে হয়; সব editor, terminal, font আর keyboard বিন্যাসে বাংলা যুক্তাক্ষর সমানভাবে দেখায় বা টাইপ হয় না; বিদেশি সহকর্মী, ক্লায়েন্ট বা open-source অবদানকারী পড়তে পারেন না।
নিরাপত্তার দিকটি আরও গুরুত্বপূর্ণ। ২০২১ সালে Cambridge-এর গবেষক Nicholas Boucher আর Ross Anderson “Trojan Source” নামে একটি গবেষণা প্রকাশ করেন: Unicode-এর দিক-নির্দেশক নিয়ন্ত্রণ-অক্ষর (bidirectional control) দিয়ে এমন source লেখা যায়, যা মানুষের চোখে একরকম দেখায় অথচ compiler আরেকভাবে পড়ে (CVE-2021-42574). একই গবেষণায় homoglyph-এর কথাও আছে — দেখতে-একই কিন্তু আলাদা অক্ষর দিয়ে দুটি ভিন্ন function, যেমন Latin “a” আর Cyrillic “а” (CVE-2021-42694). এর পরে অনেক compiler, editor আর code hosting platform সন্দেহজনক অক্ষর সতর্ক করা শুরু করে। বাংলা অক্ষর নিজে বিপজ্জনক নয়, কিন্তু যত বেশি অক্ষর-ভাণ্ডার, তত বেশি দেখতে-কাছাকাছি চিহ্ন, আর review-তে চোখ দিয়ে ধরার সুযোগ তত কম।
একটি যুক্তিসঙ্গত নীতি: identifier ইংরেজিতে (ব্যবসার শব্দ উচ্চারণ অনুযায়ী লিখলেও চলে — zakat_amount), ব্যবহারকারীকে দেখানো লেখা বাংলায় আর আলাদা অনুবাদ-file-এ, আর CI-তে এমন একটি যাচাই যা কোডে অপ্রত্যাশিত Unicode নিয়ন্ত্রণ-অক্ষর ধরে।
মূল উপলব্ধি: “ভাষা অনুমতি দেয়” আর “দলের জন্য ভালো” আলাদা প্রশ্ন। নামের ভাষা বেছে নেওয়া একটি দলগত সিদ্ধান্ত — পাঠক কারা, tool কী সমর্থন করে, আর নিরাপত্তা কীভাবে যাচাই হবে, এই তিনটি দেখে।
অনুশীলন
আগে নিজে উত্তর লিখুন, তারপর মিলিয়ে নিন।
-
আউটপুট অনুমান করুন: একটি কাল্পনিক app-এ PIN যাচাইয়ের এই Python কোড চালালে ঠিক কী ছাপা হবে? নামের সাথে আচরণ কোথায় মেলে না?
attempts = [] def is_valid_pin(pin): attempts.append(pin) return len(attempts) <= 3 and pin == "4321" print(is_valid_pin("1111"), is_valid_pin("4321")) if is_valid_pin("4321"): print("welcome") print(is_valid_pin("4321"), len(attempts))False True welcome False 4প্রতিটি ডাক
attempts-এ একটি করে যোগ করে। প্রথম লাইন: ১ম চেষ্টা “1111” — ভুল PIN,False; ২য় চেষ্টা “4321” — চেষ্টা ২ ≤ ৩ আর PIN ঠিক,True.if-এর ভেতরের ডাকটি ৩য় চেষ্টা — এখনো ≤ ৩, তাইwelcome. শেষ লাইনে ৪র্থ চেষ্টা: PIN ঠিক হলেও ৪ > ৩, তাইFalse, আর মোট চেষ্টা ৪. সমস্যা: নামis_valid_pinএকটি query-র মতো, কিন্তু function অবস্থা বদলায় — লুকানো side effect, command–query separation ভাঙা। পরিষ্কার রূপ:record_pin_attempt(pin)(command) আরis_pin_correct(pin)ওhas_attempts_left()(query), আলাদা। -
এই function পরিষ্কার করুন: একটি কাল্পনিক পাঠাগারের সদস্যপদ ঠিক করার JavaScript function নিচে। (ক) nesting depth কত, কতগুলো magic number আর কোনটি flag argument? (খ) আচরণ না বদলে পরিষ্কার রূপ লিখুন।
function chk(u, x) { if (u != null) { if (u.age >= 18) { if (x) { return u.balance > 2000 ? "premium" : "basic"; } else { return "basic"; } } else { return "minor"; } } else { return "none"; } }(ক) সবচেয়ে ভেতরের
returnতিনটিif-ব্লকের ভেতরে, তাই nesting depth ৩ (§৪-এর brace-গোনা মাপে)। Magic number দুটি:18(প্রাপ্তবয়স্কের বয়স) আর2000(premium-এর ন্যূনতম জমা)।xহলো flag argument —chk(member, true)পড়ে বোঝা যায় নাtrueমানে “premium বিবেচনা করা হবে”।(খ) একটি সম্ভাব্য পরিষ্কার রূপ — flag সরিয়ে দুটি function, guard clause আর নামওয়ালা constant:
const ADULT_AGE = 18; const PREMIUM_MIN_BALANCE_TK = 2000; function membershipTier(member) { if (member == null) return "none"; if (member.age < ADULT_AGE) return "minor"; return "basic"; } function membershipTierWithPremium(member) { const tier = membershipTier(member); if (tier !== "basic") return tier; return member.balance > PREMIUM_MIN_BALANCE_TK ? "premium" : "basic"; }দুটি function-এর depth ০. আটটি ইনপুটে (null, ১৫ বছর, ঠিক ১৮ বছর ও ঠিক ২০০০ টাকা, ৩০ বছর ও ২৫০০ টাকা — প্রতিটি
xসত্য ও মিথ্যা দুইভাবে) পুরোনোchkআর নতুন জোড়া হুবহু একই ফল দেয়: none, none, minor, minor, basic, basic, premium, basic. সীমার মান লক্ষ করুন:age >= 18-এর উল্টোage < 18, আর ঠিক ২০০০ টাকায় premium নয় (>,>=নয়)।u != nullআরmember == nullদুটিইundefined-কেও ধরে, তাই আচরণ একই। -
সমালোচনা — নিয়ম যখন সংঘর্ষে: একটি pull request-এ তিনজন reviewer তিনটি মন্তব্য করেছেন। (১) “
format_slip()১২ লাইনের, এটি চারটি ৩-লাইনের function-এ ভাঙুন।” (২) “round_down_to_taka()-এর ওপরের comment মুছে দিন — clean code-এ comment লাগে না।” comment-টি হলো:# বোর্ডের সিদ্ধান্ত: ছাড় সবসময় নিচের দিকে গোল; ব্যাংকের হিসাবের সাথে মেলাতে. (৩) “চারটি test-এ একই ৬ লাইনের setup — একটি helper-এ নিন।” প্রতিটির পক্ষে-বিপক্ষে যুক্তি দিয়ে আপনার সিদ্ধান্ত লিখুন।(১) ভাঙবেন কি? প্রশ্ন লাইন-সংখ্যা নয়, স্তর।
format_slipযদি শুধু রসিদের লেখা সাজায় — একটি ধারণা — তাহলে চার টুকরোর নাম প্রায় তাদের কোডের সমান হবে, টুকরোগুলো সবসময় একসাথে বদলাবে (Ousterhout-এর ভাষায় shallow). সিদ্ধান্ত: না, যদি না ভেতরে আলাদা কোনো দায় (যেমন মুদ্রার format) লুকিয়ে থাকে।(২) Comment মুছবেন কি? না। Comment-টি কোডের প্রতিধ্বনি নয় — এটি “কেন” বলে (বোর্ডের সিদ্ধান্ত, ব্যাংকের সাথে মেলানো), যা function-এর নাম বা কোড থেকে বোঝা যায় না, আর কেউ “আরও নির্ভুল” করতে
round()বসালে হিসাব ব্যাংকের সাথে মিলবে না। Martin-ও “কেন” আর সতর্কতা-comment গ্রহণযোগ্য বলেন; reviewer নিয়মটি অতিরিক্ত সরল করে পড়েছেন। ভালো হয় issue নম্বর যোগ করা আর এই আচরণের একটি test রাখা।(৩) Setup-এ DRY? এখানে দুই দিকেই যুক্তি আছে। Helper নিলে বদল এক জায়গায়, কিন্তু test পড়তে helper খুঁজতে হয়, আর ধীরে ধীরে helper-এ flag ঢোকে। মাঝের পথ: setup সত্যিই একই হলে ভালো নামের helper (
make_offline_student(months=3)), যেখানে গুরুত্বপূর্ণ মান call-এই দেখা যায়; প্রতিটিতে সামান্য আলাদা হলে কপিই ভালো। Test-এর গঠনের বিস্তারিত পর্ব ৪৬-এ।সার: প্রতিটি নিয়মের পেছনের উদ্দেশ্য — পাঠকের চাপ কমানো, পরিবর্তন সস্তা করা — দিয়ে বিচার করুন; reviewer-কে নিয়মের নাম নয়, এই উদ্দেশ্য দিয়ে উত্তর দিন।
-
Command-এর ক্রম: একজন সহকর্মী কিছুদিন আগে
fee.pyথেকে পুরোনো ঈদ-অফারের commented-out কোড মুছে দিয়েছেন। Comment-এold_eid_offerশব্দটি ছিল। মালিক এখন অফারটি ফিরিয়ে আনতে চান। (ক) কোন commit-এ কোডটি যোগ আর কোনটিতে মোছা হয়েছিল, কোন command-এ খুঁজবেন? (খ) সেই সময়ের file কীভাবে দেখবেন, working tree না বদলে? (গ) পুরো মোছা commit-টি উল্টে দিতে চাইলে কোন command, আর কেনreset --hardনয়?(ক)
git log -S "old_eid_offer" --oneline. §৫-এর উদাহরণ repository-তে (Git ২.৫৩) এটি দুটি commit দেখায়:6851b01 refactor: remove commented-out Eid offer(মোছা) আর314837b chore: park the old Eid offer(যোগ)। নতুনটি ওপরে।(খ)
git show 314837b:fee.py— ওই commit-এর সময়ের পুরো file দেখায়, কিছু বদলায় না। দরকারি লাইনগুলো কপি করে বর্তমান কোডে বসাতে পারেন, আর এবার comment হিসেবে নয়, সত্যিকারের কোড হিসেবে, নামওয়ালা constant আর test-সহ (EID_OFFER_MIN_MARKS = 70,EID_OFFER_DISCOUNT = 0.20).(গ)
git revert 6851b01— একটি নতুন commit বানায়, যা মোছাটিকে উল্টে দেয়; history-র কিছু মোছে না, তাই শেয়ার করা branch-এ নিরাপদ (পর্ব ১১, ভুল শোধরানো)। কিন্তু খেয়াল করুন, এতে commented-out কোডই ফিরবে — clean code-এর দিক থেকে (খ)-এর পথটি ভালো।git reset --hardbranch পেছনে সরিয়ে পরের commit আর না-commit-করা কাজ বাদ দেয় — শেয়ার করা history-তে অন্যদের কাজ ভাঙে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি অস্পষ্ট নাম নিরাপদে বদলানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🧪 বদলের আগে test চালিয়ে সব সবুজ কি না নিশ্চিত হওয়া
- ✏️ Editor-এর Rename Symbol দিয়ে একটি নাম বদলানো
- 🔁 আবার test চালানো
- 🔎
git diffদেখে শুধু নামটিই বদলেছে কি না যাচাই - 📌 স্পষ্ট message-সহ একটি ছোট commit
একটি কোচিং app-এ ভর্তির পর কত দিন পেরিয়েছে, সেটি রাখার জন্য কোন variable-এর নাম সবচেয়ে উদ্দেশ্য-প্রকাশক?
dnDaysdays_since_admissiondata
কোডে render(report, True) লেখা আছে, যেখানে দ্বিতীয় parameter সত্য হলে PDF, মিথ্যা হলে HTML বানানো হয়। Clean code-এর দিক থেকে মূল সমস্যা কী?
- Boolean parameter প্রোগ্রামকে ধীর করে
- Call site-এ True-র অর্থ বোঝা যায় না, আর function দুটি আলাদা কাজ করে
- Parameter অনেক বেশি
- Function-এর নাম ক্রিয়া হওয়া উচিত নয়
একটি function-এর গঠন: if (a) { if (b) { if (c) { doWork(); } } }, যেখানে a, b, c তিনটিই পূর্বশর্ত। তিনটিকে guard clause-এ বদলালে (if (!a) return; ইত্যাদি) doWork() কত গভীর nesting-এ থাকবে?
- ০ (কোনো ব্লকের ভেতরে নয়)
- ১
- ২
- ৩
নিচের কোন comment সবচেয়ে বেশি মূল্য যোগ করে?
count += 1 # count এক বাড়াও# নিচের দিকে গোল: ব্যাংকের হিসাবের সাথে মেলাতে (issue #142)# if marks >= 70: discount = 0.20# TODO: fix later
একটি ফি-হিসাবের কোড: fee = months * 1500; if (months > 6) fee -= 500; if (late) fee += 500;. Developer পুরো লেখায় find-and-replace দিয়ে সব “500” বদলে “300” করলেন। এখন months = 8, late = true হলে fee কত?
- 11800
- 10400
- 12000
- 11500
Boy Scout rule-এর সবচেয়ে সঠিক প্রয়োগ কোনটি?
- যে file-এ কাজ করবেন, পুরোটা নতুন করে লিখে ফেলা
- অন্যের লেখা কোডে কখনো হাত না দেওয়া
- যে function-এ কাজ করছেন, সেখানে একটি অস্পষ্ট নাম বা magic number ঠিক করে আলাদা commit-এ রাখা
- প্রতি বছর একটি মাস শুধু পরিষ্কারের জন্য রাখা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- কোড পড়া হয় লেখার চেয়ে অনেক বেশি; clean code মানে পড়তে আর বদলাতে সহজ কোড — নিয়মগুলো অভ্যাস, আইন নয়।
- নাম উদ্দেশ্য, একক আর পরিধি বলবে; type-সংকেত নয়; boolean প্রশ্নের মতো (
is_paid); দলের ভাষা আর রীতি একটাই। - Function এক কাজ করে, এক স্তরে কথা বলে; কম parameter, flag argument নয়, নামে না-বলা side effect নয়; nested
if-এর বদলে guard clause. - Comment “কেন” বলে; commented-out কোড মুছুন —
git log -Sআরgit showদিয়ে ফেরত পাওয়া যায়; formatting যন্ত্রের হাতে, আলাদা commit-এ। - নামহীন সংখ্যার বদলে named constant; error-এ প্রসঙ্গ রাখুন, null ফেরত নয়; আর Boy Scout rule — ছুঁয়ে যাওয়া কোড একটু ভালো রেখে যান, test-এর পাহারায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪২ · Code smell আর refactoring — ছোট নিরাপদ ধাপে কোড উন্নত করাপরবর্তী পর্বSmell-এর তালিকা আর প্রতিটির জন্য নাম-ওয়ালা refactoring.
- পর্ব ৪৪ · Code review — কী দেখবেন, কীভাবে মন্তব্য করবেন, linter আর static analysisএকই মডিউলClean code-এর নিয়ম দলে প্রয়োগের প্রক্রিয়া আর যন্ত্র।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python প্রোগ্রামিং · Pythonic কোডপাশের ট্র্যাকPEP 8, idiom আর Python-এ পরিষ্কার কোডের রীতি।
- JavaScript প্রোগ্রামিং · Error handlingপাশের ট্র্যাকJavaScript-এ exception,
causeআর async error.