সরাসরি পর্বে যান
পর্ব ৪৪ · ৭০-এর মধ্যে · মডিউল ৭

Code review — কী দেখবেন, কীভাবে মন্তব্য করবেন, linter আর static analysis

Code Review — What to Look For, How to Comment, Linters and Static Analysis

Taught in Bangla, this lesson covers why teams review code, the pipeline from author to tools to humans, what formatters, linters, static analysis and type checkers catch, with a tiny linter built on Python's ast, the order for reading a diff, a reviewer checklist with security questions, writing kind and specific comments, responding with fixup commits, review queues and risk-based prioritization, and pair and mob programming.

এই পর্বে যা শিখবেন

  • Code review-এর লক্ষ্য আর তার পেছনের প্রমাণ — Fagan inspection থেকে আজকের lightweight review
  • Formatter, linter, static analysis আর type checker কী ধরে, কী ধরে না — Python-এর ast দিয়ে একটি খুদে linter বানিয়ে
  • একটি diff কোন ক্রমে পড়বেন, আর reviewer-এর একটি ব্যবহারযোগ্য চেকলিস্ট, নিরাপত্তার মৌলিক প্রশ্নসহ
  • কীভাবে সদয়, নির্দিষ্ট, কাজের মন্তব্য লিখবেন; কীভাবে feedback নেবেন আর fixup commit দিয়ে সাড়া দেবেন
  • Review-এর সময় আর চাপের মডেল, কোন PR আগে দেখবেন তার একটি ঝুঁকি-হিসাব, আর pair ও mob programming কখন ভালো বিকল্প

১ · Code review কেন — ভুল ধরা থেকে জ্ঞান ভাগাভাগি পর্যন্ত

পর্ব ১৬-এ (Pull request আর fork) দেখেছেন একটি pull request কীভাবে খোলা হয়, review-এর তিন রকম সিদ্ধান্ত কী, আর মন্তব্য পেয়ে branch কীভাবে হালনাগাদ হয়। সেটি ছিল যন্ত্রপাতি। এই পর্বে প্রশ্ন হলো কাজটি: reviewer আসলে কী খুঁজবেন, কোন ক্রমে খুঁজবেন, কী ভাষায় বলবেন, আর কোন কাজ মানুষের বদলে যন্ত্রকে দেবেন।

বোর্ড পরীক্ষার খাতা মনে করুন। একজন পরীক্ষক নম্বর দেন, তারপর আরেকজন খাতাটি আবার দেখেন — যোগে ভুল আছে কি না, কোনো প্রশ্ন বাদ পড়ল কি না। দ্বিতীয়জন উত্তর নতুন করে লেখেন না; তিনি দেখেন প্রথমজনের চোখ এড়িয়ে কী গেছে। আর বহু বছর এভাবে কাজ করলে দুজনেই একে অপরের কাছ থেকে শেখেন — কোন উত্তরে সাধারণত কী ভুল হয়। Code review-ও তাই: লেখকের পরে আরেক জোড়া চোখ, যার কাজ ভুল ধরা, আর তার সাথে দলের জ্ঞান ভাগ করা।

একটি দল কেন review করে, তার পাঁচটি প্রধান কারণ:

ত্রুটি ধরা

যুক্তির ভুল, সীমার ভুল (off-by-one), বাদ পড়া case — merge-এর আগে, যখন ঠিক করা সবচেয়ে সস্তা।

জ্ঞান ভাগাভাগি

অন্তত আরেকজন জানেন কোডটি কী করে। একজন ছুটিতে গেলে বা চাকরি বদলালে কাজ আটকায় না।

সামঞ্জস্য

পুরো codebase যেন এক দলের লেখা মনে হয় — একই ধাঁচের নাম, error handling আর স্তরবিন্যাস।

শেখানো

নতুন সদস্য মন্তব্য থেকে দলের রীতি শেখেন; আবার অভিজ্ঞরাও নতুনদের প্রশ্ন থেকে নিজের অনুমান যাচাই করেন।

নিরাপত্তা

ইনপুট যাচাই, secret, কে কী করতে পারবেন — এগুলোয় দ্বিতীয় চোখ বড় ঝুঁকি ঠেকায়।

Fagan inspection (১৯৭৬): IBM-এ কাজ করার সময় Michael Fagan একটি আনুষ্ঠানিক পদ্ধতির বিবরণ প্রকাশ করেন — “Design and code inspections to reduce errors in program development”, IBM Systems Journal, ১৯৭৬. এতে নির্দিষ্ট ভূমিকা (moderator, লেখক, কোড পড়ে শোনানো reader, inspector), আগাম প্রস্তুতি, একটি সভা, ভুলের লিখিত তালিকা, সংশোধন আর follow-up থাকে। এই inspection test-এর আগেই অনেক ত্রুটি ধরে বলে জানানো হয়েছিল (হার পরিবেশভেদে খুব আলাদা, তাই সংখ্যা দিচ্ছি না); খরচ ছিল কয়েকজনের একসাথে সভার সময়।

আজকের বেশিরভাগ দল যা করে, গবেষকেরা তার নাম দিয়েছেন modern code review: হালকা, tool-ভিত্তিক (PR-এর পাতা), সভা ছাড়া, নিজের সময়ে (asynchronous), আর প্রায় প্রতিটি পরিবর্তনে। এই ধারার সবচেয়ে উদ্ধৃত গবেষণাগুলোর একটি Alberto Bacchelli আর Christian Bird-এর “Expectations, Outcomes, and Challenges of Modern Code Review” (ICSE ২০১৩), Microsoft-এর ডেভেলপারদের ওপর। আমাদের বোঝামতে তাঁদের মূল পর্যবেক্ষণ: ডেভেলপাররা review-এর প্রধান উদ্দেশ্য হিসেবে ত্রুটি খোঁজাকে বলেন, কিন্তু বাস্তবে পাওয়া মন্তব্যের বড় অংশ ছিল কোডের মান আর পড়ার সুবিধা নিয়ে, আর জ্ঞান স্থানান্তর, দলের সচেতনতা (কে কী বদলাচ্ছে) ও বিকল্প সমাধান খোঁজা ছিল বড় লাভ। সবচেয়ে বড় বাধা ছিল পরিবর্তনটি বোঝা। এক কোম্পানির গবেষণা সব দলে হুবহু খাটবে না, তবে দিকটি পরের গবেষণাতেও মিলেছে।

মূল ধারণা

Review-কে “bug-এর ছাঁকনি” ভাবলে আপনি হতাশ হবেন — অনেক bug review পার হয়ে যায়, আর তাদের জন্য test আছে (পর্ব ৪৫ থেকে Testing মডিউল)। Review-কে ভাবুন দলের যৌথ বোঝাপড়া তৈরির সময় হিসেবে: কোডটি সবাই বুঝতে পারছেন কি না, নকশাটি দলের দিকের সাথে মেলে কি না, আর ঝুঁকিপূর্ণ জায়গায় দ্বিতীয় চোখ পড়েছে কি না। Bug ধরা এর একটি মূল্যবান উপজাত।

Eric Raymond-এর প্রবন্ধ The Cathedral and the Bazaar (১৯৯৭; বই ১৯৯৯)-এর “Linus’s law” বলে: “given enough eyeballs, all bugs are shallow”. কিন্তু চোখ থাকা আর চোখ পড়া এক নয় — ভাবনার প্রশ্ন ০২-এ দেখবেন।

২ · Review-এর পাইপলাইন — লেখক, যন্ত্র, মানুষ, তারপর merge

ভালো দলে একটি পরিবর্তন চারটি ছাঁকনি পার হয়, আর প্রতিটির ব্যর্থতা লেখকের কাছেই ফিরে আসে। কোন ছাঁকনি কী ধরবে, তা আগে থেকে ভাগ করা থাকলে কেউ অন্যের কাজ নকল করেন না — মানুষ indentation গোনেন না, আর যন্ত্রকে নকশার প্রশ্ন জিজ্ঞেস করা হয় না।

Code review-এর পাইপলাইন আর ফিরতি পথ

Author self-review → automated checks → human review → merge, with feedback loops back to the author

Code review-এর পাইপলাইন: লেখক, যন্ত্র, মানুষ, merge — ফিরতি পথসহ ১ · লেখক: নিজে review ২ · যন্ত্র: CI-এর check ৩ · মানুষ: reviewer ৪ · Merge diff পড়া · test চালানো pre-commit hook formatter · linter · type test · SAST যুক্তি · নকশা · নিরাপত্তা জ্ঞান ভাগাভাগি approve + সব check সবুজ লাল check মন্তব্য ফিরতি পথে লেখক ঠিক করে আবার push করেন
ছবি ৪৪.১ — নিচের দিকে তীর মানে পরিবর্তনটি এক ধাপ এগোল; ড্যাশ দেওয়া ফিরতি তীর মানে কাজটি লেখকের কাছে ফিরল। যত আগের ধাপে ভুল ধরা পড়ে, ফিরতি পথ তত ছোট আর সস্তা। SAST = static application security testing.
  1. লেখকের নিজের review. নিজের diff পড়া, test চালানো; একটি pre-commit hook commit-এর মুহূর্তেই formatter আর linter চালায় (Git hook পর্ব ৫৬-এ)।
  2. যন্ত্রের check. Push হলেই CI একই check আবার চালায়, কারণ সবার কম্পিউটারে hook না-ও থাকতে পারে। ফল PR-এ status check; branch protection লাল check-এ merge আটকায় (pipeline পর্ব ৫৮-এ)।
  3. মানুষের review. সবুজ check-এর পরে, শুধু সেই প্রশ্নে যা যন্ত্র পারে না।
  4. Merge. Approval, সব check আর thread মীমাংসার পরে (পদ্ধতি পর্ব ১৬-এ)।

কেন যন্ত্র আগে? খরচ: linter কয়েক সেকেন্ডে যা বলে, মানুষ বললে দুজনের মিনিট যায়, সাথে অপেক্ষা। আর সম্পর্ক: “এখানে space বেশি” মানুষ বললে বিরক্তি জমে, যন্ত্র বললে কারও ব্যক্তিগত ব্যাপার নয়। Formatting-এর তর্ক একবার config-এ মীমাংসা করুন।

৩ · যন্ত্রের review — formatter, linter, static analysis আর type checker

চার ধরনের tool, চার রকম কাজ। নামগুলো কখনো মিশে যায় (অনেক linter এখন formatting-ও করে), তবু ভাগটি মাথায় রাখা কাজের:

ধরনকী করেউদাহরণ (লেখার সময়ের পরিচিত tool)
Formatterকোডের চেহারা — indentation, লাইনের দৈর্ঘ্য, উদ্ধৃতি চিহ্ন — একটি নির্দিষ্ট ধাঁচে নিজেই বদলে দেয়; অর্থ বদলায় নাPrettier (JS), Black বা ruff format (Python), gofmt (Go)
Linterনিয়ম ধরে সন্দেহজনক ধাঁচ খোঁজে: অব্যবহৃত variable, bare except, == বনাম ===, ভুলে থাকা printESLint (JS/TS), Ruff, flake8, pylint (Python)
Static analysisকোড না চালিয়েই গভীর বিশ্লেষণ — data কোথা থেকে কোথায় যায়, null হতে পারে কি না; নিরাপত্তার দিকটিকে বলে SASTCodeQL, SonarQube-ধাঁচের platform, Semgrep
Type checkerType-এর অসংগতি ধরে: str-এর জায়গায় int, None হতে পারে এমন মান সরাসরি ব্যবহারmypy বা pyright (Python type hint), TypeScript-এর tsc

“Lint” নামটি এসেছে ১৯৭০-এর দশকের শেষে Bell Labs-এ Stephen C. Johnson-এর লেখা C-এর একটি checker থেকে — কাপড়ে লেগে থাকা আঁশ (lint) ঝাড়ার মতো। কোন tool সবচেয়ে প্রচলিত, তা বছর বছর বদলায়; নামের চেয়ে ভাগটি গুরুত্বপূর্ণ।

Linter ভেতরে কীভাবে কাজ করে — একটি খুদে linter

বেশিরভাগ linter কোডকে লেখা হিসেবে নয়, গাছ হিসেবে পড়ে — abstract syntax tree (AST). Python-এর standard library-র ast module ঠিক এই গাছ দেয়। নিচের ৩৫ লাইনের খেলনা linter চারটি নিয়ম মানে: (W1) list বা dict-এর মতো বদলযোগ্য default argument, (W2) bare except, (W3) রেখে দেওয়া print, (W4) মান বসানো হয়েছে কিন্তু কখনো পড়া হয়নি এমন variable.

