در بخش اول، انتخاب فناوری اپ یادداشت را به نیاز محصول و توان تیم وصل کردیم. حالا وقت ساخت نخستین قابلیت است: دیدن فهرست یادداشتها و اضافهکردن یک مورد تازه. این صفحه کوچک میتواند نقطه شروع خوبی باشد، به شرطی که از همان ابتدا نمایش، قواعد و ذخیرهسازی را در یک فایل درهم نریزیم.
هدف این بخش ساخت معماری بزرگ نیست. میخواهیم مشخص کنیم هر مسئولیت کجا قرار میگیرد و چطور بدون اتصال واقعی به سرور، رفتار اصلی را بررسی کنیم. مثال کد با TypeScript نوشته شده و به چارچوب رابط خاصی وابسته نیست؛ همین تفکیک را میتوان در زبان و ابزار منتخب پروژه پیاده کرد.
سه مسئولیت را از هم تشخیص دهیم
صفحه باید عنوان، فهرست و وضعیت بارگذاری را نمایش دهد. منطق قابلیت باید بداند افزودن یادداشت خالی مجاز نیست و پس از ذخیره چه دادهای باید بهروز شود. لایه داده هم مسئول خواندن و نوشتن در منبع واقعی است؛ امروز میتواند حافظه باشد و بعداً ذخیرهسازی محلی یا سرور شود.
اگر همه اینها در رویداد کلیک دکمه نوشته شوند، صفحه خیلی زود به محل تصمیمهای نامرتبط تبدیل میشود. تغییر محل ذخیرهسازی، متن خطا و شکل نمایش باید از هم قابل تشخیص باشند. این تفکیک به معنی ضرورت ساخت یک کلاس برای هر خط نیست؛ فقط باید مرز مسئولیت هنگام خواندن کد روشن بماند.
| بخش | مسئولیت | نباید مالک چه چیزی باشد؟ |
|---|---|---|
| رابط | نمایش وضعیت و دریافت رویداد | جزئیات فایل یا دیتابیس |
| منطق قابلیت | هماهنگی افزودن و خواندن یادداشت | رنگ، فاصله و چیدمان |
| منبع داده | خواندن و ذخیره پایدار | پیام نمایشی مخصوص صفحه |
مدل یادداشت را از شکل کارت جدا کنیم
در شروع، یادداشت یک شناسه و متن دارد. اینکه کارت آن چند خط نشان دهد یا رنگ پسزمینه چه باشد، بخشی از داده اصلی نیست. اگر اطلاعات نمایشی را بیدلیل داخل مدل ذخیره کنیم، تغییر رابط ممکن است مهاجرت داده لازم داشته باشد؛ در حالی که محتوای یادداشت عوض نشده است.
از طرف دیگر، هر چیزی هم نباید از مدل حذف شود. شناسه برای تشخیص یادداشت ضروری است و زمان ویرایش احتمالاً هنگام همگامسازی لازم میشود. آن را وقتی نیاز مشخص داریم اضافه میکنیم. فعلاً قصد نداریم طرح همگامسازی را پیشاپیش با فیلدهای حدسی پر کنیم.
یک قرارداد کوچک برای ذخیرهسازی کافی است
منطق افزودن لازم نیست بداند داده در کدام بسته یا دیتابیس ذخیره میشود. به یک قرارداد محدود نیاز دارد: فهرست را بخوان و یک یادداشت اضافه کن. نمونه زیر برای توضیح این مرز است؛ پیادهسازی پایدار دستگاه در بخش آفلاین اضافه خواهد شد.
type Note = { id: string; text: string };
type NotesStore = {
list(): Promise<Note[]>;
add(note: Note): Promise<void>;
};
async function createNote(
rawText: string,
store: NotesStore,
makeId: () => string,
): Promise<Note> {
const text = rawText.trim();
if (!text) throw new Error('EMPTY_NOTE');
const note = { id: makeId(), text };
await store.add(note);
return note;
}
این تابع جای مدیریت کل صفحه را نمیگیرد. فقط یک تصمیم روشن دارد: متن خالی ذخیره نمیشود و نتیجه موفق بعد از تمامشدن ذخیره برمیگردد. ساخت شناسه هم به ورودی جدا سپرده شده تا در آزمون، نتیجه قابل پیشبینی باشد. انتخاب نوع شناسه و تضمین یکتایی در محصول واقعی باید با منبع داده هماهنگ شود.
وضعیت صفحه را صریح کنیم
صفحه میتواند در حال خواندن باشد، فهرست آماده داشته باشد یا با خطای خواندن روبهرو شود. افزودن یادداشت نیز وضعیت خودش را دارد. یک متغیر مبهم مثل isLoading برای همه کارها ممکن است باعث شود هنگام ذخیره یک یادداشت، تمام فهرست ناپدید شود.
بهتر است روشن باشد بارگذاری اولیه با ذخیره در حال انجام فرق دارد. داده موجود را بیدلیل کنار نگذاریم و ورودی کاربر را هم پیش از قطعیشدن ذخیره پاک نکنیم. اگر ذخیره شکست خورد، متن باید باقی بماند تا کاربر بتواند دوباره تلاش کند یا آن را کپی کند.
- پیش از نخستین خواندن موفق، وضعیت انتظار نمایش داده شود.
- بعد از خواندن موفق فهرست خالی، پیام شروع کار نمایش داده شود.
- در خطای ذخیره، متن ورودی حفظ شود و نتیجه موفق نشان داده نشود.
- در موفقیت، نتیجه ذخیرهشده وارد فهرست شود و وضعیت ورودی مطابق طراحی تغییر کند.
رابط، خطای فنی را ترجمه میکند
کد EMPTY_NOTE میتواند در لایه نمایش به پیام «متن یادداشت را بنویسید» تبدیل شود. اما خطای دسترسی به ذخیرهسازی معنای دیگری دارد و نباید همان پیام را بدهد. استفاده از یک پیام کلی برای همه خطاها، هم تجربه کاربر و هم عیبیابی را ضعیف میکند.
در نمونه کوچک، یک کد خطا برای توضیح مرز کافی است. در پروژه بزرگتر شاید نوع نتیجه یا خطای دامنه روشنتری لازم باشد. نباید از همین حالا یک چارچوب پیچیده خطا بسازیم؛ کافی است تفاوت خطای ورودی و شکست زیرساخت از بین نرود.
آزمون را از رفتار اصلی شروع کنیم
برای بررسی تابع افزودن، لازم نیست اپ کامل بالا بیاید. یک منبع حافظهای میتواند دادههای ذخیرهشده را نگه دارد و ساخت شناسه هم مقدار ثابت آزمون را برگرداند. بررسی میکنیم متن با فاصله اضافی درست ذخیره میشود، متن خالی وارد منبع داده نمیشود و شکست ذخیره به موفقیت تبدیل نمیشود.
این آزمون جای آزمون دیتابیس واقعی یا رابط را نمیگیرد. فقط مرز مشخصی را پوشش میدهد. برای ماندگاری پس از بستن برنامه، باید پیادهسازی واقعی ذخیرهسازی را جداگانه روی دستگاه بررسی کنیم. نگهداشتن داده در آرایه حافظهای، قابلیت آفلاین پایدار نیست.
مالکیت داده را یکی نگه داریم
اگر صفحه فهرست یک نسخه از یادداشتها داشته باشد و ویرایشگر نسخه دیگری، احتمال ناسازگاری بالا میرود. مشخص کنید منبع معتبر داده کجاست و صفحه چطور تغییر را میبیند. لازم نیست همین حالا کتابخانه مدیریت وضعیت انتخاب شود؛ تصمیم مهم این است که هر صفحه مستقلاً حقیقت تازهای تولید نکند.
این موضوع هنگام حذف هم خودش را نشان میدهد. اگر یک یادداشت حذف شد، صفحه جزئیات نباید با داده قدیمی بهسادگی آن را دوباره ذخیره کند. فعلاً همین سناریو را در فهرست آزمونها ثبت میکنیم؛ جزئیات وضعیت مشترک را در بخش مدیریت State باز میکنیم.
چرخه عمر را به بعد موکول نکنیم
در موبایل ممکن است صفحه از بین برود یا فرایند برنامه توسط سیستمعامل بسته شود. داده مهم کاربر نباید فقط به عمر یک کامپوننت وابسته باشد. همچنین اگر یک خواندن دیر تمام شد، نتیجه آن نباید کورکورانه وضعیت صفحهای را که دیگر فعال نیست تغییر دهد.
روش عملی لغو یا نادیدهگرفتن نتیجه به ابزار انتخابی وابسته است. در این بخش API خاصی پیشنهاد نمیکنیم؛ رفتار مورد انتظار را ثبت میکنیم تا هنگام اتصال منطق به رابط بررسی شود. راهنمای لایه UI اندروید و راهنمای معماری Flutter هر دو برای روشنکردن مالکیت وضعیت و جدایی نمایش از داده مفیدند.
چه چیزی را فعلاً نسازیم؟
برای یک فهرست کوچک، ساخت چندین لایه مشابه، سرویس سراسری برای همه دادهها یا سیستم پلاگین ضرورت ندارد. قرارداد باید به نیاز فعلی محدود بماند. اگر فقط خواندن و افزودن لازم است، متدهایی برای جستوجوی پیچیده و همگامسازی فرضی اضافه نکنیم.
معماری ابتدایی خوب، تغییر بعدی را قابل فهم میکند. قرار نیست همه آینده را پیشبینی کند. اگر افزودن ویرایش باعث شد قرارداد داده تغییر کند، این لزوماً شکست طراحی نیست؛ مهم این است که تغییر در مرز مشخصی رخ دهد و قواعد قبلی با آزمون محافظت شوند.
تمرین این بخش، اضافهکردن یک آزمون برای شکست ذخیره و نوشتن رفتار رابط در همان حالت است. سپس بررسی کنید اگر کاربر متن فارسی و انگلیسی را ترکیب کند، انتخاب متن و جهت نمایش چه میشود. بخش بعد به رابط فارسی میپردازد؛ جایی که راستچینکردن ظرف بهتنهایی کافی نیست.
