ক্যাপস্টোন — একটি সম্পূর্ণ প্রজেক্ট Git ওয়ার্কফ্লো সহ সিমুলেট করা
এই পাঠে যা শিখবেন
- এই কোর্সের বিভিন্ন মডিউলের টুকরোগুলো বাস্তবে একসাথে কীভাবে কাজ করে, বিচ্ছিন্নভাবে নয়
- কেন একটি বাস্তব ফিচার-ডেভেলপমেন্ট চক্রে প্রায়ই একটি সত্যিকারের ৩-ওয়ে মার্জ প্রয়োজন হয়, fast-forward নয়
- SOLID/DIP ডিজাইন কীভাবে সরাসরি ইউনিট টেস্ট লেখা সহজ করে তোলে
- একটি সম্পূর্ণ, সাত-ধাপের প্রজেক্ট লগ যা প্রতিটি ধাপের ফলাফল সত্যিকারের কোড-চালিত গণনা থেকে দেখায়
১ · এই ক্যাপস্টোন কী করে
এই পাঠে নতুন কোনো তত্ত্ব নেই — বরং এই কোর্সের ৫৭টি পাঠে শেখা প্রতিটি বড় টুকরো একটি একক, বাস্তব দৃশ্যপটে একসাথে বসানো হয়েছে: একটি ছোট টিম একটি "Task Manager" অ্যাপ্লিকেশনে একটি ট্যাগিং ফিচার যোগ করছে। নিচের ফ্লোচার্টে সাতটি ধাপ দেখানো হয়েছে — প্রতিটি ধাপ কোন মডিউলের উপকরণ পুনর্ব্যবহার করছে তা কোডেই বন্ধনীতে উল্লেখ করা আছে।
git বাইনারি নেই। নিচের
কোড সেলের Git ওয়ার্কফ্লো অংশ (ধাপ ৩ ও ৭) M7-M9-এ প্রতিষ্ঠিত Repo/branches/
commits/parents মডেলের একটি from-scratch সিমুলেশন — real git-এর প্রকৃত
অ্যালগরিদম (fast-forward চেক, merge-base খোঁজা, দুই-প্যারেন্ট মার্জ কমিট তৈরি) যথাযথভাবে অনুসরণ করে, কিন্তু
সম্পূর্ণ Python-এ বাস্তবায়িত, কারণ এই স্যান্ডবক্সে real git বাইনারি চালানো সম্ভব নয়।
২ · সম্পূর্ণ সাত-ধাপ সিমুলেশন
নিচের একটি একক কোড সেলে পুরো দৃশ্যপট চলে — একটি ইউজার স্টোরি থেকে শুরু করে একটি চূড়ান্ত, মার্জড অবস্থা পর্যন্ত। প্রতিটি সংখ্যা (টেস্ট ফলাফল, মার্জ কমিটের parents, PR-এর অ্যাপ্রুভাল-গেট) কোডেই সত্যিকারের গণনা থেকে আসে।
# ============================================================
# ক্যাপস্টোন -- একটি "Task Manager"-এ ট্যাগিং ফিচার যোগ করার
# সম্পূর্ণ, শেষ-থেকে-শুরু প্রজেক্ট সিমুলেশন (M3, M4, M7-M9, M10, M12)
# ============================================================
print("=" * 70)
print("ধাপ ১ -- ইউজার স্টোরি ও অ্যাকসেপ্টেন্স ক্রাইটেরিয়া (M3/L13)")
print("=" * 70)
class UserStory:
def __init__(self, role, goal, reason, acceptance_criteria):
self.role = role
self.goal = goal
self.reason = reason
self.acceptance_criteria = acceptance_criteria
def format_story(self):
return f"একজন {self.role} হিসেবে, আমি চাই {self.goal}, যাতে {self.reason}।"
story = UserStory(
role="প্রজেক্ট ম্যানেজার",
goal="প্রতিটি টাস্ককে 'urgent' ট্যাগ দিয়ে চিহ্নিত করতে পারি",
reason="জরুরি কাজগুলো দ্রুত আলাদা করে চেনা যায়",
acceptance_criteria=[
"AC1: add_tag(task, 'urgent') কল করলে টাস্কে ট্যাগ যুক্ত হয়",
"AC2: get_tasks_by_tag('urgent') শুধু 'urgent' ট্যাগযুক্ত টাস্কগুলো রিটার্ন করে",
"AC3: একই ট্যাগ দুইবার যোগ করলে ডুপ্লিকেট তৈরি হয় না",
],
)
print(story.format_story())
for ac in story.acceptance_criteria:
print(f" - {ac}")
print()
print("=" * 70)
print("ধাপ ২ -- SOLID-কমপ্লায়েন্ট ক্লাস ডিজাইন, DIP-সহ (M4/L16-L18)")
print("=" * 70)
class TaskRepository: # abstract ইন্টারফেস -- DIP-এর abstraction
def save(self, task):
raise NotImplementedError
def all(self):
raise NotImplementedError
class InMemoryTaskRepository(TaskRepository): # concrete ইমপ্লিমেন্টেশন
def __init__(self):
self._tasks = []
def save(self, task):
self._tasks.append(task)
def all(self):
return list(self._tasks)
class TaskService: # high-level মডিউল -- শুধু abstraction-এর উপর নির্ভরশীল
def __init__(self, repository: TaskRepository): # constructor-এর মাধ্যমে dependency injection
self.repository = repository
def add_task(self, title, tags=None):
task = {"title": title, "tags": set(tags or [])}
self.repository.save(task)
return task
def add_tag(self, task, tag):
task["tags"].add(tag) # set ব্যবহারে AC3 (ডুপ্লিকেট প্রতিরোধ) স্বয়ংক্রিয়ভাবে পূরণ হয়
def get_tasks_by_tag(self, tag):
return [t for t in self.repository.all() if tag in t["tags"]]
print("TaskService(repository) -- কংক্রিট InMemoryTaskRepository constructor দিয়ে injected হয়েছে।")
print("(SRP: storage ও business logic আলাদা ক্লাসে। DIP: TaskService কোনো কংক্রিট ক্লাসকে সরাসরি জানে না।)")
print()
print("=" * 70)
print("ধাপ ৩ -- Git ওয়ার্কফ্লো: ফিচার ব্রাঞ্চ, কমিট, ও main-এর সমান্তরাল অগ্রগতি (M7-M9)")
print("=" * 70)
def make_repo():
return {"commits": {}, "branches": {"main": None}, "head": "main"}
def current_commit(repo):
ref = repo["head"]
return repo["branches"][ref] if ref in repo["branches"] else ref
def git_commit(repo, message, snapshot):
parent = current_commit(repo)
parents = [parent] if parent else []
chash = f"c{len(repo['commits']) + 1}"
repo["commits"][chash] = {"message": message, "snapshot": dict(snapshot), "parents": parents}
repo["branches"][repo["head"]] = chash
return chash
def git_branch(repo, name):
repo["branches"][name] = current_commit(repo)
def git_switch(repo, name):
repo["head"] = name
def is_ancestor(repo, maybe_ancestor, start_hash):
stack = [start_hash]
while stack:
cur = stack.pop()
if cur == maybe_ancestor:
return True
if cur is None:
continue
stack.extend(repo["commits"][cur]["parents"])
return False
def find_merge_base(repo, hash_a, hash_b):
ancestors_a = set()
stack = [hash_a]
while stack:
cur = stack.pop()
if cur is None or cur in ancestors_a:
continue
ancestors_a.add(cur)
stack.extend(repo["commits"][cur]["parents"])
queue = [hash_b]
seen = set()
while queue:
cur = queue.pop(0)
if cur in ancestors_a:
return cur
if cur is None or cur in seen:
continue
seen.add(cur)
queue.extend(repo["commits"][cur]["parents"])
return None
def git_merge(repo, source_branch, message):
target_branch = repo["head"]
target_hash = repo["branches"][target_branch]
source_hash = repo["branches"][source_branch]
if is_ancestor(repo, target_hash, source_hash):
repo["branches"][target_branch] = source_hash
return source_hash, "fast-forward"
base = find_merge_base(repo, target_hash, source_hash)
merged_snapshot = {**repo["commits"][target_hash]["snapshot"], **repo["commits"][source_hash]["snapshot"]}
chash = f"c{len(repo['commits']) + 1}"
repo["commits"][chash] = {"message": message, "snapshot": merged_snapshot, "parents": [target_hash, source_hash]}
repo["branches"][target_branch] = chash
return chash, f"three-way merge (merge base={base}, 2 parents)"
repo = make_repo()
c1 = git_commit(repo, "প্রাথমিক প্রজেক্ট স্ক্যাফোল্ড", {"app.py": "# base app"})
print(f" {c1} -- main-এ প্রাথমিক কমিট")
git_branch(repo, "feature/task-tags")
git_switch(repo, "feature/task-tags")
c2 = git_commit(repo, "TaskRepository ও TaskService ক্লাস যোগ (DIP-সহ)", {"task_service.py": "class TaskRepository: ..."})
c3 = git_commit(repo, "add_tag() ও get_tasks_by_tag() মেথড ইমপ্লিমেন্ট", {"task_service.py": "... add_tag ..."})
print(f" {c2}, {c3} -- feature/task-tags-এ দুটি কমিট (main থেকে ব্রাঞ্চ করে)")
# রিভিউ প্রক্রিয়া চলাকালীন, অন্য একজন ডেভেলপারের কাজ ইতিমধ্যে main-এ মার্জ হয়ে গেছে
git_switch(repo, "main")
c4 = git_commit(repo, "README আপডেট (রিভিউ চলাকালীন অন্য ডেভেলপারের কাজ)", {"README.md": "..."})
print(f" {c4} -- main স্বাধীনভাবে এগিয়ে গেছে রিভিউ চলাকালীন")
print(f" ফলাফল: main({c4}) ও feature/task-tags({c3}) উভয়েই {c1} থেকে ডাইভার্জড -- সত্যিকারের ৩-ওয়ে মার্জ প্রয়োজন হবে।")
print()
print("=" * 70)
print("ধাপ ৪ -- TDD-স্টাইল ইউনিট টেস্ট স্যুট (M10/L45-L46)")
print("=" * 70)
def test_add_tag_adds_tag():
repository = InMemoryTaskRepository()
service = TaskService(repository)
task = service.add_task("রিপোর্ট লেখা")
service.add_tag(task, "urgent")
assert "urgent" in task["tags"], "AC1 ব্যর্থ: ট্যাগ যোগ হয়নি"
def test_get_tasks_by_tag_filters_correctly():
repository = InMemoryTaskRepository()
service = TaskService(repository)
t1 = service.add_task("A", tags=["urgent"])
service.add_task("B", tags=[])
result = service.get_tasks_by_tag("urgent")
assert result == [t1], "AC2 ব্যর্থ: শুধু urgent ট্যাগযুক্ত টাস্ক আসা উচিত ছিল"
def test_duplicate_tag_not_added_twice():
repository = InMemoryTaskRepository()
service = TaskService(repository)
task = service.add_task("C")
service.add_tag(task, "urgent")
service.add_tag(task, "urgent")
assert len(task["tags"]) == 1, "AC3 ব্যর্থ: ডুপ্লিকেট ট্যাগ তৈরি হয়েছে"
TEST_SUITE = [
("test_add_tag_adds_tag (AC1)", test_add_tag_adds_tag),
("test_get_tasks_by_tag_filters_correctly (AC2)", test_get_tasks_by_tag_filters_correctly),
("test_duplicate_tag_not_added_twice (AC3)", test_duplicate_tag_not_added_twice),
]
def run_tests(suite):
results = []
for name, fn in suite:
try:
fn()
results.append((name, "PASS"))
except AssertionError as e:
results.append((name, f"FAIL: {e}"))
return results
test_results = run_tests(TEST_SUITE)
for name, status in test_results:
print(f" {status:6} -- {name}")
all_passed = all(status == "PASS" for _, status in test_results)
print(f" সব টেস্ট পাস: {all_passed}")
print()
print("=" * 70)
print("ধাপ ৫ -- পুল রিকোয়েস্ট ও রিভিউ (M9/L40)")
print("=" * 70)
class PullRequest:
def __init__(self, source_branch, target_branch):
self.source_branch = source_branch
self.target_branch = target_branch
self.status = "open"
self.reviews = {} # reviewer -> সর্বশেষ রিভিউ (নতুন রিভিউ পুরনোটি সুপারসিড করে)
def add_review(self, reviewer, decision, comment):
self.reviews[reviewer] = {"decision": decision, "comment": comment}
def can_merge(self):
decisions = [r["decision"] for r in self.reviews.values()]
return "approved" in decisions and "changes_requested" not in decisions
pr = PullRequest("feature/task-tags", "main")
pr.add_review("রহিম", "changes_requested", "duplicate-tag কেসের জন্য একটি টেস্ট মিসিং ছিল")
print(f" রিভিউ ১ -- রহিম: changes_requested | can_merge() = {pr.can_merge()}")
# লেখক ফিডব্যাক অনুযায়ী duplicate-tag টেস্ট যোগ করেছেন (ধাপ ৪-এই আছে) -- পুনরায় রিভিউ চাওয়া হলো
pr.add_review("রহিম", "approved", "এখন সব ঠিক আছে, মার্জ করা যায়")
print(f" রিভিউ ২ -- রহিম: approved | can_merge() = {pr.can_merge()}")
print()
print("=" * 70)
print("ধাপ ৬ -- CI পাইপলাইন রান (M12/L53)")
print("=" * 70)
class CIPipeline:
def run_build(self, commit_label, results):
passed = all(status == "PASS" for _, status in results)
status = "PASSED" if passed else "FAILED"
ok = sum(1 for _, s in results if s == "PASS")
print(f" CI [{commit_label}]: বিল্ড {status} ({ok}/{len(results)} টেস্ট পাস)")
return status
ci = CIPipeline()
ci_status = ci.run_build(f"feature/task-tags @ {c3}", test_results)
print()
print("=" * 70)
print("ধাপ ৭ -- চূড়ান্ত মার্জ (M8/L35) -- ৩-ওয়ে মার্জ, ২-প্যারেন্ট মার্জ কমিট")
print("=" * 70)
merge_type = None
if pr.can_merge() and ci_status == "PASSED":
git_switch(repo, "main")
merge_hash, merge_type = git_merge(repo, "feature/task-tags", "Merge PR: টাস্কে 'urgent' ট্যাগিং ফিচার যোগ")
pr.status = "merged"
print(f" মার্জ সফল: {merge_hash} ({merge_type})")
print(f" মার্জ কমিটের parents: {repo['commits'][merge_hash]['parents']} (main={c4}, feature={c3} -- উভয়ই)")
else:
print(" মার্জ করা যায়নি -- PR অ্যাপ্রুভড নয় অথবা CI ফেইল করেছে।")
print()
print("=" * 70)
print("সারসংক্ষেপ প্রজেক্ট লগ -- সাতটি ধাপ, চূড়ান্ত অবস্থা")
print("=" * 70)
print(f" ১. ইউজার স্টোরি : {story.format_story()}")
print(f" ২. SOLID/DI ডিজাইন : TaskService <-(injected)- InMemoryTaskRepository")
print(f" ৩. Git ওয়ার্কফ্লো : main {c1}->{c4} | feature/task-tags {c1}->{c2}->{c3}")
print(f" ৪. টেস্ট স্যুট : {sum(1 for _, s in test_results if s == 'PASS')}/{len(test_results)} পাস (all_passed={all_passed})")
print(f" ৫. পুল রিকোয়েস্ট : status={pr.status}, can_merge()={pr.can_merge()}")
print(f" ৬. CI পাইপলাইন : {ci_status}")
print(f" ৭. চূড়ান্ত মার্জ : main এখন {repo['branches']['main']} ({merge_type})")
is_ancestor(repo, c4, c3) মিথ্যা (c4, c3-এর ইতিহাসে নেই) — তাই fast-forward
সম্ভব নয়, find_merge_base(c4, c3) সঠিকভাবে c1 রিটার্ন করে (উভয়ের সবচেয়ে সাম্প্রতিক কমন
পূর্বপুরুষ), এবং নতুন মার্জ কমিট c5-এর parents = [c4, c3] — একটি প্রকৃত দুই-প্যারেন্ট মার্জ
কমিট। তিনটি টেস্টই (AC1, AC2, AC3) পাস করে (আগের পাঠগুলোতে প্রতিটি হাতে-যাচাই করা হয়েছে), PR-এর
can_merge() প্রথমে False (changes_requested) তারপর True (approved) হয়, এবং CI "PASSED"
রিপোর্ট করে — ফলে সাতটি শর্তই পূরণ হয়ে চূড়ান্ত মার্জ সম্পন্ন হয়।
এই কোর্সের প্রতিটি মডিউল বিচ্ছিন্ন নয় — একটি বাস্তব ফিচার-ডেভেলপমেন্ট চক্র একসাথে রিকোয়ারমেন্ট এনালাইসিস, ভালো ক্লাস ডিজাইন, Git ব্রাঞ্চিং/মার্জিং, টেস্টিং, কোড রিভিউ, ও CI/CD ব্যবহার করে — এবং এই টুকরোগুলো একে অপরের উপর নির্ভরশীল (যেমন DIP-কমপ্লায়েন্ট ডিজাইন সরাসরি সহজ ইউনিট টেস্টিং সম্ভব করে তোলে)। এটিই "সফটওয়্যার ইঞ্জিনিয়ারিং" শব্দটির প্রকৃত অর্থ — শুধু কোড লেখা নয়, বরং এই সম্পূর্ণ, আন্তঃসংযুক্ত অনুশীলন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ ফিচার ব্রাঞ্চকে main-এ মার্জ করতে কেন একটি সত্যিকারের ৩-ওয়ে মার্জ (দুই-প্যারেন্ট মার্জ কমিট) লাগল, একটি সাধারণ fast-forward নয় — এটি main-এ কী ঘটছিল তা সম্পর্কে কী বোঝায়?
fast-forward তখনই সম্ভব যখন current (target) ব্রাঞ্চের টিপ source ব্রাঞ্চের টিপের একটি পূর্বপুরুষ — অর্থাৎ main ফিচার ব্রাঞ্চ তৈরি হওয়ার পর থেকে একদমই নড়েনি। যেহেতু PR রিভিউ প্রক্রিয়া (M9/L40) কিছুটা সময় নিয়েছে, বাস্তবসম্মতভাবে এই সময়ে অন্য কাজও main-এ মার্জ হয়েছে (উদাহরণে c4) — অর্থাৎ main ও feature/task-tags উভয় ব্রাঞ্চই সত্যিকারের ডাইভার্জ করেছে c1 থেকে। এই ডাইভারজেন্স মানেই git-কে উভয় দিকের পরিবর্তন একটি নতুন, দুই-প্যারেন্ট মার্জ কমিটে একত্র করতে হয় — যা ঠিক এই দৃশ্যপটে বাস্তবসম্মতভাবে ঘটার কথা।
প্র ০২ TaskService-এর constructor-এ TaskRepository ইনজেক্ট করার সিদ্ধান্ত (DIP, M4/L18) কীভাবে ধাপ ৪-এর ইউনিট টেস্ট লেখা সহজ করে তুলল?
কারণ TaskService কখনো নিজে কংক্রিট InMemoryTaskRepository তৈরি করে না — এটি বাইরে থেকে
পাস করা হয় (constructor-এর মাধ্যমে dependency injection)। ফলে প্রতিটি টেস্টে একটি তাজা, বিচ্ছিন্ন
InMemoryTaskRepository() সহজেই তৈরি করে TaskService-এ পাস করা যায় — কোনো real
ডেটাবেস, নেটওয়ার্ক কল, বা শেয়ার্ড স্টেট ছাড়াই। এটি সরাসরি M10/L44-এর ইউনিট-টেস্ট আইসোলেশন প্রয়োজনীয়তা
এবং M10/L47-এর টেস্ট-ডাবল প্র্যাক্টিস সম্ভব করে তোলে — DIP ছাড়া এই ধরনের সহজ, বিচ্ছিন্ন টেস্টিং অনেক
কঠিন হতো।
প্র ০৩ PR-এর can_merge() ইতিমধ্যেই True হওয়া সত্ত্বেও কেন চূড়ান্ত মার্জের আগে CI স্ট্যাটাসও আলাদাভাবে চেক করা হলো — একটি না থাকলে কী ঝুঁকি থাকত?
PR অ্যাপ্রুভাল (মানুষ রিভিউয়ারের মতামত) এবং CI পাস (স্বয়ংক্রিয় বিল্ড/টেস্ট যাচাই) — এই দুটো ভিন্ন, পরিপূরক কোয়ালিটি গেট, একে অপরের বিকল্প নয়। একজন মানব রিভিউয়ার ডিজাইন/লজিক সমস্যা দেখতে পারেন কিন্তু ভুলবশত একটি সূক্ষ্ম রানটাইম বাগ মিস করতে পারেন — CI সেই বাগ ধরবে প্রকৃতপক্ষে টেস্ট চালিয়ে। উল্টোদিকে, CI শুধু যা explicitly টেস্ট করা হয়েছে তা যাচাই করে, ডিজাইন কোয়ালিটি বা ব্যবসায়িক যুক্তিসঙ্গততা নয় — সেটা মানুষ রিভিউয়ারের কাজ। শুধু একটি গেট থাকলে, অন্যটি যা ধরত এমন সমস্যা কখনো কখনো অলক্ষিত থেকে যেত।
অনুশীলন
-
চিন্তা করুন: যদি রহিমের প্রথম রিভিউ (changes_requested) দেওয়ার আগেই CI চালানো হতো এবং CI "PASSED" রিপোর্ট করত, তাহলে কি ধাপ ৭-এ মার্জ হয়ে যেত? কেন বা কেন নয়?
না, মার্জ হতো না। ধাপ ৭-এর শর্ত হলো
pr.can_merge() and ci_status == "PASSED"— দুটোই সত্যি হতে হবে। CI পাস করলেও, যতক্ষণ PR-এর সর্বশেষ রিভিউ decision "changes_requested" থাকে,can_merge()False থাকবে (যেহেতু "approved" এখনো decisions তালিকায় নেই)। এটি দেখায় দুটো গেটই স্বাধীনভাবে সত্য হতে হয় — একটি পাস করলেই অন্যটির প্রয়োজনীয়তা এড়ানো যায় না। -
পরীক্ষা করুন: উপরের কোড সেলে ধাপ ৩-এ main-এর দ্বিতীয় কমিট c4 করার আগে বদলে দেখুন — যদি main-এ কোনো নতুন কমিটই না হতো (শুধু feature/task-tags-এ কমিট হতো), ধাপ ৭-এর
merge_typeকী প্রিন্ট হতো? কোড থেকে বদলে Run করে যাচাই করুন।যদি main c1-এই থেকে যেত (c4 তৈরি না হতো), তাহলে মার্জের সময়
is_ancestor(repo, c1, c3)সত্যি হতো (c1, c3-এর পূর্বপুরুষ, যেহেতু c3 এর chain c3→c2→c1)। ফলেgit_mergefast-forward পথ নিত — main-এর পয়েন্টার সরাসরি c3-তে সরে যেত, কোনো নতুন মার্জ কমিট তৈরি না হয়েই।merge_typeতখন "fast-forward" প্রিন্ট হতো, "three-way merge" নয় — ঠিক M8/L35-এর বর্ণনা অনুযায়ী।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস আবার দেখুন ৫৮টি পাঠ SDLC থেকে SOLID, ডিজাইন প্যাটার্ন, Git ও CI/CD পর্যন্ত — পুরো যাত্রা এক নজরে।
- Programming Languages & Compiler Design কোর্স পরবর্তী যাত্রা এই কোর্স শেখায় কীভাবে একটি প্রোডাক্ট প্রসেস ও কোলাবোরেশন দিয়ে তৈরি হয় — সেই কোর্স শেখায় যে ভাষায় সেই প্রোডাক্ট লেখা হয়, তা আসলে কীভাবে কাজ করে।
- Data Structures & Algorithms কোর্স সহোদর কোর্স এই কোর্সের প্রসেস ও কোলাবোরেশন ফাউন্ডেশনের পাশাপাশি, একটি সমাধান সঠিক ও দক্ষভাবে লেখার মূল ভিত্তি।