import ast
# Toy linter: 4 rules on Python's own syntax tree (real tools: Ruff, flake8, pylint).
SOURCE = '''def add_fee(order, items=[]):
    total = 0
    discount = 5
    for it in items:
        total += it["price"]
    print("debug total", total)
    try:
        order.save(total)
    except:
        pass
    return total
'''
def lint(src):
    out = []
    for fn in ast.walk(ast.parse(src)):
        if not isinstance(fn, ast.FunctionDef): continue
        for d in fn.args.defaults:                                  # W1
            if isinstance(d, (ast.List, ast.Dict, ast.Set)):
                out.append((d.lineno, "W1 mutable default argument"))
        stored, used = {}, set()
        for n in ast.walk(fn):
            if isinstance(n, ast.Name):
                if isinstance(n.ctx, ast.Store): stored.setdefault(n.id, n.lineno)
                else: used.add(n.id)
            elif isinstance(n, ast.ExceptHandler) and n.type is None:  # W2
                out.append((n.lineno, "W2 bare except"))
            elif isinstance(n, ast.Call) and getattr(n.func, "id", "") == "print":
                out.append((n.lineno, "W3 print left in"))           # W3
        out += [(ln, f"W4 unused variable '{v}'")                      # W4
                for v, ln in stored.items() if v not in used]
    return sorted(out)
for line, msg in lint(SOURCE):
    print(f"line {line}: {msg}")

আউটপুট:

line 1: W1 mutable default argument
line 3: W4 unused variable 'discount'
line 6: W3 print left in
line 9: W2 bare except

চারটি নিয়ম চারটি ভুল ধরল, প্রতিটি লাইন নম্বরসহ। ast.walk গাছের প্রতিটি node ঘুরে দেখে; ast.Name node-এর ctx বলে দেয় নামটিতে মান বসানো হচ্ছে (Store) না পড়া হচ্ছে (Load). discount শুধু Store-এ আছে, তাই W4. খেয়াল করুন total ধরা পড়েনি — total += … একই সাথে পড়ে আর লেখে, আর পরে return total-এ পড়া হয়েছে। কেন W1 গুরুত্বপূর্ণ? Python default মানটি function তৈরির সময় একবারই বানায়; তাই items=[] লিখলে প্রতিটি ডাকে একই list ভাগ হয়, আর একবারের যোগ পরের ডাকে রয়ে যায় (নিচের MCQ-এর তৃতীয় প্রশ্নে নিজে যাচাই করুন)। প্রচলিত সমাধান items=None, তারপর ভেতরে if items is None: items = []. W2-র except: pass হলো swallowed exception — order.save ব্যর্থ হলেও কেউ জানবে না (পর্ব ৩১-এর error handling). খেলনার সীমা: এটি অব্যবহৃত parameter দেখে না, _ নামকে আলাদা করে না, nested function-এর scope বোঝে না; আসল tool এগুলো যত্নে সামলায়, আর প্রতিটি নিয়ম বন্ধ বা নির্দিষ্ট লাইনে চুপ করানো যায়।

Static analysis কী বাড়তি দেয় — আর তার দাম

Linter সাধারণত এক ফাইলের ধাঁচ দেখে। Static analysis tool পুরো প্রোগ্রাম জুড়ে data-র পথ অনুসরণ করতে পারে: “এই form-এর মান কোনো যাচাই ছাড়াই ঘুরে ঘুরে একটি SQL query-তে ঢুকছে” — একে বলে taint analysis. CodeQL কোডকে database-এর মতো query করতে দেয়; SonarQube-ধাঁচের platform নিয়ম, জটিলতা আর পুনরাবৃত্তির হিসাব এক ড্যাশবোর্ডে দেখায় — সুবিধা সংস্করণ আর license-ভেদে আলাদা।

দাম হলো false positive: tool এমন জায়গাকে সমস্যা বলে, যা আসলে সমস্যা নয়। বেশি হলে মানুষ সতর্কবার্তা পড়াই ছেড়ে দেন। তাই অভিজ্ঞ দল অল্প নিয়মে শুরু করে, পুরোনো কোডের হাজারো সতর্কবার্তা একবারে না মিটিয়ে শুধু নতুন কোডে নিয়ম বাধ্যতামূলক করে (baseline), আর কোনো নিয়ম চুপ করালে কারণ লেখে — legacy কোডে ধাপে ধাপে উন্নতির কৌশল পর্ব ৪৩-এ (Technical debt, legacy code আর maintenance).

Commit-এর মুহূর্তে — pre-commit hook

Python জগতে pre-commit নামের জনপ্রিয় framework একটি YAML ফাইল পড়ে commit-এর আগে tool চালায় (JS জগতে husky আর lint-staged একই কাজ করে)। নিচে rev-এ লেখার সময়ের সংস্করণ; ব্যবহারের আগে সর্বশেষ সংস্করণ আর hook-এর নাম documentation-এ মিলিয়ে নিন:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff          # linter; --fix applies safe automatic fixes
        args: [--fix]
      - id: ruff-format   # formatter
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: detect-private-key
      - id: check-added-large-files

শেষের দুটি hook ভুল করে private key বা বিশাল ফাইল commit হওয়া আটকায়। তবে hook লেখকের কম্পিউটারে চলে আর git commit --no-verify দিয়ে পাশ কাটানো যায় — এটি সুবিধা, নিশ্চয়তা নয়। নিশ্চয়তা আসে CI-তে একই check আর branch protection থেকে।

৪ · কী দেখবেন, কোন ক্রমে — একটি diff ধাপে ধাপে

বাড়ি তৈরির নকশা যাচাইয়ের কথা ভাবুন। প্রকৌশলী প্রথমে দেখেন ভিত আর কলাম ঠিক আছে কি না; তারপর ঘরের বিন্যাস; একেবারে শেষে জানালার রং। আগে রং নিয়ে আধঘণ্টা কথা বলে পরে দেখা গেল কলাম ভুল — তাহলে রং নিয়ে আলোচনাটাই নষ্ট। Review-ও একই: বড় প্রশ্ন আগে, কারণ বড় প্রশ্নের উত্তর ছোটগুলোকে অপ্রাসঙ্গিক করে দিতে পারে।

  1. প্রসঙ্গ: PR-এর বিবরণ আর linked issue — কী সমস্যা সমাধান হচ্ছে? পরিবর্তনটি আদৌ দরকার কি?
  2. সঠিকতা আর test: কোডটি যা বলছে তা করে কি না; সীমার মান (০, ঋণাত্মক, খালি list, সর্বোচ্চ); test আছে কি না আর সেগুলো আসলেই আচরণ যাচাই করে কি না।
  3. নকশা আর architecture-এ মানানসই: কোডটি ঠিক স্তরে বসেছে কি না — যেমন domain-এর নিয়মের ভেতরে সরাসরি database ডাকা (পর্ব ৩৮ Software architecture আর পর্ব ৪০ Clean আর hexagonal architecture).
  4. পড়ার সুবিধা আর নাম: ছয় মাস পরে নতুন কেউ বুঝবেন কি? Function একটি কাজ করে কি না (পর্ব ৪১ Clean code, পর্ব ৪২ Code smell আর refactoring).
  5. নিরাপত্তা: বাইরের ইনপুট, secret, কে কী করতে পারবেন (অংশ ৫)।
  6. Performance — যেখানে প্রাসঙ্গিক: loop-এর ভেতরে query, অপ্রয়োজনীয় বড় কপি; কিন্তু মাপা ছাড়া অনুমানে micro-optimisation চাওয়া নয়।
  7. Error handling: ব্যর্থ হলে কী হয়? Error চাপা পড়ছে কি?
  8. নথি: public interface বদলালে docstring, README, changelog (পর্ব ১৮); ডাকে এমন অন্য কোড ভাঙছে কি না।

এই ক্রম মনোযোগ ভাগ করার জন্য, গুরুত্বের র‍্যাংক নয়। নিরাপত্তার ছিদ্র পঞ্চম ধাপে চোখে পড়লেও সেটি সবচেয়ে বড় blocking সমস্যা হতে পারে। আর security-sensitive পরিবর্তনে (login, টাকা-পয়সা, অনুমতি) অনেক দল নিরাপত্তার দিকটি প্রথমেই আলাদা করে দেখে।

এবার একটি কাল্পনিক অনলাইন বাজার অ্যাপের PR. লেখক coupon-এর হিসাবে “ন্যূনতম কেনাকাটা” শর্ত যোগ করেছেন। আসল git diff:

$ git diff -- coupon.py
diff --git a/coupon.py b/coupon.py
index 1073cfa..b77f764 100644
--- a/coupon.py
+++ b/coupon.py
@@ -1,5 +1,14 @@
-def apply_coupon(cart, code):
-    coupon = coupons.find(code)
+def apply_coupon(cart, code, user):
+    print("coupon", code, user.phone)
+    sql = f"SELECT * FROM coupons WHERE code = '{code}'"
+    coupon = db.fetch_one(sql)
     if coupon is None:
         return cart.total
-    return cart.total - coupon.amount
+    try:
+        if coupon["min_total"] > cart.total:
+            return cart.total
+    except:
+        pass
+    for item in cart.items:
+        db.log_coupon_view(item.id, code)
+    return cart.total - coupon["amount"]

নিচের stepper-এ নতুন ফাইলটি (ডানে লাইন নম্বর) ওপরের ক্রমে পড়া হচ্ছে। প্রতিটি ধাপে কোন লাইন দেখছেন, কী পেলেন, আর মন্তব্যটি কোন ধরনের:

1def apply_coupon(cart, code, user):2    print("coupon", code, user.phone)3    sql = f"SELECT * FROM coupons WHERE code = '{code}'"4    coupon = db.fetch_one(sql)5    if coupon is None:6        return cart.total7    try:8        if coupon["min_total"] > cart.total:9            return cart.total10    except:11        pass12    for item in cart.items:13        db.log_coupon_view(item.id, code)14    return cart.total - coupon["amount"]
Review-এর খাতা
ধাপ
যা পেলাম
মন্তব্যের ধরন
  1. প্রসঙ্গ আর test আগে। বিবরণ বলছে “ন্যূনতম কেনাকাটার শর্ত”, কিন্তু test ফাইল নেই, আর signature-এ নতুন user, যার কারণ বিবরণে নেই। প্রথম মন্তব্য একটি প্রশ্ন: “user কেন লাগছে? নতুন শর্তের test কোথায়?”
  2. সঠিকতা। লাইন ১৪: coupon-এর amount কার্টের মোটের চেয়ে বেশি হলে (৩০০ টাকার কার্টে ৫০০ টাকার coupon) ফল ঋণাত্মক — দোকান উল্টো টাকা দেবে? Blocking.
  3. নকশা। লাইন ৩–৪: আগে coupon আসত repository (coupons.find) দিয়ে; এখন হিসাবের function-এর ভেতরেই SQL. দলের architecture-এ domain-এর নিয়ম database জানে না (পর্ব ৪০)। আগের পথে ফিরলে নিরাপত্তার সমস্যাটিও অনেকটা মেটে।
  4. পড়ার সুবিধা। Function এখন চারটি কাজ করে: খোঁজা, শর্ত যাচাই, analytics-এর log, হিসাব; আর user শুধু debug print-এ। Log আলাদা function-এ গেলে ভালো — তবে merge আটকানোর মতো নয়।
  5. নিরাপত্তা। লাইন ৩: ব্যবহারকারীর লেখা code সরাসরি SQL-এর লেখায় — SQL injection-এর ক্লাসিক রূপ; সমাধান parameterized query. লাইন ২: গ্রাহকের ফোন নম্বর log-এ — ব্যক্তিগত তথ্য log-এ রাখা চলে না। দুটিই blocking.
  6. Performance. লাইন ১২–১৩: যত item, তত query — N+1 query-র ধাঁচ, যা বিক্রির ভিড়ে checkout ধীর করতে পারে। এক call-এ সব item-এর log — প্রস্তাব, কারণসহ।
  7. Error handling. লাইন ৭–১১: min_total না থাকলে KeyError, আর bare except: pass তা গিলে ফেলে — শর্ত নীরবে বাদ পড়ে। Blocking: নির্দিষ্ট exception ধরা, বা default মান ঠিক করা।
  8. নথি আর সিদ্ধান্ত। নতুন parameter-এ apply_coupon(cart, code) ডাকা সব পুরোনো জায়গা ভাঙবে। মোট চারটি blocking (ঋণাত্মক মোট, SQL injection, ফোন নম্বর log, চাপা পড়া error) — request changes, সাথে সারাংশ আর একটি প্রশংসা: ন্যূনতম শর্তের ধারণাটি পরিষ্কার।

