পাঠ ১৩ · ৫৭-এর মধ্যে · মডিউল ৩
Home / Courses / Mobile App Development / ক্লিন আর্কিটেকচার

মোবাইলের জন্য ক্লিন আর্কিটেকচার লেয়ার

Clean architecture layers for mobile
১০ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ক্লিন আর্কিটেকচারের তিনটি লেয়ার এবং তাদের নির্ভরতার দিক (dependency direction)
  • কীভাবে Domain লেয়ারের একটি use case একটি abstract রিপোজিটরি ইন্টারফেসের ওপর নির্ভর করে, কোনো concrete ইমপ্লিমেন্টেশনের ওপর নয়
  • একটি সত্যিকারের ফেক/ইন-মেমরি রিপোজিটরি বনাম রিমোট-ব্যাকড রিপোজিটরি — একই ইন্টারফেস, ভিন্ন বাস্তবায়ন
  • কেন এই ডিজাইন টেস্টিং সহজ করে ও FSWF-এর ডিপেন্ডেন্সি ইনজেকশনের সাথে একই যুক্তি শেয়ার করে

১ · তিনটি লেয়ার — Presentation → Domain → Data

Presentation
UI ও ViewModel (L11-এর MVVM) — স্ক্রিনে কী দেখানো হবে তা সামলায়, কোনো বিজনেস নিয়ম নিজে জানে না।
Domain
Use case ফাংশন/ক্লাস — অ্যাপের প্রকৃত বিজনেস লজিক এখানে থাকে, কোনো UI বা ডেটা-সোর্স নির্দিষ্ট বিষয় জানে না।
Data
Repository-এর concrete বাস্তবায়ন — লোকাল স্টোরেজ (M8) বা রিমোট নেটওয়ার্ক (M9) থেকে প্রকৃত ডেটা আনে।

মূল নিয়ম: নির্ভরতা সবসময় বাইরে থেকে ভেতরে — Presentation Domain-এর ওপর নির্ভর করে, Domain কখনো Presentation বা কোনো concrete Data ক্লাস সম্পর্কে জানে না। Domain শুধু একটি abstract ইন্টারফেস ("এমন কিছু যার get_all_notes() মেথড আছে") জানে — বাস্তবে সেটা লোকাল SQLite নাকি একটি রিমোট সার্ভার, তা Domain-এর কাছে অপ্রাসঙ্গিক।

Presentation NotesViewModel Domain get_recent_notes_use_case() শুধু abstract ইন্টারফেস জানে NotesRepository (interface) InMemoryNotesRepository (fake) RemoteBackedNotesRepository
দুটি ভিন্ন concrete রিপোজিটরিই একই ইন্টারফেস বাস্তবায়ন করে -- Domain layer-এর use case কোনটা ব্যবহার হচ্ছে তা জানে না।

২ · একটি নোট-স্ক্রিনের ক্লিন আর্কিটেকচার — বাস্তব কোড

নিচের কোডে NotesRepository একটি Protocol (abstract ইন্টারফেস — কোন মেথড থাকতে হবে তার চুক্তি, কোনো বাস্তবায়ন নয়)। get_recent_notes_use_case() শুধু এই ইন্টারফেসের ওপর নির্ভর করে লেখা — এটি জানে না রিপোজিটরিটি ফেক নাকি রিমোট-ব্যাকড।

Python
from typing import Protocol

# ---------- ABSTRACT ইন্টারফেস -- Domain এই "চুক্তি"-র বিরুদ্ধে কোড লেখে ----------
class NotesRepository(Protocol):
    def get_all_notes(self) -> list: ...
    def add_note(self, text: str) -> dict: ...


# ---------- DOMAIN LAYER -- শুধু abstract ইন্টারফেসের ওপর নির্ভর করে, concrete কিছু জানে না ----------
def get_recent_notes_use_case(repository: NotesRepository, limit: int) -> list:
    all_notes = repository.get_all_notes()
    sorted_notes = sorted(all_notes, key=lambda n: n["created_at"], reverse=True)
    return sorted_notes[:limit]


# ---------- DATA LAYER -- দুটি concrete রিপোজিটরি, একই ইন্টারফেস ----------
class InMemoryNotesRepository:
    """ফেক/ইন-মেমরি রিপোজিটরি -- সাধারণত ইউনিট টেস্টে ব্যবহৃত হয়"""
    def __init__(self):
        self._notes = []
        self._next_id = 1

    def get_all_notes(self):
        return list(self._notes)   # কপি রিটার্ন করা -- কলার যেন মূল লিস্ট মিউটেট করতে না পারে

    def add_note(self, text):
        note = {"id": self._next_id, "text": text, "created_at": self._next_id}
        self._notes.append(note)
        self._next_id += 1
        return note