লক্ষ করুন, এর অন্তত দুটি — bare except আর রেখে দেওয়া print — অংশ ৩-এর linter-ই ধরত, আর SQL-এর লেখায় ইনপুট বসানো অনেক SAST tool ধরে। যন্ত্র আগে চললে reviewer-এর খাতা ছোট হতো, আর মনোযোগ যেত ঋণাত্মক মোট আর নকশার প্রশ্নে।

Reviewer-এর চেকলিস্ট

নিচের code review checklist একটি শুরুর খসড়া; দলের অভিজ্ঞতা অনুযায়ী বদলান, আর যে প্রশ্নের উত্তর যন্ত্র দিতে পারে, সেটি তালিকা থেকে সরিয়ে tool-এ দিন। লম্বা তালিকা কেউ পড়ে না — দশ-বারোটি প্রশ্নই যথেষ্ট।

৫ · নিরাপত্তার চোখে review — ইনপুট, secret আর অনুমতি

সব reviewer নিরাপত্তা-বিশেষজ্ঞ নন, হওয়ার দরকারও নেই। কিন্তু কয়েকটি প্রশ্ন প্রতিটি review-এ তোলা যায়, আর এগুলোই বেশিরভাগ সাধারণ ভুল ধরে। সবকিছুই এখানে প্রতিরক্ষার দিক থেকে — নিজের কোডকে শক্ত করার জন্য।

ইনপুট কোথা থেকে আসছে?

Form, URL, file, অন্য service — সবই trust boundary-র বাইরের। ব্যবহারের আগে input validation; আর SQL, shell command বা HTML-এ জোড়ার সময় parameterized query বা escape — লেখা জোড়া দিয়ে নয়।

কোনো secret আছে কি?

API key, password, token কোডে বা config-এ লেখা secret একবার push হলে history-তে থেকে যায়। দেখামাত্র blocking; আর ফাঁস হয়ে থাকলে মুছে ফেলাই যথেষ্ট নয় — key বদলাতে (rotate) হবে (পর্ব ৫৭)।

কে এটি করতে পারবেন?

Authorization: login থাকলেই যথেষ্ট নয় — এই ব্যবহারকারী কি এই order-টি দেখতে বা বদলাতে পারেন? URL-এ অন্যের order নম্বর বসালে কী হয়? Least privilege মানা হচ্ছে কি?

Log আর error-এ কী যাচ্ছে?

ফোন নম্বর, NID, password-এর মতো ব্যক্তিগত তথ্য log-এ নয়; ব্যবহারকারীকে দেখানো error-এ ভেতরের stack trace বা database-এর বিবরণ নয়।

এই প্রশ্নগুলোর পেছনে গোছানো জ্ঞানভাণ্ডার আছে: OWASP (Open Worldwide Application Security Project)-এর Code Review Guide আর সাধারণ ঝুঁকির তালিকা “OWASP Top 10”, আর MITRE-এর CWE তালিকা, যেখানে প্রতিটি দুর্বলতার ধরনের একটি নম্বর আছে (যেমন SQL injection-এর জন্য CWE-89). Python আর JavaScript-এ নির্দিষ্ট উদাহরণসহ আরও জানতে Python-এ নিরাপদ কোড আর JavaScript-এ web নিরাপত্তা পর্ব দেখুন; বড় system-এর নকশার দিক System Design-এর নিরাপত্তা পর্বে।

ঝুঁকিপূর্ণ ফাইলে (login, payment, অনুমতি) নির্দিষ্ট দলের review বাধ্যতামূলক করতে CODEOWNERS কাজে আসে — পর্ব ১৬-এ দেখেছেন। আর repository-তে secret ঢোকা আটকানোর স্বয়ংক্রিয় ব্যবস্থা (secret scanning, push-এর সময় আটকানো) আর ফাঁস হলে history পরিষ্কার করার ধাপ পর্ব ৫৭-এ (Git-এ নিরাপত্তা)।

৬ · কীভাবে মন্তব্য করবেন — সদয়, নির্দিষ্ট, কারণসহ, আর অবস্থা স্পষ্ট

একই সমস্যার কথা দুভাবে বলা যায়, আর ফল হয় সম্পূর্ণ আলাদা। দর্জির দোকানে ভাবুন: “জামাটা খারাপ হয়েছে” শুনে দর্জি কিছুই ঠিক করতে পারবেন না। “ডান হাতা বাঁ হাতার চেয়ে প্রায় এক ইঞ্চি লম্বা — মাপের কাগজে দুটোই ২২ ইঞ্চি লেখা ছিল” শুনলে তিনি জানেন কী, কেন, আর কোথায় ঠিক করতে হবে। ভালো review মন্তব্যের ছয়টি গুণ:

  • কোড নিয়ে, মানুষ নিয়ে নয়: “আপনি ভুল করেছেন” নয়, “এই loop খালি list-এ …”।
  • নির্দিষ্ট: কোন লাইন, কোন ইনপুটে, কী ঘটে।
  • কারণসহ: bug, নিরাপত্তা, দলের নিয়ম, নাকি রুচি? কারণ জানলে লেখক পরের বার নিজেই ধরবেন।
  • কাজে লাগানো যায়: একটি প্রস্তাব, উদাহরণ বা প্রশ্ন।
  • অবস্থা স্পষ্ট: merge আটকাবে (blocking), ঐচ্ছিক (nit), নাকি শুধু প্রশ্ন?
  • আদেশের বদলে প্রশ্ন, আর প্রশংসাও: “এখানে None এলে কী হবে?” লেখককে ভাবায়, আর হয়তো এমন কারণ বেরোয় যা reviewer জানতেন না। ভালো কাজ চোখে পড়লে লিখুন — কোন ধাঁচ চালিয়ে যাওয়া উচিত, সেটিও শেখা।

অবস্থা স্পষ্ট করার একটি প্রকাশিত প্রস্তাব হলো Conventional Comments (conventionalcomments.org; আমাদের জানামতে Paul Slaughter-এর প্রস্তাব)। এতে প্রতিটি মন্তব্য শুরু হয় একটি লেবেলে — যেমন praise, nitpick, suggestion, issue, question, thought, todo — আর ঐচ্ছিক “decoration” বন্ধনীতে, যেমন (blocking), (non-blocking). গঠন: <label> (<decoration>): <subject>, তারপর দরকারে আলোচনা। এটি একটি সম্প্রদায়ের প্রস্তাব, কোনো মান (standard) নয়; অনেক দল নিজের ছোট রূপ বানায় (শুধু nit: আর blocking:)। যেভাবেই হোক, blocking comment কোনটি, তা পাঠকের অনুমানের ওপর ছেড়ে দেবেন না।

নিচের খেলনা প্রোগ্রামটি কয়েকটি মন্তব্য পরীক্ষা করে: কড়া বা তাচ্ছিল্যের শব্দ, লেবেল নেই, blocking কি না অস্পষ্ট, আর কারণ নেই। এটি শুধু শব্দ মেলায় — ভাষা বোঝে না, তা নিজেই দেখবেন:

// Toy review-comment checker: keyword rules only, no real language understanding.
const HARSH = ["stupid", "terrible", "obviously", "just", "lazy", "why would you"];
const LABELS = ["issue", "suggestion", "nitpick", "question", "praise", "thought"];
const NEEDS_REASON = ["issue", "suggestion"];          // nitpick/question/praise: optional
const REASON = ["because", "so that", "otherwise", "since", "risk"];

function check(text) {
  const t = text.toLowerCase(), notes = [];
  const label = LABELS.find(l => t.startsWith(l));
  const harsh = HARSH.filter(w => new RegExp("\\b" + w + "\\b").test(t));
  if (harsh.length) notes.push("harsh/dismissive words: " + harsh.join(", "));
  if (!label) notes.push("no label: is this blocking, a nit or a question?");
  if (NEEDS_REASON.includes(label)) {
    if (!/\((non-)?blocking\)/.test(t)) notes.push("blocking status unclear");
    if (!REASON.some(r => t.includes(r))) notes.push("no reason (why does it matter?)");
  }
  return { label: label || "-", notes };
}

const comments = [
  "This is terrible. Just use a map.",
  "issue (blocking): amount can be negative because the form allows '-'. Add a check?",
  "suggestion: extract the fee rule into a function.",
  "nitpick (non-blocking): rename tmp to pendingOrders.",
  "Why would you call the DB inside the loop?",
  "question: can this list be empty? If so, line 14 throws.",
  "praise: this just works with the old API, nice!",
];
for (const c of comments) {
  const r = check(c);
  console.log((r.notes.length ? "FLAG " : "ok   ") + r.label.padEnd(11) + c.slice(0, 34) + "…");
  r.notes.forEach(n => console.log("       - " + n));
}
console.log("\nTemplate: <label> (<blocking|non-blocking>): <what you saw>, <why it matters>. <suggestion or question>");

আউটপুট:

FLAG -          This is terrible. Just use a map.…
       - harsh/dismissive words: terrible, just
       - no label: is this blocking, a nit or a question?
ok   issue      issue (blocking): amount can be ne…
FLAG suggestion suggestion: extract the fee rule i…
       - blocking status unclear
       - no reason (why does it matter?)
ok   nitpick    nitpick (non-blocking): rename tmp…
FLAG -          Why would you call the DB inside t…
       - harsh/dismissive words: why would you
       - no label: is this blocking, a nit or a question?
ok   question   question: can this list be empty? …
FLAG praise     praise: this just works with the o…
       - harsh/dismissive words: just

Template: <label> (<blocking|non-blocking>): <what you saw>, <why it matters>. <suggestion or question>

প্রথম আর পঞ্চম মন্তব্যে দুটি সমস্যা: তাচ্ছিল্যের সুর (“terrible”, “just”, “why would you”) আর লেবেল নেই — লেখক বুঝবেন না এটি merge আটকাচ্ছে কি না। তৃতীয়টি (suggestion) ভদ্র, কিন্তু বলে না কেন extract করা উচিত, বা না করলে merge হবে কি না। দ্বিতীয়টি আদর্শ রূপ: লেবেল, অবস্থা, কী দেখেছেন, কেন গুরুত্বপূর্ণ (“because the form allows '-'”), আর প্রশ্নের আকারে প্রস্তাব। nitpick, question, praise-এ নিয়মটি কারণ চায় না — নকশার সিদ্ধান্ত, কোডের শুরুতেই লেখা। শেষ লাইনটি খেয়াল করুন: “this just works … nice!” প্রশংসা, অথচ “just” শব্দের জন্য FLAG — এটি false positive. শব্দ মিলিয়ে সুর বোঝা যায় না; এমন tool শুধু নিজের লেখা আরেকবার পড়ার মনে-করানো হিসেবে কাজের, কাউকে বিচার করতে নয়। শেষে ছাপা template-টি নিজের মন্তব্যের কাঠামো হিসেবে রাখতে পারেন।

এবার একটি কড়া মন্তব্য ধাপে ধাপে কাজের মন্তব্যে বদলানো দেখুন। বিষয়: অংশ ৪-এর coupon.py-এর লাইন ১২–১৩, যেখানে প্রতিটি item-এর জন্য একটি query.