class RemoteBackedNotesRepository:
    """একটি সিমুলেটেড রিমোট সার্ভার -- বাস্তব অ্যাপে এটি HTTP কল করতো (M9)"""
    def __init__(self, simulated_server_data):
        self._server = simulated_server_data   # dict, একটি রিমোট ডেটাবেস হিসেবে
        self.request_count = 0

    def get_all_notes(self):
        self.request_count += 1
        return list(self._server.values())

    def add_note(self, text):
        self.request_count += 1
        new_id = max(self._server.keys(), default=0) + 1
        note = {"id": new_id, "text": text, "created_at": new_id}
        self._server[new_id] = note
        return note


# ---------- PRESENTATION LAYER -- Domain-এর ওপর নির্ভর করে, কোন রিপোজিটরি তা জানে না ----------
class NotesViewModel:
    def __init__(self, repository):
        self.repository = repository

    def load_recent_notes(self, limit=2):
        recent = get_recent_notes_use_case(self.repository, limit)
        print(f"[Presentation] সাম্প্রতিক {len(recent)}টি নোট:")
        for note in recent:
            print(f"   - {note['text']}")
        return recent


print("== ফেক/ইন-মেমরি রিপোজিটরি দিয়ে ==")
fake_repo = InMemoryNotesRepository()
fake_repo.add_note("প্রথম নোট")
fake_repo.add_note("দ্বিতীয় নোট")
fake_repo.add_note("তৃতীয় নোট")

vm1 = NotesViewModel(fake_repo)
vm1.load_recent_notes(limit=2)

print("\n== রিমোট-ব্যাকড রিপোজিটরি দিয়ে (হুবহু একই NotesViewModel ও use case কোড) ==")
simulated_remote_db = {
    1: {"id": 1, "text": "সার্ভারে থাকা প্রথম নোট", "created_at": 1},
    2: {"id": 2, "text": "সার্ভারে থাকা দ্বিতীয় নোট", "created_at": 2},
}
remote_repo = RemoteBackedNotesRepository(simulated_remote_db)
remote_repo.add_note("সার্ভারে নতুন যোগ হওয়া নোট")

vm2 = NotesViewModel(remote_repo)   # ভিন্ন repository, কিন্তু ViewModel/use case কোড অপরিবর্তিত
vm2.load_recent_notes(limit=2)

print(f"\nরিমোট রিপোজিটরিতে মোট রিকোয়েস্ট হয়েছে: {remote_repo.request_count}")

    
লক্ষ্য করুন NotesViewModel ও get_recent_notes_use_case()-এর কোডে InMemoryNotesRepository বা RemoteBackedNotesRepository-এর নাম একবারও লেখা নেই — শুধু repository.get_all_notes() কল করা হয়েছে, যা উভয় ক্লাসেই আছে (একই ইন্টারফেস)। remote_repo.request_count শেষে ২ হয় — একবার add_note() কলে, আরেকবার get_recent_notes_use_case()-এর ভেতরের get_all_notes() কলে — এটি দেখায় রিমোট রিপোজিটরি প্রতিটি অপারেশনে সত্যিই একটি (সিমুলেটেড) নেটওয়ার্ক-সদৃশ কলের হিসেব রাখছে, যেখানে ফেক রিপোজিটরির কোনো এমন খরচ নেই — এই পার্থক্যটাই টেস্টে ফেক রিপোজিটরি ব্যবহারের মূল সুবিধা (দ্রুত, খরচহীন)।
মূল কথা · Key takeaway

ক্লিন আর্কিটেকচারে Domain layer-এর use case কখনো জানে না তার ডেটা কোথা থেকে আসছে — এটি শুধু একটি abstract ইন্টারফেসের ওপর নির্ভর করে, ঠিক FSWF-এর ডিপেন্ডেন্সি ইনজেকশন (L21)-এর DIContainer.resolve()-এর মতোই মূলনীতি। এই বিচ্ছিন্নতাই একটি use case-কে ইউনিট টেস্টে ফেক রিপোজিটরি দিয়ে দ্রুত টেস্ট করা, আর প্রোডাকশনে রিমোট-ব্যাকড রিপোজিটরি দিয়ে চালানো — উভয়ই এক কোড দিয়ে সম্ভব করে তোলে।

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

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

প্র ০১ যদি get_recent_notes_use_case()-এর ভেতরে সরাসরি InMemoryNotesRepository() তৈরি করে ব্যবহার করা হতো, তাহলে কী সমস্যা হতো?

তাহলে use case-টি চিরকালের জন্য InMemoryNotesRepository-র সাথে "হার্ডকোডেড" হয়ে যেত — প্রোডাকশনে রিমোট-ব্যাকড রিপোজিটরি ব্যবহার করতে চাইলে use case-এর কোড বদলাতে হতো, এবং ইউনিট টেস্টে একটি সত্যিকারের ইন-মেমরি ডেটাসেট ছাড়া অন্য কিছু (যেমন একটি ইচ্ছাকৃত এরর-থ্রো-করা ফেক) ইনজেক্ট করার কোনো উপায়ই থাকতো না — ঠিক FSWF L21-এর "হার্ডকোডেড ডিপেন্ডেন্সি" সমস্যাটাই এখানেও ঘটতো।