ধাপ ১ · মূল মন্তব্যcoupon.py:12
Why would you query the DB inside a loop??
This is obviously wrong. Fix it.
ধাপ ২ · মানুষ নয়, কোডcoupon.py:12
This loop runs one DB query per cart item.
ধাপ ৩ · নির্দিষ্ট করাcoupon.py:12-13
This loop runs one DB query per cart item
(db.log_coupon_view on line 13). A cart with
40 items makes 40 extra queries per checkout.
ধাপ ৪ · কেন গুরুত্বপূর্ণcoupon.py:12-13
This loop runs one DB query per cart item
(line 13). A 40-item cart makes 40 extra
queries per checkout, because each call is a
separate round trip; during sale rushes that
adds up on the checkout path.
ধাপ ৫ · প্রস্তাব, প্রশ্নের আকারেcoupon.py:12-13
... adds up on the checkout path.
Could we log all item ids in one call, e.g.
db.log_coupon_views([i.id for i in cart.items], code)?
Or is there a reason it must be per item?
ধাপ ৬ · লেবেল আর অবস্থাচূড়ান্ত রূপ
suggestion (non-blocking): this loop runs one DB
query per cart item (line 13). A 40-item cart
makes 40 extra queries per checkout, because each
call is a separate round trip. Could we log all
ids in one call, e.g. db.log_coupon_views(ids, code)?
Happy to merge without it if a follow-up issue exists.
  1. মূল মন্তব্য। “why would you” মানুষটিকে প্রশ্নবিদ্ধ করে, “obviously” বোঝায় লেখক বোকা, আর “Fix it” বলে না কী, কেন, বা merge আটকাবে কি না। লেখক হয় আত্মরক্ষায় যাবেন, নয়তো যা-তা বদলাবেন।
  2. মানুষ নয়, কোড। কর্তা এখন “this loop” — পর্যবেক্ষণ, অভিযোগ নয়। কিন্তু এখনো অস্পষ্ট: কোন লাইন, সমস্যা কত বড়?
  3. নির্দিষ্ট। লাইন নম্বর, function-এর নাম আর একটি মূর্ত উদাহরণ: ৪০টি item-এ ৪০টি বাড়তি query.
  4. কেন গুরুত্বপূর্ণ। “because each call is a separate round trip” — কারণ জানলে লেখক পরের বার নিজেই ধাঁচটি চিনবেন; আর বলা হলো ব্যথা কোথায়: checkout-এ, বিক্রির ভিড়ে।
  5. প্রস্তাব, প্রশ্নের আকারে। নির্দিষ্ট বিকল্প, কিন্তু আদেশ নয় — “Could we …?”, আর খোলা দরজা: “Or is there a reason …?” হয়তো analytics দলের item-ভিত্তিক log দরকার, যা reviewer জানেন না।
  6. লেবেল আর অবস্থা। suggestion (non-blocking): না মেটালেও merge হতে পারে, শেষ লাইনে কীভাবে — follow-up issue খুলে। বাংলায়: “প্রস্তাব (merge আটকাবে না): লাইন ১৩-এর loop প্রতি item-এ একটি query করে … সব id এক call-এ পাঠানো যায় কি?”

৭ · লেখকের দায়িত্ব আর feedback নেওয়া — fixup commit দিয়ে সাড়া

Review দুজনের কাজ। reviewer-এর সময় বাঁচানোর সবচেয়ে বড় সুযোগ থাকে লেখকের হাতে — PR খোলার আগে।

লেখক যা করবেন

  • Self-review: PR খুলে platform-এর diff পাতায় নিজের পরিবর্তন পুরোটা পড়ুন, যেন অন্যের কোড — debug-এর print, মন্তব্য করে রাখা কোড, অপ্রাসঙ্গিক ফাইল এখানেই ধরা পড়ে।
  • ছোট, এক কাজের PR (পর্ব ১৬); কেন জরুরি, অংশ ৮-এর মডেলে দেখবেন।
  • প্রসঙ্গ: কী, কেন, কোন বিকল্প বাদ দিলেন, reviewer কোথায় বেশি মন দেবেন (“refund.py-এর শর্তটি মূল; বাকিটা নাম বদল”); UI বদলালে আগে-পরের screenshot.
  • Test নতুন আচরণের, আর হাতে কীভাবে পরীক্ষা করলেন। “Locally works” প্রমাণ নয়।
  • CI সবুজ করে তবে review চাওয়া — লাল check নিয়ে কাউকে ডাকা মানে তাঁর সময়কে যন্ত্রের সমান ধরা।
  • যান্ত্রিক আর অর্থপূর্ণ বদল আলাদা commit-এ; reviewer git diff --color-moved বা -w দিয়ে যান্ত্রিক অংশ দ্রুত পার করেন।

Feedback নেবেন কীভাবে

নিজের কোডে মন্তব্যের তালিকা দেখলে খারাপ লাগা স্বাভাবিক — ক্রিকেটে ক্যাচ ফসকানোর পরে অধিনায়কের দিকে তাকানোর মতো। কয়েকটি অভ্যাস সাহায্য করে:

  • মন্তব্য কোডের ওপর, আপনার ওপর নয় — আর প্রতিটি মন্তব্য একজন সহকর্মীর দেওয়া সময়।
  • প্রতিটি thread-এর উত্তর দিন: “ঠিক করেছি, df0e077”, বা কারণসহ দ্বিমত। যুক্তি দিয়ে দ্বিমত অভদ্রতা নয়।
  • তর্ক তিন-চার বার্তা পেরোলে ছোট একটা call, তারপর সিদ্ধান্তটি thread-এ লিখে রাখা।
  • একই মন্তব্য বারবার পেলে নিজের চেকলিস্টে যোগ করুন, বা একটি linter নিয়ম প্রস্তাব করুন।

এখন একটি কাল্পনিক refund-এর PR. Branch fix/refund-check-এ দুটি commit: 322edad “Reject refunds larger than the payment” আর 8346907 “Add refund tests”. Reviewer তিনটি মন্তব্য দিয়েছেন। লেখক কীভাবে সাড়া দেন — দল মন্তব্যের সময় history-বদল চায় না, শুধু merge-এর আগে একবার পরিষ্কার (পর্ব ১৫ Rebase-এর autosquash):

PR: fix/refund-check
branch-এ commit
খোলা thread
push
reviewer দেখেন
  1. তিনটি মন্তব্য এল। (১) issue (blocking) 322edad-এ: “শূন্য বা ঋণাত্মক refund আটকানো হচ্ছে না।” (২) nitpick 8346907-এ: “test_too_large কী যাচাই করে, নাম থেকে স্পষ্ট নয়।” (৩) question: “নিজস্ব exception না করে ValueError কেন?”
  2. আগে উত্তর, কোড পরে। প্রশ্নের উত্তর কোড না বদলেই: “দলের অন্য জায়গাতেও ইনপুট-ভুলে ValueError; নিজস্ব exception-এর আলাদা issue খুলতে পারি।” বাকি দুটিতে “একমত, ঠিক করছি।”
  3. Blocking-টির fixup. শর্ত হলো amount <= 0 or amount > order.paid; git commit --fixup=322edad বানাল df0e077 — কোন commit-এর সংশোধন, বার্তাতেই লেখা।
  4. Nit-এর fixup আর push. Test-এর নাম বদল আর ঋণাত্মক refund-এর নতুন test: --fixup=8346907 → 3b7cb93. সাধারণ git push — history বদলায়নি। প্রতিটি thread-এ commit-এর hash দিয়ে উত্তর।
  5. Reviewer শুধু নতুনটুকু দেখেন। History অক্ষত, তাই শেষ review-এর পরের দুটি fixup পড়েই যাচাই — পুরো PR আবার নয়। Approve.
  6. Merge-এর আগে autosquash. git rebase -i --autosquash main প্রতিটি fixup-কে লক্ষ্যের সাথে মেশাল: দুটি commit, নতুন hash af51301 আর 6ea1995; নিজের branch-এ --force-with-lease. git range-diff দেখায় squash-এ নতুন কিছু ঢোকেনি।
  7. Merge. Rebase-merge বা merge commit-এর দলে main-এ যায় দুটি অর্থপূর্ণ commit, “fixup!” নয়। Squash merge-এর দলে autosquash-এর ধাপটাই লাগত না (পর্ব ১৬)।

একই ধাপগুলোর আসল Git আউটপুট (Git ২.৫৩-এ; লেখক Rahim, সময় স্থির রাখা — তাই hash পুনরুৎপাদনযোগ্য; remote একটি স্থানীয় bare repository). Interactive rebase-এ editor খুললে Git নিজেই fixup লাইনগুলো সঠিক জায়গায় সাজিয়ে দেয়; তালিকা না বদলে সংরক্ষণ করে বন্ধ করলেই চলে:

$ git commit --fixup=322edad
[fix/refund-check df0e077] fixup! Reject refunds larger than the payment
 1 file changed, 2 insertions(+), 2 deletions(-)
$ git commit --fixup=8346907
[fix/refund-check 3b7cb93] fixup! Add refund tests
 1 file changed, 5 insertions(+), 1 deletion(-)
$ git push
To ../origin.git
   8346907..3b7cb93  fix/refund-check -> fix/refund-check
$ git log --oneline main..
3b7cb93 fixup! Add refund tests
df0e077 fixup! Reject refunds larger than the payment
8346907 Add refund tests
322edad Reject refunds larger than the payment
$ git rebase -i --autosquash main
Successfully rebased and updated refs/heads/fix/refund-check.
$ git log --oneline main..
6ea1995 Add refund tests
af51301 Reject refunds larger than the payment
$ git push --force-with-lease
To ../origin.git
 + 3b7cb93...6ea1995 fix/refund-check -> fix/refund-check (forced update)
$ git range-diff --creation-factor=100 main 8346907 HEAD
1:  322edad ! 1:  af51301 Reject refunds larger than the payment
    @@ refund.py
     @@
      def refund(order, amount):
          """Give money back to the customer."""
    -+    if amount > order.paid:
    -+        raise ValueError("refund exceeds payment")
    ++    if amount <= 0 or amount > order.paid:
    ++        raise ValueError("refund must be between 0 and the payment")
          order.balance -= amount
          order.history.append(("refund", amount))
          return order.balance
2:  8346907 ! 2:  6ea1995 Add refund tests
    @@ test_refund.py (new)
     +    paid, balance, history = 500, 0, []
     +
     +class RefundTest(unittest.TestCase):
    -+    def test_too_large(self):
    ++    def test_refund_larger_than_paid_is_rejected(self):
     +        with self.assertRaises(ValueError):
     +            refund(Order(), 600)
    ++
    ++    def test_negative_refund_is_rejected(self):
    ++        with self.assertRaises(ValueError):
    ++            refund(Order(), -50)

প্রথম git push সাধারণ — 8346907..3b7cb93, শুধু সামনে এগোনো। Autosquash-এর পরে history বদলেছে, তাই দ্বিতীয় push-এ + আর “forced update”; --force-with-lease নিশ্চিত করে যে এর মধ্যে remote-এ অন্য কেউ কিছু push করেননি। git range-diff দুটি commit-সারির তুলনা করে: ! মানে “একই commit, বিষয়বস্তু বদলেছে”, আর নিচে “diff-এর diff” — বাইরের -/+ পুরোনো বনাম নতুন সংস্করণ, ভেতরের + মূল পরিবর্তন। এখানে দেখা যাচ্ছে শুধু শর্ত, বার্তা আর test-এর নাম বদলেছে — অপ্রত্যাশিত কিছু নেই। --creation-factor=100 দিয়েছি কারণ প্রথম commit-এর ছোট diff-এর বেশিরভাগই বদলেছে; default মান (৬০) রাখলে Git প্রথম জোড়াটিকে আলাদা commit ভেবে < আর > দেখাত (আমাদের চালানোয় তাই হয়েছিল)। Range-diff-এর বিস্তারিত git help range-diff-এ।

৮ · সময়, চাপ আর অগ্রাধিকার — turnaround, review load আর ঝুঁকির হিসাব

Review-এর গুণমান পরিস্থিতির ওপরও নির্ভর করে। SmartBear-এর Jason Cohen-এর বই Best Kept Secrets of Peer Code Review (২০০৬)-এ বর্ণিত Cisco-র সমীক্ষার সারকথা (পর্ব ১৬-এ সংক্ষেপে দেখেছেন): একবারে বেশি লাইন, খুব দ্রুত পড়া আর একটানা দীর্ঘ review — তিনটিই ত্রুটি খুঁজে পাওয়ার হার কমায়। সংখ্যাগুলো এক পরিবেশের, তাই দিচ্ছি না।

অন্য দিক হলো review turnaround — অনুরোধ থেকে প্রথম সাড়া পর্যন্ত সময়। Google-এর প্রকাশ্য engineering practices নির্দেশিকায় (“Speed of Code Reviews”) আমাদের জানামতে বলা আছে, সাড়া দিতে সর্বোচ্চ এক কর্মদিবস লাগা উচিত। একই নির্দেশিকা বলে: পরিবর্তন নিখুঁত না হলেও সামগ্রিকভাবে কোডের স্বাস্থ্য স্পষ্টভাবে ভালো করলে approve করা উচিত — নিখুঁত কোড নেই, আছে আরও ভালো কোড।

অপেক্ষা কেন এত বাড়ে, তা queue-র গণিত — পর্ব ৬-এর (Kanban আর lean) Little’s law আর WIP. পর্ব ১৬-এর মডেলে সব PR সমান আকারের ছিল; এবার মিশ্র আকার আর reviewer-এর সংখ্যা বদলে দেখুন। অনুমানগুলো বানানো, শুরুতেই লেখা:

// Review load model. ASSUMPTIONS (invented for the model, not measured data):
//  A1 PRs arrive at random, on average one every 2.25 working hours, for 2000 hours
//  A2 PR size: 60% small (20-100 lines), 30% medium (100-400), 10% large (400-1200)
//  A3 one review takes 0.2 h + lines/250 h; FIFO queue; R reviewers, each one PR at a time
//  A4 a review asks for another round with probability min(0.7, lines/1000);
//     the author fixes in 1 h, then the PR joins the queue again
function mulberry32(a) { return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a);
  t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const GROUPS = [["small", 0, 100], ["medium", 100, 400], ["large", 400, 1e9]];
function run(R, seed) {
  const rnd = mulberry32(seed), prs = [];
  for (let t = 0; t < 2000; ) {
    t += -Math.log(1 - rnd()) * 2.25; const u = rnd();
    const L = Math.round(u < 0.6 ? 20 + rnd() * 80 : u < 0.9 ? 100 + rnd() * 300 : 400 + rnd() * 800);
    prs.push({ L, open: t, ready: t, rounds: 0, work: 0 });
  }
  const free = Array(R).fill(0), queue = prs.slice();
  while (queue.length) {
    queue.sort((a, b) => a.ready - b.ready);
    const p = queue.shift(), r = free.indexOf(Math.min(...free)), w = 0.2 + p.L / 250;
    free[r] = Math.max(free[r], p.ready) + w; p.work += w; p.rounds++;
    if (rnd() < Math.min(0.7, p.L / 1000)) { p.ready = free[r] + 1; queue.push(p); } else p.done = free[r];
  }
  const busy = prs.reduce((s, p) => s + p.work, 0);
  console.log(`reviewers=${R}  load per reviewer=${Math.round(busy / 20 / R)}%`);
  // time = share of all review hours spent on this group
  for (const [name, lo, hi] of GROUPS) {
    const g = prs.filter(p => p.L >= lo && p.L < hi), d = g.map(p => p.done - p.open).sort((a, b) => a - b);
    const sum = f => g.reduce((s, p) => s + f(p), 0);
    console.log(`  ${name.padEnd(7)}${String(g.length).padStart(4)} PRs  rounds ${(sum(p => p.rounds) / g.length).toFixed(2)}` +
      `  time ${String(Math.round(sum(p => p.work) / busy * 100)).padStart(2)}%` +
      `  median ${d[g.length >> 1].toFixed(1).padStart(4)} h  p90 ${d[Math.floor(g.length * 0.9)].toFixed(1).padStart(5)} h`);
  }
}
run(1, 44); run(2, 44);

আউটপুট (seed ৪৪, তাই প্রতিবার একই):

reviewers=1  load per reviewer=79%
  small   554 PRs  rounds 1.06  time 16%  median  4.0 h  p90  18.0 h
  medium  252 PRs  rounds 1.36  time 26%  median  7.0 h  p90  24.8 h
  large    82 PRs  rounds 3.06  time 58%  median 16.6 h  p90  86.2 h
reviewers=2  load per reviewer=39%
  small   554 PRs  rounds 1.06  time 17%  median  0.5 h  p90   2.1 h
  medium  252 PRs  rounds 1.32  time 26%  median  1.4 h  p90   5.0 h
  large    82 PRs  rounds 3.04  time 58%  median  7.9 h  p90  30.6 h

তিনটি শিক্ষা। (১) বড় PR অল্প, কিন্তু সময় খায় বেশি: মাত্র ৮২টি বড় PR (মোটের প্রায় ৯%) reviewer-এর মোট সময়ের ৫৮% নেয় — নিজের আকারের জন্য, আর গড়ে ৩টি round-এর জন্য (অনুমান A4). (২) একজন reviewer ৭৯% ব্যস্ত হলেই অপেক্ষা লম্বা: ছোট PR-এর review নিজে মিনিট পঁচিশেক (০.২ + ৬০/২৫০ ঘণ্টা), কিন্তু median সময় ৪ ঘণ্টা, আর প্রতি দশটির একটি ১৮ ঘণ্টার বেশি — কারণ queue-তে সামনে প্রায়ই একটি বড় PR থাকে। (৩) Reviewer দ্বিগুণ হলে অপেক্ষা শুধু অর্ধেক হয় না, তার চেয়ে অনেক বেশি কমে: চাপ ৩৯%-এ নামলে ছোট PR-এর median ০.৫ ঘণ্টা — প্রায় শুধু review-এর নিজের সময়। ঈদের আগে বাস কাউন্টারে একটি বাড়তি জানালা খুললে লাইন হঠাৎ অনেক ছোট হয় — একই অ-রৈখিক আচরণ। একই seed-এ দুই দৃশ্যের PR হুবহু এক, তাই তুলনা ন্যায্য; অন্য seed-এ সংখ্যা বদলায়, প্রবণতা টেকে। সীমা: আসল reviewer অন্য কাজও করেন, আর সব PR সমান জরুরি নয়।

সময় সীমিত হলে কোন PR আগে, কোথায় বেশি মনোযোগ? কিছু দল সহজ একটি ঝুঁকি-হিসাব রাখে — কত লাইন, কতগুলো ফাইল, সংবেদনশীল জায়গা ছুঁয়েছে কি না, code বদলালেও test ছোঁয়া হয়েছে কি না। নিচের ওজনগুলো বানানো:

import math
# Toy "diff risk" score to decide where review attention goes first.
# Weights are invented for teaching; a team would tune them on its own history.
SENSITIVE = ("auth/", "payment/", "migrations/")

def risk(files):              # files: list of (path, added, deleted); code = .py
    code = [f for f in files if f[0].endswith(".py") and not f[0].startswith("tests/")]
    tests = [f for f in files if f[0].startswith("tests/")]
    lines = sum(a + d for _, a, d in code)
    score, why = 10 * math.log2(1 + lines / 50), [f"{lines} code lines"]
    score += 3 * len(code); why.append(f"{len(code)} code files")
    hot = sorted({p for p, _, _ in code if p.startswith(SENSITIVE)})
    if hot: score += 15; why.append("sensitive: " + ", ".join(hot))
    if code and not tests: score += 20; why.append("code changed, no test touched")
    return round(score), why

PRS = {
    "#101 fix typo in receipt": [("ui/receipt.html", 1, 1)],
    "#102 add coupon limit":    [("cart/coupon.py", 30, 4), ("tests/test_coupon.py", 40, 0)],
    "#103 refund rounding":     [("payment/refund.py", 12, 6)],
    "#104 new login throttle":  [("auth/login.py", 140, 20), ("auth/limits.py", 60, 0),
                                 ("config/app.toml", 3, 1), ("tests/test_login.py", 90, 0)],
    "#105 rename helpers":      [(f"util/h{i}.py", 8, 8) for i in range(12)],
}
ranked = sorted(((*risk(f), name) for name, f in PRS.items()), reverse=True)
for score, why, name in ranked:
    print(f"{score:3d}  {name}")
    print("      " + "; ".join(why))

আউটপুট:

 79  #105 rename helpers
      192 code lines; 12 code files; code changed, no test touched
 45  #104 new login throttle
      220 code lines; 2 code files; sensitive: auth/limits.py, auth/login.py
 42  #103 refund rounding
      18 code lines; 1 code files; sensitive: payment/refund.py; code changed, no test touched
 10  #102 add coupon limit
      34 code lines; 1 code files
  0  #101 fix typo in receipt
      0 code lines; 0 code files

হিসাবটি: আকারের অংশ 10 × log2(1 + লাইন/50) — log, কারণ ২০০ লাইন ১০০ লাইনের দ্বিগুণ ঝুঁকি নয়; প্রতিটি code ফাইলে ৩; সংবেদনশীল path-এ ১৫; code বদলেছে অথচ test নেই, তাহলে ২০. যেমন #103: 10 × log2(1.36) ≈ ৪.৪ + ৩ + ১৫ + ২০ ≈ ৪২. এবার সীমাগুলো দেখুন — এগুলোই আসল শিক্ষা। শীর্ষে উঠেছে #105, যা শুধু helper-এর নাম বদল: ১২টি ফাইল আর test নেই বলে ৭৯। সত্যিকারের ঝুঁকি কম, আর git diff --color-moved দিয়ে কয়েক মিনিটে দেখা যায়। অন্যদিকে #104-এর config/app.toml হিসাবেই আসেনি (শুধু .py গোনা হয়), অথচ config-এর বদল প্রায়ই বড় ঝামেলা বাধায়। #101 শূন্য — HTML-কে code ধরা হয়নি। অর্থাৎ score মনোযোগের ইঙ্গিত, রায় নয়: “#103 টাকা-পয়সার কোড, test নেই — আজই দেখা দরকার”, এটুকুই এর কাজ। আর score দিয়ে কারও কাজের মূল্যায়ন নয় (প্র ০৩)।

৯ · Review-এর বিকল্প আর সঙ্গী — pair programming আর mob programming

PR-ভিত্তিক review “পরে দেখা”: লেখক শেষ করেন, তারপর reviewer পড়েন। আরেকটি পথ হলো কাজের সময়েই একসাথে থাকা।

Pair programming

দুজন, এক কম্পিউটার (বা screen share). একজন “driver” লেখেন, আরেকজন “navigator” পথ আর ভুল দেখেন; নিয়মিত ভূমিকা বদল। Kent Beck-এর Extreme Programming (১৯৯৯ সালের বই)-এর একটি মূল অনুশীলন। Pair programming-কে অনেকে “ধারাবাহিক review” বলেন।

Mob (ensemble) programming

পুরো দল এক screen-এ, একজন লেখেন, সবাই মিলে ভাবেন, কয়েক মিনিট পরপর লেখক বদলান। Woody Zuill আর Hunter Industries-এর দল ২০১০-এর দশকের শুরুতে এটি জনপ্রিয় করেন বলে ধরা হয়। Mob programming-এ আলাদা review প্রায় লাগে না।

PR review (async)PairMob
Feedback কখনকাজ শেষে, ঘণ্টা বা দিন পরেসাথে সাথেসাথে সাথে
অপেক্ষাqueue-তেপ্রায় নেইনেই
মানুষ-ঘণ্টাকম (reviewer-এর কিছু সময়)প্রায় দ্বিগুণ লেখার সময়দলের সবার সময়
ভিন্ন সময়-অঞ্চলভালো চলেকঠিনখুব কঠিন
লিখিত রেকর্ডPR-এ থাকেআলাদা করে লিখতে হয়আলাদা করে লিখতে হয়
সবচেয়ে ভালো যেখানেরোজকার ছোট পরিবর্তনজটিল যুক্তি, নতুন সদস্যকে শেখানোকঠিন নকশা, জরুরি সমস্যা, দলের রীতি তৈরি

২০০০-এর আশেপাশে Laurie Williams-সহ কয়েকজনের পরীক্ষা (মূলত শিক্ষার্থীদের ওপর) pair-এ কম ত্রুটি আর কিছুটা বাড়তি মোট সময়ের কথা জানিয়েছিল; পরের meta-analysis-এ ফল মিশ্র। তাই বেশিরভাগ দল মিশিয়ে চলে: রোজকার কাজে PR review, কঠিন কাজে জোড়ায় বসে লেখা। Bus factor — কতজন হঠাৎ না থাকলে project আটকে যায় — বাড়ানোর তিনটিই উপায়।

আরও গভীরে: আনুষ্ঠানিক review-এর মান হিসেবে IEEE 1028 (Standard for Software Reviews and Audits) inspection, walkthrough, technical review আর audit-এর পার্থক্য আর ধাপ বর্ণনা করে। বাস্তব শিল্পের ধারা নিয়ে Sadowski ও সহকর্মীদের “Modern Code Review: A Case Study at Google” (ICSE SEIP ২০১৮) পড়ার মতো — ছোট পরিবর্তন, দ্রুত সাড়া আর tool-এর ভূমিকা নিয়ে। Google-এর Tricorder (ICSE ২০১৫) নিয়ে লেখা দেখায়, static analysis-এর ফল review-এর ভেতরেই দেখানো আর false positive কম রাখা কেন জরুরি।