প্র ০২ Domain layer কেন Presentation layer সম্পর্কে কিছুই জানে না?

কারণ একই get_recent_notes_use_case() সম্ভবত একাধিক ভিন্ন স্ক্রিনে ব্যবহৃত হতে পারে (যেমন একটি "সাম্প্রতিক নোট" উইজেট আর একটি "সব নোট" স্ক্রিন) — যদি use case-টি কোনো নির্দিষ্ট ViewModel বা UI ফরম্যাট সম্পর্কে জানতো, তাহলে সেটি আর পুনঃব্যবহারযোগ্য থাকতো না। Domain layer শুধু বিজনেস লজিক (কোন নোটগুলো "সাম্প্রতিক") নিয়ে কাজ করে, সেই ডেটা UI-তে কীভাবে দেখানো হবে তা সম্পূর্ণ Presentation layer-এর দায়িত্ব।

প্র ০৩ যদি vm2 = NotesViewModel(remote_repo)-এর বদলে vm2 = NotesViewModel(fake_repo) ব্যবহার করা হতো, তাহলে get_recent_notes_use_case()-এর কোনো কোড বদলাতে হতো কি?

না, একটুও না — এটিই এই ডিজাইনের মূল লক্ষ্য। NotesViewModel ও get_recent_notes_use_case() কোথাও কোনো নির্দিষ্ট রিপোজিটরি ক্লাসের নাম উল্লেখ করে না — শুধু "যেকোনো কিছু যার get_all_notes()/add_note() মেথড আছে" এটুকু ধরে নেয়। তাই যেকোনো নতুন রিপোজিটরি (যেমন ভবিষ্যতে একটি লোকাল SQLite-ব্যাকড রিপোজিটরি, M8-এ শেখানো হবে) এই একই ইন্টারফেস মেনে লিখলেই, use case বা ViewModel-এর কোনো কোড না বদলেই সোয়াপ করা যাবে।

অনুশীলন

  1. চিন্তা করুন: একটি টেস্টে আপনি চান রিপোজিটরি ইচ্ছাকৃতভাবে একটি এরর থ্রো করুক (যেমন নেটওয়ার্ক ব্যর্থ হওয়ার সিমুলেশন), যাতে দেখা যায় use case বা ViewModel এই পরিস্থিতি সঠিকভাবে সামলায় কি না। এই ধরনের একটি "ফেইলিং রিপোজিটরি" কীভাবে ডিজাইন করবেন বলে মনে হয়?

    একটি তৃতীয় ক্লাস FailingNotesRepository লেখা যেতে পারে, যা একই NotesRepository ইন্টারফেস মেনে চলে (get_all_notes()/add_note() মেথড থাকবে) কিন্তু get_all_notes()-এর ভেতরে সরাসরি raise ConnectionError("সিমুলেটেড নেটওয়ার্ক ব্যর্থতা") থ্রো করবে। যেহেতু এটি একই ইন্টারফেস মেনে চলে, NotesViewModel(FailingNotesRepository()) তৈরি করে ঠিক একই load_recent_notes() কল করলেই দেখা যাবে use case/ViewModel এরর-হ্যান্ডলিং সঠিকভাবে করে কি না — কোনো কোড না বদলেই।

  2. পরীক্ষা করুন: উপরের কোড সেলে vm1.load_recent_notes(limit=2)-এর limit-কে 5-এ পরিবর্তন করুন (ফেক রিপোজিটরিতে মোট মাত্র ৩টি নোট আছে) এবং দেখুন আউটপুটে কতটি নোট দেখানো হয়।

    sorted_notes[:5] — যেহেতু পাইথনের স্লাইসিং লিস্টের দৈর্ঘ্যের চেয়ে বড় ইনডেক্স চাইলে এরর দেয় না, শুধু যা আছে তা-ই রিটার্ন করে — মাত্র ৩টি নোটই প্রিন্ট হবে (len(recent) হবে ৩, ৫ নয়), কারণ ফেক রিপোজিটরিতে সাকুল্যে ৩টি নোটই যোগ করা হয়েছিল।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাপ লাইফসাইকেল, মোবাইল UI/UX, MVVM/MVI আর্কিটেকচার, নেটিভ বনাম ক্রস-প্ল্যাটফর্ম, নেভিগেশন, স্টেট ম্যানেজমেন্ট, লোকাল স্টোরেজ, ডিভাইস ফিচার ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • FSWF · Dependency Injection in Back-End Frameworks সহোদর পাঠ DIContainer ও হার্ডকোডেড বনাম ইনজেক্টেড ডিপেন্ডেন্সির সাধারণ ধারণা এই পাঠেই তৈরি হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks ও Mobile App Development — সব এক জায়গায়।
আগের পাঠ
MVI ও ইউনিডাইরেকশনাল ডেটা ফ্লো