পরীক্ষা ও ইন্টারভিউতে:

  • Inspection আর modern code review-এর পার্থক্য? — Fagan inspection আনুষ্ঠানিক: ভূমিকা, প্রস্তুতি, সভা, লিখিত ত্রুটি-তালিকা। Modern review হালকা, tool-ভিত্তিক, asynchronous, প্রায় প্রতিটি পরিবর্তনে।
  • Code review-এর লক্ষ্য কী কী? — ত্রুটি ধরা, জ্ঞান ভাগাভাগি, সামঞ্জস্য, শেখানো, নিরাপত্তা; গবেষণায় জ্ঞান স্থানান্তর আর সচেতনতা বড় ফল হিসেবে এসেছে।
  • Linter আর static analysis-এর পার্থক্য? — Linter সাধারণত স্থানীয় ধাঁচ আর রীতির নিয়ম; static analysis কোড না চালিয়ে data ও control-এর পথ জুড়ে গভীর বিশ্লেষণ (যেমন taint). সীমারেখা ঝাপসা।
  • একটি খারাপ review মন্তব্যকে ভালো করবেন কীভাবে? — মানুষ নয় কোড, নির্দিষ্ট লাইন ও ইনপুট, কারণ, প্রস্তাব বা প্রশ্ন, আর blocking/nit/question লেবেল।
  • আপনার PR-এ reviewer-এর সাথে একমত না হলে কী করবেন? — কারণসহ উত্তর, বুঝতে প্রশ্ন, দরকারে ছোট call, সিদ্ধান্ত thread-এ লিখে রাখা; দলের মান বা tech lead-এর সিদ্ধান্ত মেনে নেওয়া।

১০ · AI-এর যুগে — AI reviewer, আর AI-লেখা কোডের review

এখন দুই দিক থেকে AI review-এর কাজে ঢুকেছে: অনেক platform-এ AI নিজেই PR-এ মন্তব্য দেয়, আর অনেক PR-এর কোডের বড় অংশই AI-এর লেখা। দুটিতেই মানুষের review আরও জরুরি — AI-লেখা কোড দেখতে পরিপাটি, তাই চোখ ঢিলে হয়ে যায়।

AI-এর চেনা ভুলকীভাবে ধরবেন
অসংখ্য style-মন্তব্য, আসল যুক্তির ভুল বাদStyle linter-কে দিন; মানুষ অংশ ৪-এর ক্রমে পড়ুন
আত্মবিশ্বাসী ভুল দাবি: “এখানে SQL injection নেই”কোড পড়ে আর ছোট পরীক্ষা চালিয়ে যাচাই
বানানো linter নিয়ম, tool-এর flag বা config keyTool-এর অফিসিয়াল documentation; config চালিয়ে দেখা
এমন test যা কিছুই যাচাই করে না, বা implementation-এর হুবহু প্রতিচ্ছবিকোডে ইচ্ছে করে একটি ভুল ঢুকিয়ে দেখুন test fail করে কি না (mutation testing, পর্ব ৫০)
অপ্রয়োজনীয় design pattern বা abstraction-এর প্রস্তাবYAGNI (পর্ব ২৬) — এই বদল আজকের কোন সমস্যা মেটায়?
“Clean up history”-র নামে git push --force বা reset --hard, সতর্কতা ছাড়াশুধু নিজের branch-এ --force-with-lease; আগে throwaway clone-এ চালিয়ে দেখা
অন্য উৎস থেকে প্রায় হুবহু কোড, license-এর প্রশ্নসহউৎস জিজ্ঞেস করুন; দলের license-নীতি
Prompt-এ পুরো diff, সাথে secret বা গ্রাহকের তথ্যআগে গোপন মান মুছুন; অনুমোদিত tool-ই ব্যবহার

AI-কে reviewer-এর সহকারী হিসেবে ব্যবহারের একটি prompt — এতে অংশ ৪-এর ক্রম, অবস্থার লেবেল আর “যা জানো না তা বলো না” নির্দেশ আছে:

তুমি একজন সতর্ক code reviewer. নিচে একটি PR-এর বিবরণ আর diff দিলাম (secret আর গ্রাহকের তথ্য মুছে দিয়েছি)। [PR-এর বিবরণ] [git diff main...feature এর আউটপুট] ১. এই ক্রমে দেখো: সঠিকতা ও test → নকশা → পড়ার সুবিধা → নিরাপত্তা (ইনপুট, secret, অনুমতি) → performance → error handling → নথি। ২. প্রতিটি মন্তব্য Conventional Comments ধাঁচে লেখো: label (blocking/non-blocking): কোন লাইন, কী সমস্যা, কেন গুরুত্বপূর্ণ, একটি প্রস্তাব বা প্রশ্ন। ৩. formatting বা style নিয়ে মন্তব্য করবে না — linter সেটি দেখে। ৪. কোনো দাবি নিশ্চিত না হলে "যাচাই দরকার" লিখবে; diff-এ নেই এমন কোড বা tool-এর flag বানাবে না। ৫. শেষে সারাংশ: blocking কয়টি, আর একটি ভালো দিক।

AI-এর সাহায্যে review — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Fagan-এর আনুষ্ঠানিক inspection ত্রুটি ধরায় সফল বলে জানা গিয়েছিল, তবু শিল্প কেন হালকা PR review-এর দিকে গেল? এতে কী হারাল?

১৯৭৬ সালে Michael Fagan যখন IBM-এ inspection-এর বিবরণ প্রকাশ করেন, তখন software তৈরি হতো লম্বা চক্রে: নকশা, তারপর কোড, তারপর মাসের পর মাস test. এমন ধারায় একটি ভুল test-এর পর্যায়ে ধরা পড়লে খরচ ছিল বিশাল, তাই কয়েকজনকে একটি সভায় বসিয়ে আগাম প্রস্তুতিসহ কোড পড়ানো লাভজনক ছিল। ভূমিকা স্পষ্ট ছিল — moderator, reader, লেখক, inspector — আর প্রতিটি ত্রুটি লিখিত হতো, যাতে পরে পরিসংখ্যান থেকে process উন্নত করা যায়।

পরের কয়েক দশকে তিনটি জিনিস বদলাল। প্রথমত, পরিবর্তনের আকার আর হার: আজ একটি দল দিনে অনেকবার ছোট পরিবর্তন merge করে; প্রতিটির জন্য সভা ডাকা অসম্ভব। দ্বিতীয়ত, tool: version control আর web-এর review পাতা লাইনের পাশে মন্তব্য, স্বয়ংক্রিয় check আর আলোচনার ইতিহাস এক জায়গায় দিল, ফলে সভা ছাড়াই অনেক কাজ হয়। তৃতীয়ত, দল ছড়িয়ে পড়ল নানা শহর আর সময়-অঞ্চলে — asynchronous review ছাড়া উপায় রইল না।

কী হারাল? আগাম প্রস্তুতি আর শৃঙ্খলা। Inspection-এ প্রত্যেকে নির্দিষ্ট গতিতে, চেকলিস্ট হাতে পড়তেন; PR-এ অনেকে ব্যস্ততার মধ্যে এক নজরে “LGTM” (looks good to me) লিখে দেন। ত্রুটির লিখিত পরিসংখ্যানও সাধারণত থাকে না, তাই দল জানে না কোন ধরনের ভুল বারবার হচ্ছে। আর সভায় মুখোমুখি আলোচনায় যে গভীর নকশার প্রশ্ন উঠত, লেখায় তা প্রায়ই চাপা পড়ে।

তাই অনেক দল মাঝামাঝি পথ নেয়: রোজকার পরিবর্তনে হালকা review, কিন্তু ঝুঁকিপূর্ণ অংশে (টাকা-পয়সা, নিরাপত্তা, চিকিৎসা বা যানবাহনের safety-critical কোড) আনুষ্ঠানিক review, নির্দিষ্ট চেকলিস্ট আর একাধিক reviewer. আর ত্রুটির ধরন retrospective-এ তুলে চেকলিস্ট আর linter নিয়মে ফেরত দেয় — Fagan-এর process উন্নতির ধারণা অন্য রূপে টিকে থাকে।

মূল উপলব্ধি: পদ্ধতি বদলায় পরিবেশের সাথে। হালকা review গতি আর ব্যাপকতা কিনেছে, শৃঙ্খলা আর মাপ দিয়ে। ঝুঁকি যত বেশি, আনুষ্ঠানিকতার দিকে তত সরে আসা যুক্তিসংগত।

প্র ০২“যথেষ্ট চোখ থাকলে সব bug সহজ” — অথচ ২০১৪ সালের Heartbleed একটি বহুল-ব্যবহৃত open-source library-তে বছর দুয়েক ধরা পড়েনি। Review নিয়ে এ থেকে কী শেখার আছে?

Eric Raymond তাঁর The Cathedral and the Bazaar-এ “Linus’s law” নাম দিয়ে যুক্তি দিয়েছিলেন, open source-এ অনেক মানুষ কোড দেখেন বলে bug দ্রুত ধরা পড়ে। ২০১৪ সালের এপ্রিলে প্রকাশ্যে আসা Heartbleed এই বিশ্বাসে বড় ধাক্কা দেয়। এটি ছিল OpenSSL-এর TLS “heartbeat” extension-এ একটি ত্রুটি: অনুরোধে দাবি করা দৈর্ঘ্য আর আসল data-র দৈর্ঘ্য মিলছে কি না যাচাই না করায় server নিজের memory থেকে বাড়তি অংশ — কখনো private key বা password — ফেরত পাঠাতে পারত। প্রকাশিত বিবরণ অনুযায়ী পরিবর্তনটি ২০১১-র শেষে যোগ হয়ে ২০১২-র একটি release-এ যায়, আর একজন core developer সেটি দেখেছিলেন বলেও জানা যায়। তবু প্রায় দুই বছর এটি কারও চোখে পড়েনি — অথচ পৃথিবীর বিপুল সংখ্যক server এই library ব্যবহার করত।

শিক্ষা কয়েকটি। প্রথমত, কোড দেখতে পারা আর কোড দেখা এক নয়। লক্ষ লক্ষ ব্যবহারকারী ছিলেন, কিন্তু এর জটিল C কোড নিয়মিত পড়তেন অল্প কয়েকজন, আর তখন project-টির অর্থ আর লোকবল ছিল খুবই সীমিত বলে পরে ব্যাপকভাবে আলোচিত হয়। এর পরে Linux Foundation Core Infrastructure Initiative নামে গুরুত্বপূর্ণ open-source project-এ অর্থ জোগানোর উদ্যোগ নেয়। দ্বিতীয়ত, দৈর্ঘ্য-যাচাইয়ের মতো ভুল ঠিক সেই ধরনের, যা ক্লান্ত মানুষের চোখ এড়ায় কিন্তু যন্ত্র ধরতে পারে — fuzzing আর static analysis-এর গুরুত্ব এর পরে আরও বেশি করে আলোচিত হয়। তৃতীয়ত, review-এর মান নির্ভর করে reviewer-এর প্রসঙ্গ-জ্ঞান আর সময়ের ওপর — নিরাপত্তা-সংবেদনশীল কোডে বিশেষজ্ঞ reviewer আর ধীরে পড়া দরকার।

এটি বলে না যে open source কম নিরাপদ, বা review অর্থহীন। বলে যে review একটি স্তর মাত্র — test, fuzzing, static analysis আর নিয়মিত নিরাপত্তা-নিরীক্ষার সাথে মিলিয়েই এটি কাজ করে।

মূল উপলব্ধি: চোখের সংখ্যা নয়, মনোযোগী আর যোগ্য চোখের সংখ্যা গুরুত্বপূর্ণ। আর যেখানে মানুষের মনোযোগ সবচেয়ে দুর্বল, সেখানে যন্ত্র বসানোই review-এর বুদ্ধিমান নকশা।

প্র ০৩একজন manager প্রস্তাব দিলেন: “প্রত্যেক reviewer মাসে কয়টি bug ধরলেন, সেটি দিয়ে তাঁদের মূল্যায়ন হবে।” এর সমস্যা কী? Review-এর মূল্য তাহলে কীভাবে বুঝবেন?

প্রথম সমস্যা হলো এটি review-এর একটি অংশকে পুরোটা ভাবছে। Bacchelli আর Bird-এর ২০১৩ সালের গবেষণাসহ অনেক কাজ দেখিয়েছে, review-এর বড় ফল জ্ঞান স্থানান্তর, দলের সচেতনতা আর কোডের পড়ার সুবিধা — যার কোনোটিই “bug-এর সংখ্যা”-য় ধরা পড়ে না। একজন reviewer যিনি নতুন সহকর্মীকে ধৈর্য ধরে দলের architecture বুঝিয়ে দিলেন, এই মাপে তিনি শূন্য পাবেন।

দ্বিতীয় সমস্যা Goodhart-এর সূত্র: কোনো মাপ লক্ষ্য হয়ে গেলে তা আর ভালো মাপ থাকে না। এখন reviewer-রা ছোট ছোট nit-কে “bug” বলে লিখবেন, একটি ত্রুটিকে তিনটিতে ভাঙবেন, আর ছোট পরিষ্কার PR — যেখানে খোঁজার মতো কিছু নেই — দেখতে চাইবেন না। লেখকেরাও প্রতিক্রিয়া দেখাবেন: যে reviewer বেশি “bug” লেখেন, তাঁকে এড়িয়ে চলবেন। দলের মধ্যে আস্থা ভেঙে যায়, আর review হয়ে ওঠে প্রতিযোগিতা।

তৃতীয়ত, সবচেয়ে ভালো ফল হলো এমন PR, যাতে bug-ই নেই — এই মাপ সেই সাফল্যকে শাস্তি দেয়।

তাহলে কী দেখবেন? ব্যক্তির নয়, ব্যবস্থার মাপ, আর একাধিক: PR খোলা থেকে প্রথম সাড়া আর merge পর্যন্ত সময় (অংশ ৮); PR-এর আকারের বণ্টন; release-এর পরে পাওয়া ত্রুটি কোন ধরনের, আর সেগুলো review-এ ধরা সম্ভব ছিল কি না; কতজন মানুষ codebase-এর কোন অংশ চেনেন (bus factor). পাশাপাশি গুণগত প্রশ্ন — retrospective-এ দলকে জিজ্ঞেস করা, review থেকে তাঁরা কী শিখছেন, কোথায় বিরক্ত হচ্ছেন। DORA-র গবেষণাও (পর্ব ৫৯) দলের delivery-কে ব্যক্তির নয়, ব্যবস্থার মাপ দিয়ে দেখে।

মূল উপলব্ধি: Review-এর মূল্য মূলত দলের মধ্যে তৈরি হয়, ব্যক্তির খাতায় নয়। মাপুন প্রবাহ আর ফল, আর মাপকে আলোচনার সূচনা হিসেবে ব্যবহার করুন — পুরস্কার বা শাস্তির হাতিয়ার হিসেবে নয়।

প্র ০৪একজন নতুন junior ডেভেলপারকে বলা হলো দলের সবচেয়ে অভিজ্ঞ ডেভেলপারের PR review করতে। তিনি দ্বিধায় — “আমি কী বলব?” দলের নেতা আর junior নিজে কী করতে পারেন?

বাংলাদেশের অনেক কর্মস্থলে বয়স আর পদের প্রতি সম্মান গভীর — “ভাইয়া” বা “আপু”-র কোডে প্রশ্ন তোলা অনেকের কাছে বেয়াদবির মতো লাগে। আবার এটি শুধু আমাদের সমস্যা নয়; সারা পৃথিবীর দলেই নতুনরা ভাবেন, তাঁদের প্রশ্ন বোকামি শোনাবে। ফল হয় নীরব “LGTM”, আর অভিজ্ঞ মানুষের ভুল — যা সবারই হয় — বিনা বাধায় ঢুকে পড়ে।

অথচ junior-এর review অত্যন্ত মূল্যবান, ঠিক তাঁর নতুনত্বের কারণে। তিনি যা বুঝতে পারছেন না, ছয় মাস পরে আরেকজন নতুন সদস্যও তা বুঝবেন না — তাই “এই অংশটি কী করে, বুঝতে পারিনি” একটি আসল পড়ার-সুবিধার সমস্যা। আর review-ই তাঁর codebase শেখার সবচেয়ে দ্রুত পথ।

Junior যা করতে পারেন: প্রশ্নের আকারে লেখা — “None এলে কী হয়, আমি ঠিক বুঝিনি — একটু বুঝিয়ে দেবেন?” এটি অভিযোগ নয়, তবু ভুল থাকলে লেখক নিজেই খুঁজে পাবেন। অংশ ৪-এর চেকলিস্ট ধরে পড়া, যাতে মনে হয় একটি নিয়ম মানছেন, ব্যক্তিগত মত নয়। question আর thought লেবেল ব্যবহার, যা কোনো দাবি করে না। আর যা ভালো লাগল, সেটি লেখা — প্রশংসা দিয়ে কথোপকথন শুরু সহজ।

দলের নেতা যা করতে পারেন: প্রকাশ্যে বলা — “আমার PR-এ প্রশ্ন করবেন, সেটাই আপনার কাজ” — আর junior-এর একটি প্রশ্নে কোড বদলালে সেটিও প্রকাশ্যে স্বীকার করা। Google-এর Project Aristotle-এর মতো প্রকাশিত দল-গবেষণায় psychological safety — ভুল স্বীকার বা প্রশ্ন করতে ভয় না পাওয়া — কার্যকর দলের অন্যতম প্রধান বৈশিষ্ট্য হিসেবে এসেছিল বলে জানানো হয়েছে। আর junior-কে প্রথম দিকে দ্বিতীয় reviewer করা, যাতে শেখা হয় কিন্তু একা দায় না থাকে।

মূল উপলব্ধি: Review-এর মান শুধু চেকলিস্টে নয়, দলের সংস্কৃতিতে। প্রশ্ন করা নিরাপদ না হলে সবচেয়ে মূল্যবান প্রশ্নগুলো কখনো লেখা হয় না।

প্র ০৫একটি দলের অর্ধেক মানুষ ঢাকায়, অর্ধেক অন্য মহাদেশে, সময়ের ব্যবধান ১০ ঘণ্টার মতো। PR প্রতিবার এক দিন অপেক্ষা করে। Pair programming, PR review আর অন্য কৌশল মিলিয়ে কী করবেন?

এখানে মূল শত্রু হলো ঘুরে আসার সময়: ঢাকায় সন্ধ্যায় PR খোলা হলো, অন্য পাশে তখন রাত; পরের দিন তাঁরা মন্তব্য করলেন, তখন ঢাকায় রাত। প্রতিটি round-এ পুরো এক দিন। অংশ ৮-এর মডেলে দেখেছেন round-এর সংখ্যা আর অপেক্ষা গুণ হয় — দুই round মানে দুই দিন।

প্রথম কৌশল: round কমানো। PR ছোট আর পরিষ্কার হলে প্রথম review-এই approve-এর সম্ভাবনা বাড়ে। বিবরণে আগেই সম্ভাব্য প্রশ্নের উত্তর (“কেন এই পদ্ধতি, কী বিকল্প বাদ দিলাম”)। মন্তব্যে স্পষ্ট লেবেল: nit-গুলো non-blocking হলে লেখক সেগুলো মেটিয়ে নিজেই merge করতে পারেন — অনেক দল “approve with nits” নিয়ম রাখে, অর্থাৎ ছোট বিষয় মিটিয়ে আবার review ছাড়াই merge.

দ্বিতীয় কৌশল: একই সময়-অঞ্চলে reviewer. বেশিরভাগ রোজকার PR-এর জন্য নিজের শহরের সহকর্মী যথেষ্ট; অন্য পাশের দলকে ডাকা হোক শুধু তাঁদের অংশ ছুঁলে (CODEOWNERS দিয়ে)। এতে জ্ঞান ভাগাভাগির একটি অংশ কমে — তা মেটাতে নিয়মিত design আলোচনা আর লিখিত ADR (পর্ব ৩৮)।

তৃতীয় কৌশল: যে ঘণ্টাগুলো মেলে, তা কাজে লাগানো। ঢাকার সকাল আর অন্য পাশের সন্ধ্যায় হয়তো দুই ঘণ্টা মেলে। সেই সময়ে কঠিন বা বিতর্কিত পরিবর্তনে pair programming — তখন কোড লেখার সময়েই review হয়ে যায়, পরের দিনের অপেক্ষা থাকে না। জটিল নকশার সিদ্ধান্তে ছোট mob session.

চতুর্থ কৌশল: যন্ত্রকে বেশি দায়িত্ব। শক্তিশালী CI, linter, type checker, ভালো test coverage থাকলে reviewer নিশ্চিন্তে শুধু নকশা আর যুক্তি দেখেন; আর feature flag (পর্ব ১৭) দিয়ে অসম্পূর্ণ কাজ merge করে রাখা যায়, যাতে PR লম্বা হয়ে conflict না জমে।

কোনোটিই বিনা খরচে নয়: pair-এ দুজনের সময় যায়, আর একই অঞ্চলের reviewer-এ দল দুই ভাগ হওয়ার ঝুঁকি (Conway’s law). Retrospective-এ মাপুন, কোনটি আসলে অপেক্ষা কমাচ্ছে।

মূল উপলব্ধি: Review একটি নির্দিষ্ট আচার নয়, একটি লক্ষ্য — দ্বিতীয় চোখ আর ভাগ করা জ্ঞান। পরিস্থিতি অনুযায়ী লক্ষ্যটি অন্য পথেও অর্জন করা যায়: পরে পড়ে, একসাথে লিখে, বা যন্ত্র দিয়ে।

অনুশীলন

প্রথমে নিজে উত্তর লিখুন, তারপর মিলিয়ে দেখুন। Git-এর কাজ একটি throwaway scratch ফোল্ডারে করুন।

  1. এই diff-টি review করুন (নকশা ও সমালোচনা): একটি কাল্পনিক বাজার অ্যাপের দিনের মোট বিক্রির হিসাব। PR-এর শিরোনাম “Add date filter to daily total”. অংশ ৪-এর ক্রমে review করে প্রতিটি মন্তব্য লেবেলসহ লিখুন, শেষে সিদ্ধান্ত দিন।
    $ git diff -- report.js
    diff --git a/report.js b/report.js
    index 1fd0e81..18b86ff 100644
    --- a/report.js
    +++ b/report.js
    @@ -1,6 +1,10 @@
    -function dailyTotal(orders) {
    +const PAY_KEY = "demo-pay-key-7f3a9c";
    +
    +function dailyTotal(orders, d) {
       let sum = 0;
    -  for (const o of orders) sum += o.amount;
    +  for (var i = 0; i <= orders.length; i++) {
    +    if (orders[i].date == d) sum += orders[i].amount;
    +  }
       return sum;
     }
    -module.exports = { dailyTotal };
    +module.exports = { dailyTotal, PAY_KEY };

    একটি আদর্শ উত্তর (ক্রম অনুযায়ী):

    • সঠিকতা — issue (blocking): loop-এর শর্ত i <= orders.length; শেষ ধাপে orders[orders.length] হলো undefined, তাই .date পড়তে গিয়ে TypeError — প্রতিটি ডাকেই function ভাঙবে। < হবে, অথবা আগের মতো for (const o of orders).
    • সঠিকতা — question: == type বদলে তুলনা করে। date string আর d কখনো Date object হলে ফল অপ্রত্যাশিত। === আর একটি নির্দিষ্ট ধাঁচ (যেমন "2026-01-15") প্রস্তাব।
    • Test — issue (blocking): কোনো test নেই; একটি test থাকলেই ওপরের TypeError ধরা পড়ত। খালি list, মিলে-যাওয়া আর না-মেলা তারিখ — অন্তত তিনটি case.
    • নকশা — suggestion (non-blocking): হিসাবের ফাইল থেকে একটি key export হচ্ছে — রিপোর্টের module-এর এতে কোনো কাজ নেই; দায়িত্ব মিশে গেছে।
    • পড়ার সুবিধা — nitpick (non-blocking): parameter-এর নাম d → date; আর var-এর বদলে let (বা for…of). ESLint-এর মতো linter এগুলো ধরত।
    • নিরাপত্তা — issue (blocking): PAY_KEY কোডে লেখা আর export করা — secret repository-তে ঢুকছে। Environment variable বা secret manager থেকে পড়া উচিত, আর এই PR-এর branch push হয়ে থাকলে key-টি বদলে (rotate) ফেলতে হবে, কারণ history-তে থেকে যায় (পর্ব ৫৭)।
    • প্রশংসা — praise: তারিখ অনুযায়ী ফিল্টারের প্রয়োজনটি স্পষ্ট, আর function-এর মূল কাঠামো ছোট রাখা হয়েছে।

    সিদ্ধান্ত: Request changes — ৩টি blocking (off-by-one, test নেই, secret). সাথে সারাংশ: “key-টি এখনই rotate করা দরকার কি না, জানাবেন; বাকিগুলো ছোট।” লক্ষ করুন, var, == আর হয়তো hard-coded key-ও যন্ত্র ধরতে পারত — দলের linter আর secret scanning চালু থাকলে reviewer-এর সময় যেত শুধু off-by-one আর নকশায়।

  2. আউটপুট অনুমান করুন: অংশ ৩-এর mini_lint.py-এর SOURCE-এর জায়গায় নিচের কোড বসালে linter কী ছাপবে? প্রতিটি লাইনের কারণ বলুন, আর কোন সতর্কবার্তাটি আসল tool সম্ভবত দিত না?
    def report(rows, seen={}):
        count = 0
        for r in rows:
            try:
                count += int(r)
            except ValueError:
                print("bad row", r)
        result = count
        _ = rows.sort()
        return count

    আসলে চালালে আউটপুট:

    line 1: W1 mutable default argument
    line 7: W3 print left in
    line 8: W4 unused variable 'result'
    line 9: W4 unused variable '_'

    লাইন ১: seen={} একটি dict default — W1. লাইন ৬-এ W2 নেই, কারণ except ValueError নির্দিষ্ট exception ধরে; নিয়মটি শুধু ধরনহীন except:-এ। লাইন ৭: print — W3. লাইন ৮: result-এ মান বসেছে, কিন্তু কোথাও পড়া হয়নি — W4. লাইন ৯: _-ও W4 পেল, কারণ খেলনা linter নামটিকে আলাদা করে না; Python-এ _ প্রচলিতভাবে “ইচ্ছে করে ফেলে দেওয়া মান”, আর Ruff-এর মতো tool default নিয়মে সাধারণত একে অব্যবহৃত বলে না — এটি false positive. আরও দুটি জিনিস খেয়াল করুন: seen parameter-টি কোথাও ব্যবহার হয়নি, তবু খেলনাটি কিছু বলেনি (parameter দেখে না); আর rows.sort() নিজেই None ফেরত দেয় — ডাকার মানুষের list উল্টে-পাল্টে যাচ্ছে, এই side effect কোনো linter নয়, reviewer-এর ধরার জিনিস।

  3. এই মন্তব্যগুলো নতুন করে লিখুন: প্রতিটিকে লেবেল, অবস্থা, নির্দিষ্ট পর্যবেক্ষণ, কারণ আর প্রস্তাব বা প্রশ্নসহ লিখুন। (ক) “Bad naming everywhere.” (খ) “Why no tests? This will break.” (গ) “Use a dictionary here, obviously.” (ঘ) “LGTM” — একটি ৬০০ লাইনের payment PR-এ, দুই মিনিটে।
    • (ক) nitpick (non-blocking): process() আর data2 নাম থেকে বোঝা যাচ্ছে না এরা কী করে। calculate_monthly_fee() আর unpaid_invoices হলে লাইন ৪০–৬০ পড়া সহজ হবে, কারণ পরের পাঠককে ভেতরে ঢুকে দেখতে হবে না। — “everywhere”-এর বদলে নির্দিষ্ট উদাহরণ।
    • (খ) issue (blocking): fee-এর হিসাবে নতুন শর্ত (মাস শেষে ছাড়) এসেছে, কিন্তু test ফাইল বদলায়নি। শূন্য বিল আর মাসের শেষ দিন — এই দুই সীমায় ভুল হলে গ্রাহকের বিল ভুল হবে। এই দুটির test যোগ করবেন?
    • (গ) suggestion (non-blocking): এই if/elif শৃঙ্খলে ৯টি জেলার নাম থেকে charge বের হচ্ছে। একটি dict (CHARGE_BY_DISTRICT) হলে নতুন জেলা যোগ করতে শুধু একটি লাইন লাগবে আর ভুলের সুযোগ কমবে। আপনার কী মনে হয়? — “obviously” বাদ, কারণ যোগ, আর সিদ্ধান্ত লেখকের হাতে।
    • (ঘ) এখানে সমস্যা মন্তব্যের ভাষা নয়, review-এর গভীরতা। দুই মিনিটে ৬০০ লাইনের payment কোড পড়া যায় না। সৎ বিকল্প: “thought: আমি শুধু refund.py অংশটি মন দিয়ে দেখেছি — ঠিক আছে। বাকি অংশ আজ বিকেলে দেখব; এর মধ্যে PR-টি দুই ভাগ করা যায় কি?” আর approve কেবল পুরোটা দেখার পরে। “Rubber-stamp” approval দলকে মিথ্যা নিরাপত্তা দেয়।
  4. হিসাব: (ক) অংশ ৮-এর diff_risk.py-এর নিয়মে একটি PR-এর score হাতে হিসাব করুন: payment/fee.py-এ ৪০ লাইন যোগ, ১০ লাইন বাদ; migrations/m0007_index.py-এ ৫০ লাইন যোগ; কোনো test ফাইল নেই। তারপর tests/test_fee.py-এ ৩০ লাইন যোগ করলে score কত হবে? (খ) একটি দলে গড়ে দিনে ৮টি PR খোলে, আর প্রতিটি PR খোলা থেকে merge পর্যন্ত গড়ে ১.৫ কর্মদিবস থাকে। Little’s law অনুযায়ী যেকোনো সময় গড়ে কয়টি PR খোলা থাকে? দল যদি গড় সময় ০.৫ দিনে নামায়, কী বদলায়?

    (ক) code ফাইল ২টি (দুটিই .py, কোনোটি tests/-এ নয়), মোট লাইন = ৪০ + ১০ + ৫০ = ১০০.

    • আকার: 10 × log2(1 + 100/50) = 10 × log23 ≈ 10 × ১.৫৮৫ = ১৫.৮৫
    • ফাইল: ৩ × ২ = ৬
    • সংবেদনশীল (payment/, migrations/): একবারই +১৫, দুটি path থাকলেও
    • test নেই: +২০

    মোট ≈ ১৫.৮৫ + ৬ + ১৫ + ২০ = ৫৬.৮৫ → ৫৭. Test ফাইল যোগ করলে: test-এর লাইন আকারে গোনা হয় না, তাই শুধু +২০ চলে যায় → ৩৬.৮৫ → ৩৭. (প্রোগ্রাম চালিয়ে মিলিয়েছি: 57 আর 37.) এই নিয়মে test-এর মান দেখা হয় না — একটি ফাঁকা test ফাইলও ২০ কমাত; score লক্ষ্য হলে মানুষ ঠিক এটাই শেখেন।

    (খ) Little’s law: L = λ × W = ৮ PR/দিন × ১.৫ দিন = ১২টি PR গড়ে একসাথে খোলা। W = ০.৫ দিন হলে L = ৮ × ০.৫ = ৪টি. একই হারে কাজ আসছে, কিন্তু অসমাপ্ত কাজের স্তূপ তিন ভাগের এক ভাগ — কম conflict, কম context-বদল।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি পরিবর্তনের review-এর পথ সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔍 লেখক নিজের diff পড়েন আর test চালান
  2. 🪝 Commit-এর সময় pre-commit hook formatter আর linter চালায়
  3. 🤖 Push-এর পরে CI-তে linter, type checker, test আর SAST
  4. 👀 Reviewer যুক্তি, নকশা আর নিরাপত্তা দেখে মন্তব্য করেন
  5. 🔧 লেখক fixup commit দিয়ে সাড়া দেন, reviewer approve করেন
  6. ✅ সব check সবুজ, merge

Michael Fagan-এর ১৯৭৬ সালের inspection আর আজকের PR-ভিত্তিক “modern code review”-এর পার্থক্য হিসেবে কোনটি সবচেয়ে সঠিক?

  • Inspection asynchronous, PR review-এ সবাইকে একসাথে সভায় বসতে হয়
  • PR review-এ শুধু যন্ত্র কোড দেখে, মানুষ নয়
  • Inspection আনুষ্ঠানিক — নির্দিষ্ট ভূমিকা, প্রস্তুতি, সভা, লিখিত ত্রুটি-তালিকা; modern review হালকা, tool-ভিত্তিক, asynchronous
  • Inspection শুধু নকশার নথির জন্য, কোডের জন্য কখনো নয়

একটি review মন্তব্য: nitpick (non-blocking): rename tmp to pendingOrders. Conventional Comments-এর ধাঁচে এর অর্থ কী?

  • এটি না মানলে merge হবে না
  • ছোট, ঐচ্ছিক প্রস্তাব; না মানলেও merge আটকানো উচিত নয়
  • Reviewer শুধু জানতে চাইছেন tmp কী
  • Reviewer নামটির প্রশংসা করছেন

Python-এ def add(x, bag=[]): bag.append(x); return bag লেখা। তারপর add(1) আর add(2) ডাকা হলো। দ্বিতীয় ডাকটি কী ফেরত দেয়, আর linter এখানে কেন সতর্ক করে?

  • [2] — প্রতিটি ডাকে নতুন খালি list
  • Error, কারণ default মান বদলানো যায় না
  • None, কারণ append কিছু ফেরত দেয় না
  • [1, 2] — default list একবারই তৈরি হয়ে সব ডাকে ভাগ হয়

একটি দলের review-এর মন্তব্যের অর্ধেকের বেশি indentation, লাইনের দৈর্ঘ্য আর quote চিহ্ন নিয়ে। সবচেয়ে কার্যকর পরিবর্তন কোনটি?

  • একটি formatter config ঠিক করে pre-commit hook আর CI-তে চালানো
  • প্রতিটি PR-এ আরও একজন reviewer যোগ করা
  • Reviewer-দের style আরও মন দিয়ে দেখতে বলা
  • Review বাদ দিয়ে সরাসরি merge

Review-এ দেখা গেল: sql = f"SELECT * FROM coupons WHERE code = '{code}'", যেখানে code ব্যবহারকারীর form থেকে আসে। সবচেয়ে উপযুক্ত মন্তব্য কোনটি?

  • nitpick: variable-এর নাম sql না করে query করুন
  • issue (blocking): ইনপুট সরাসরি SQL-এ জোড়া হচ্ছে — SQL injection-এর ঝুঁকি; parameterized query বা repository ব্যবহার করবেন?
  • suggestion (non-blocking): ইনপুট থেকে উদ্ধৃতি চিহ্ন হাতে সরিয়ে দিন
  • question: SELECT * কি ধীর হবে?

একটি দলে একজন reviewer প্রায় ১০০% ব্যস্ত, আর PR-এর প্রথম সাড়া পেতে গড়ে দুই দিন লাগে। কোনো বাড়তি মানুষ নেওয়া যাবে না। কোন পদক্ষেপ অপেক্ষা সবচেয়ে বেশি কমানোর সম্ভাবনা রাখে?

  • কয়েকটি PR মিলিয়ে বড় PR বানিয়ে কম সংখ্যক review
  • Reviewer-কে না পড়েই দ্রুত approve করতে বলা
  • Review-এর দায়িত্ব দলের সবার মধ্যে ভাগ করা, PR ছোট রাখা, আর style-এর কাজ linter-কে দেওয়া
  • শুধু শুক্রবার একসাথে সব PR review করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Review-এর লক্ষ্য পাঁচটি: ত্রুটি, জ্ঞান ভাগাভাগি, সামঞ্জস্য, শেখানো, নিরাপত্তা। Fagan inspection (১৯৭৬) আনুষ্ঠানিক; modern review হালকা আর asynchronous, আর গবেষণায় জ্ঞান স্থানান্তর তার বড় ফল।
  2. যন্ত্র আগে: formatter (চেহারা), linter (সন্দেহজনক ধাঁচ), static analysis/SAST (data-র পথ), type checker — hook-এ সুবিধার জন্য, CI-তে নিশ্চয়তার জন্য। False positive কম রাখুন।
  3. ক্রম: প্রসঙ্গ → সঠিকতা ও test → নকশা → পড়ার সুবিধা → নিরাপত্তা (ইনপুট, secret, অনুমতি) → performance → error handling → নথি।
  4. মন্তব্য: কোড নিয়ে, নির্দিষ্ট, কারণসহ, প্রস্তাব বা প্রশ্নসহ, আর লেবেলে অবস্থা স্পষ্ট (blocking / nit / question); প্রশংসাও। লেখক: self-review, ছোট PR, প্রসঙ্গ, test, প্রতিটি thread-এর উত্তর, fixup + autosquash.
  5. Review একটি queue: বড় PR আর ব্যস্ত reviewer অপেক্ষা অ-রৈখিকভাবে বাড়ায়; দ্রুত প্রথম সাড়া, ভাগ করা চাপ, ঝুঁকি অনুযায়ী মনোযোগ — আর দরকারে pair বা mob programming.

পরের পর্বে: এই মডিউলে কোড সাজানো, পরিষ্কার রাখা আর পড়ে যাচাই করা দেখলেন। কিন্তু কোড যা বলার কথা তা-ই করছে — এটা প্রমাণ করবেন কীভাবে? পর্ব ৪৫-এ Testing মডিউলের শুরু: verification আর validation, test-এর স্তর, আর black-box বনাম white-box testing.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান