ویرگول
ورودثبت نام
امیرمحمد ابوالحسنی
امیرمحمد ابوالحسنیبرنامه نویس فرانت‌اند
امیرمحمد ابوالحسنی
امیرمحمد ابوالحسنی
خواندن ۱۶ دقیقه·۷ ماه پیش

کدنویسی تمیز: نکات کلیدی از دوره‌ی Maximilian Schwarzmüller - بخش دوم

در این مقاله قصد دارم نکاتی را که از مشاهدهی دورهی «کد تمیز» به تدریس Maximilian Schwarzmüller آموختهام، با نگارشی شخصی و ساده با شما به اشتراک بگذارم. اگر هنوز بخش اول این مقاله را مطالعه نکردهاید، توصیه میشود پیش از ادامه، آن را مرور کنید. اکنون به سراغ این موضوع میرویم که چه عواملی یک تابع یا متد تمیز را تعریف میکنند.

توابع و متدها

نوشتن یک تابع تمیز مستلزم توجه به چند نکتهی کلیدی است. هم نگارش و هم فراخوانی تابع باید ساده، شفاف و قابلدرک باشد. کار با توابع نباید پیچیده یا گیجکننده باشد.

یکی از اصول بنیادین در این زمینه، توجه به تعداد و ترتیب ورودیهاست. هرچه یک تابع پارامترهای کمتری داشته باشد، خواندن و نگهداری کد آسانتر خواهد بود. در حالت ایدهآل، یک تابع نباید هیچ ورودیای داشته باشد. در چنین حالتی، نیازی به در نظر گرفتن ترتیب پارامترها نخواهد بود و درک رفتار تابع بسیار سادهتر میشود. هرچند دستیابی به این وضعیت همواره ممکن نیست، اما در صورت امکان، این بهترین حالت محسوب میشود.

یک تابع با تنها یک ورودی همچنان خوانا و قابلاستفاده باقی میماند، زیرا در این حالت ترتیب ورودیها اهمیتی ندارد. وجود دو ورودی نیز قابلقبول است، اما باید با دقت بیشتری از آنها استفاده شود. زمانی که تعداد ورودیها به سه یا بیشتر میرسد، درک و استفاده از تابع دشوارتر میشود؛ بنابراین بهتر است از چنین شرایطی پرهیز شود.

اگر کاهش تعداد ورودیها ممکن نباشد، میتوان آنها را در قالب یک شیء (برای مثال، یک شیء در JavaScript) گروهبندی کرد. این روش باعث میشود نام پارامترها واضحتر باشد و ترتیب آنها نیز دیگر اهمیت نداشته باشد. برای نمونه:

// Dirty Code const user = new User('Max', 31, 'max@test.com'); // Clean Code const user = new User({ name: 'Max', email: 'max@test.com', age: 31 });

گاهی بهجای نوشتن یک تابع مستقل، بهتر است یک کلاس ایجاد کرده و متدهای مرتبط را بهصورت ساختاریافته و شفاف درون آن قرار داد. این کار موجب میشود کد خواناتر و از نظر مفهومی شفافتر باشد. علاوه بر این، محیطهای توسعه (IDEها) هنگام استفاده از کلاسها با ارائهی پیشنهادها و راهنماییهایی دربارهی پارامترها و نحوهی استفاده از متدها، به درک بهتر کد کمک میکنند. برای مثال:

class User { constructor(name, age, email) { this.name = name; this.age age; this.email = email; } } const user = new User('Max', 31, 'max@test.com');

نکتهای مهم آن است که گاهی ممکن است وجود دو ورودی در نگاه اول قابلقبول به نظر برسد، اما در واقع یکی از آنها زائد باشد. برای مثال، زمانی که یک نوع خاص را بهعنوان ورودی دریافت میکنیم و سپس براساس آن نوع عملیات متفاوتی انجام میدهیم، بهتر است برای هر نوع، یک تابع جداگانه نوشته شود. این کار باعث سادهتر شدن ساختار کد و افزایش خوانایی آن میشود. برای نمونه:

// Dirty Code function log(message, isError) { if (isError) { console.error(message); } else { console.log(message); } } log('Hi there!', false); // Clean Code function log(message) { console.log(message); } function logError(errorMessage) { console.error(errorMessage); } log('Hi there!'); LogError('An error!');

ورودیهای پویا نیز نیازمند توجه ویژهای هستند. در بسیاری از زبانهای برنامهنویسی، روشهایی برای مدیریت این نوع ورودیها وجود دارد. برای مثال، در زبان JavaScript میتوان از عملگر پخش (spread operator) استفاده کرد تا تعداد نامشخصی از ورودیها را دریافت نمود. این روش به ما اجازه میدهد که تعداد ورودیها را بهصورت انعطافپذیر مدیریت کنیم، در حالی که همچنان خوانایی کد حفظ میشود.

function sumUp(...numbers) { let sum = 0; for (const number of numbers) { sum += number; return sum; } } const total = sumUp(10, 19, -3, 22, 5, 100);

نکته مهم دیگر این است که باید از خروجیهای غیرمنتظره پرهیز کنیم. توابع باید خودبسنده باشند و از اصول توابع خودبسنده (Pure Functions) پیروی کنند. یک تابع خودبسنده، خروجی را تنها بر اساس ورودیها تولید میکند و هیچ اثر جانبی (Side Effect) ندارد. اگر یک تابع تغییراتی ایجاد کند که بر ورودیها تأثیر بگذارد یا وضعیت سیستم را دگرگون سازد، دیگر یک تابع خودبسنده محسوب نمیشود و دارای اثر جانبی خواهد بود.

از این رو، باید تا حد امکان از اثرهای جانبی ناخواسته اجتناب کرد. اگر ایجاد چنین اثری اجتنابناپذیر باشد، لازم است یا در نام تابع به آن اشاره شود یا این تغییرات را به توابع جداگانه منتقل کنیم.

برای مثال، قطعه کد زیر مناسب نیست زیرا باعث میشود شیء کاربر بهطور غیرمنتظرهای تغییر کند:

function createId(user) { user.id = 'ul'; } const user = { name: 'Max' }; createId(user); console.log(user);

کد زیر تا حدی قابلقبول است، زیرا هرچند تغییری در شیء کاربر ایجاد میشود، اما تابع بهصراحت این تغییر را نشان میدهد:

function addId(user) { user.id = 'ul'; } const user = { name: 'Max' }; addId(user); console.log(user);

کد زیر بهترین حالت است، زیرا کاملاً واضح و صریح است که تغییرات انجامشده در شیء کاربر عمدی است:

class User { constructor(name) { this.name = name; } addId() { this.id = 'ul'; } } const customer = new User('Max'); customer.addId(); console.log(customer);

بدنهی یک تابع باید کوتاه و ساده باشد. نوشتن توابع کوتاه و ساده یکی از اصول بنیادی برنامهنویسی است. توابع کوچکتر این امکان را فراهم میکنند که نام آنها بهطور واضح هدفشان را بیان کند، که باعث خوانایی و درک بهتر کد میشود. بهطور کلی، بهجای نوشتن یک تابع پیچیده، بهتر است آن را به توابع کوچکتر تقسیم کنیم که هرکدام وظیفهی خاصی را انجام دهند.

ما باید بخشهای کدی را که وظیفه خاصی را انجام میدهند شناسایی کرده و آنها را در توابع جداگانه قرار دهیم. علاوه بر این، بخشهایی از کد که نیاز به تفسیر بیشتری نسبت به کدهای اطراف خود دارند باید جدا شوند تا خواندن و درک آنها آسانتر باشد.

توابع باید تنها یک کار انجام دهند. این به این معنا نیست که هر تابع باید تنها یک خط کد داشته باشد؛ زیرا در این صورت تعداد توابع بهطور قابلملاحظهای زیاد میشود که خود میتواند مشکلساز باشد.

اگر به یک تابع نگاه کنیم و متوجه شویم که بیشتر از یک کار انجام میدهد، آن تابع باید به توابع کوچکتر تقسیم شود. برای مثال، ممکن است یک تابع همزمان اقدام به اعتبارسنجی (Validation)، ساخت محتوا و نمایش دادهها کند. چنین توابعی باید به اجزای کوچکتر تقسیم شوند.

برای تعیین زمان تقسیم یک تابع به توابع کوچکتر، از مفهومی به نام سطوح انتزاع (Levels of Abstraction) استفاده میکنیم. در یک تابع، اگر بخشهایی از کد در سطوح مختلف انتزاع قرار داشته باشند، این نشاندهندهی این است که باید تابع به توابع کوچکتر تقسیم شود.

در طراحی توابع، یکی از اصول کلیدی این است که هر تابع باید فقط یک وظیفه خاص را انجام دهد. به این معنی که تمامی خطوط کد در داخل یک تابع باید برای دستیابی به یک هدف واحد خدمت کنند. اگر یک تابع چندین کار مختلف انجام میدهد، باید آن را به توابع کوچکتر تقسیم کرد.

در برنامهنویسی، کد میتواند در سطوح مختلف انتزاع قرار داشته باشد:

  • کد سطح پایین (Low-level code): مانند email.includes() که یک متد داخلی زبان برنامهنویسی است و ما دقیقاً میدانیم که چه کاری انجام میدهد.
  • کد سطح بالا (High-level code): مانند saveUser() که توسط برنامهنویس نوشته شده و نام آن به وضوح توضیح میدهد که چه کاری انجام میدهد، حتی اگر ندانیم که درون آن چه اتفاقی میافتد.

نوشتن کد در سطوح بالا یا پایین بهطور خود به خود نه خوب است و نه بد؛ نکته مهم این است که این سطوح را در زمینه مناسب بهکار ببریم. اگر در یک تابع از کد سطح پایین استفاده میکنید، نام تابع باید به اندازه کافی توصیفی باشد تا درک آن برای دیگران آسانتر شود.

از ترکیب سطوح مختلف انتزاع در یک تابع پرهیز کنید. اگر یک تابع کارهایی مانند ساخت دادهها، اعتبارسنجی و نمایش خروجی را همزمان انجام میدهد، این تابع شامل سطوح مختلف انتزاع است و باید به بخشهای کوچکتر تقسیم شود.

چه زمانی باید بخشی از کد را در یک تابع جداگانه قرار دهیم؟

زمانی که چندین خط کد دارید که همه به یک هدف خاص مرتبط هستند، میتوانید آنها را در یک تابع جداگانه قرار دهید.

اگر بخشی از کد پیچیدهتر از کدهای اطراف خود باشد و نیاز به توضیح بیشتری داشته باشد، بهتر است آن را در یک تابع جداگانه با نام توصیفی قرار دهید.

یکی از مزایای بزرگ تقسیم کد به توابع کوچکتر، قابلیت استفاده مجدد است. باید از نوشتن کد تکراری پرهیز کنیم. در واقع، هرگز نباید کد خود را تکرار کنیم. بنابراین، هر منطقی که در چندین قسمت از کد تکرار میشود، باید در یک تابع اختصاصی قرار گیرد تا از اضافهگویی جلوگیری شود.

با این حال، باید توجه داشت که خوانایی کد همیشه باید اولویت اول باشد. تقسیم توابع به بخشهای کوچکتر باید با دقت انجام شود زیرا گاهی تقسیم غیرضروری ممکن است خوانایی کد را کاهش دهد. برای مثال، اگر برای هر عملیات یک تابع جداگانه ایجاد کنیم و نتوانیم نام مناسبی برای آن پیدا کنیم یا دائماً نیاز به جستجو برای توابع مختلف داشته باشیم، ممکن است این کار پیچیدگی ایجاد کند و به جای بهبود خوانایی، کد را دشوارتر کند. در چنین مواقعی، بهتر است تابع را یکپارچه نگه داریم.

در نهایت، باید سعی کنیم توابع خود را خود بسنده نگه داریم. توابع خودبسنده، توابعی هستند که پیشبینیپذیرند و هیچ اثر جانبی غیرمنتظرهای ندارند. این نوع توابع راحتتر قابل تست و نگهداری هستند.

تستهای واحد یکی از ابزارهای اساسی در نوشتن کد تمیز هستند. زمانی که قصد داریم برای کد تست بنویسیم، باید از خود بپرسیم: آیا این تابع به راحتی قابل تست است؟ اگر پاسخ مثبت باشد، احتمالاً کد تمیزی نوشتهایم. در غیر این صورت، باید کد را بازنویسی کرده و توابع را به بخشهای کوچکتر تقسیم کنیم تا قابل تستتر و تمیزتر شوند.

ساختارهای کنترلی

هنگام کار با ساختارهای کنترلی مانند حلقهها و عبارات شرطی، یکی از مشکلاتی که ممکن است پیش بیاید پیچیدگی کد است. چنین کدی ممکن است برای ما یا دیگران بهراحتی قابلفهم نباشد. یکی از راهحلها برای رفع این مشکل، کاهش استفاده از ساختارهای شرطی تو در تو (nested) است.

برای این منظور، میتوانیم از تکنیکهایی مانند Guard و Fail-Fast استفاده کنیم.

  • گارد به روشهایی اطلاق میشود که در آن شرایط یا خطاهای خاص به محض بروز، فوراً مدیریت میشوند و جریان اصلی کد بلافاصله متوقف میشود. استفاده از Guard به ما این امکان را میدهد که شرایط خاص یا خطاها را بهسادگی مدیریت کرده و آنها را از منطق اصلی جدا کنیم.
  • فیلفست یک الگوی طراحی است که در آن برنامه یا سیستم بلافاصله پس از شناسایی خطا یا مشکل متوقف میشود و از ادامهی اجرا جلوگیری میکند. این الگو در محیطهایی مفید است که خطاهای زودهنگام میتوانند مشکلات بزرگی ایجاد کنند و نیاز به شناسایی و رفع سریع دارند.

در مثال زیر، از این تکنیکها برای بهینهسازی کد استفاده شده است:

// Dirty Code function handleCheckout(user, cart) { if (user) { if (cart.items.length > 0) { if (cart.isStockAvailable) { proceedToCheckout(); } else { alert("Some items are out of stock."); } } else { alert("Your cart is empty."); } } else { alert("Please log in to continue."); } } // Clean Code function handleCheckout(user, cart) { if (!user) return alert("Please log in to continue."); if (cart.items.length === 0) return alert("Your cart is empty."); if (!cart.isStockAvailable) return alert("Some items are out of stock."); proceedToCheckout(); }

با منفی کردن شرایط و استفاده از خطاها، میتوانیم کد را از پیچیدگیهای غیرضروری رها کنیم. این روش به ما اجازه میدهد تا کد را سادهتر نگه داریم و از گم شدن در لایههای متعدد شرایط جلوگیری کنیم. به جای پیچیده کردن کد با شرایط تو در تو، میتوانیم آن را بهطور واضح و مستقیم با منفی کردن شرایط یا معرفی خطاها مدیریت کنیم.

در مثال زیر، از این تکنیک برای سادهسازی کد استفاده شده است:

// Dirty Code if (user && !user.isLoggedIn && product.isActive && !product.isDeleted) { addToFavorites(product); } // Clean Code function canFavorite(user, product) { return user && !user.isLoggedIn && product.isActive && !product.isDeleted; } if (canFavorite(user, product)) { addToFavorites(product); }

با نوشتن گاردها در توابع جداگانه و دادن نامهای توصیفی به آنها، میتوانیم به گونهای عمل کنیم که تنها با خواندن نام تابع، بهراحتی شرایطی که باید کد در آنها اجرا شود را درک کنیم.

یکی از روشهای موثر برای جلوگیری از پیچیدگی زیاد در کد، استفاده از پلیمورفیسم (اشکال متعدد) و توابع کارخانهای است. با استفاده از این تکنیکها، میتوانیم کدی بنویسیم که هم قابل گسترش و هم انعطافپذیر باشد.

  • فرض کنید چندین نوع شیء داریم که ساختار مشابهی دارند، اما رفتارهای متفاوتی نشان میدهند. به جای شلوغ کردن کد با دستورات if یا switch، میتوانیم از پلیمورفیسم استفاده کنیم تا برای هر نوع شیء پیادهسازی جداگانهای ارائه دهیم. در این صورت، هر شیء میداند که باید چگونه رفتار کند و دیگر نیازی به گنجاندن شرایط در کد اصلی مانند "اگر این باشد، آن را انجام بده؛ اگر آن باشد، این را انجام بده" نداریم. مزایای استفاده از این الگوها قابل توجه است: ما شرایط پیچیده و اضافی را از بین میبریم که به حفظ کد تمیزتر و سادهتر کمک میکند. علاوه بر این، افزودن یک نوع جدید ساده میشود—میتوانیم به راحتی یک شیء جدید معرفی کنیم بدون نیاز به تغییر کد موجود.

در ادامه به یک مثال ساده در جاوااسکریپت میپردازیم که نشان میدهد چگونه میتوانیم با استفاده از پلیمورفیسم و تابع کارخانهای شرایط را حذف کرده و کدی مقیاسپذیرتر بنویسیم. در این مثال که نمایش پیام خوشآمدگویی برای کاربران با نقشهای مختلف را نشان میدهد، کد را با استفاده از پلیمورفیسم و توابع کارخانهای تمیز خواهیم کرد.

// Dirty Code function welcomeMessage(user) { if (user.role === "admin") { return "Welcome admin!" } else if (user.role === "guest") { return "Welcome guest!" } return "Welcome user!" } // Clean Code function createUser(role) { const roles = { admin: () => ({ welcome: () => "Welcome admin!" }), guest: () => ({ welcome: () => "Welcome guest!" }), }; return roles[role]?.() || { welcome: () => "Welcome user!" }; }

خطاها

یکی از اشتباهات رایجی که بسیاری از توسعهدهندگان مرتکب میشوند، استفاده از عبارات مانند if برای بررسی شرایط به جای مدیریت مستقیم خطاها است. زمانی که با شرایط خطا مواجه میشویم، باید از خطاها برای مدیریت آنها استفاده کنیم. اگر چیزی قرار است یک خطا باشد، باید خطا را مستقیماً مدیریت کنیم و از دور زدن این مرحله با استفاده از عبارت if خودداری کنیم.

این رویکرد کد ما را سادهتر کرده و رفتارهای خطا را واضح میکند. به جای تلاش برای جلوگیری از وقوع خطا با شرایط پیچیده در داخل کد، باید خطاها را مدیریت کرده و بهطور مناسب به آنها واکنش نشان دهیم.

در زیر، مثالهایی از رویکردهای مختلف آمده است که نشان میدهند چگونه میتوان خطاها را بهطور مؤثر مدیریت کرد.

// Try Catch With Throwing Errors // Dirty Code function processTransactions(transactions) { if (!transactions || transactions.length === 0) { showErrorMessage("No transactions provided!"); return; } // process logic here... } // Clean Code function processTransactions(transactions) { if (!transactions || transactions.length === 0) { const error = new Error("No transactions provided!"); throw error; } // ... continue processing } try { processTransactions(transactions); } catch (error) { showErrorMessage(error.message); }


// Error Guards // Dirty Code function showUserInfo(user) { if (!user) { return; } console.log(`User: ${user.name}`); } // Clean Code function showUserInfo(user) { if (!user || !user.name) { throw new Error("Invalid user object: 'name' is required"); } console.log(`User: ${user.name}`); } try { showUserInfo(user); } catch (err) { showToast(err.message); }


// Extracting Validation // Dirty Code function checkout(user, cart) { if (!user || !user.isLoggedIn) { return alert("Please log in first"); } if (!cart || cart.items.length === 0) { return alert("Your cart is empty"); } // continue with checkout... submitOrder(cart); } // Clean Code function validateCheckout(user, cart) { if (!user || !user.isLoggedIn) { throw new Error("Please log in first"); } if (!cart || cart.items.length === 0) { throw new Error("Your cart is empty"); } } function checkout(user, cart) { try { validateCheckout(user, cart); submitOrder(cart); } catch (error) { showToast(error.message); } }

اشیاء، کلاسها و ساختارهای داده

در برنامهنویسی، اشیاء و ساختارهای داده دو مفهوم اساسی هستند که تفاوتهای زیادی با یکدیگر دارند. نحوه استفاده صحیح از هرکدام از آنها میتواند تأثیر زیادی بر تمیزی و قابلفهم بودن کد داشته باشد.

اشیاء در برنامهنویسی شیءگرا بهگونهای طراحی شدهاند که جزئیات داخلی خود را پنهان کنند. به عبارت دیگر، این اشیاء دادههای خود را خصوصی نگه میدارند و تنها از طریق متدها یا API عمومی با دنیای خارج تعامل میکنند. اشیاء معمولاً مسئول انجام کارهای پیچیدهتری مانند منطق تجاری هستند.
ساختارهای داده (مانند آرایهها، دیکشنریها یا اشیاء ساده بدون متد) عمدتاً برای ذخیره و انتقال داده استفاده میشوند. معمولاً آنها متدی ندارند و هیچ رفتار خاصی انجام نمیدهند — فقط کانتینرهایی برای داده هستند.
زمانی که این دو مفهوم را ترکیب میکنیم و دادهها و رفتارها را بهطور همزمان ذخیره میکنیم، کد ما پیچیدهتر و شکنندهتر میشود. ساختار کد غیرشفاف میشود و نگهداری آن در آینده دشوار خواهد بود.

بنابراین، از اشیاء برای تعریف رفتارها و مسئولیتها استفاده کنید. از ساختارهای داده برای ذخیره اطلاعات ساده استفاده کنید.

ساختارها

پلیمورفیسم یکی از مفاهیم کلیدی در برنامهنویسی شیءگرا است که به ما اجازه میدهد تا رفتارهای مختلفی را برای اشیاء مختلف با استفاده از یک نام مشترک تعریف کنیم.

  • برای مثال، فرض کنید تابعی به نام draw() داریم. در حالی که این تابع رفتار خاصی برای یک دایره دارد، ممکن است برای یک مربع بهطور کاملاً متفاوت عمل کند—اما از همان نام draw() استفاده میشود.

زمانی که پلیمورفیسم بهدرستی استفاده میشود:

  • کد ما را تمیزتر و قابلفهمتر میکند.
  • نیاز به شرایط تکراری مانند if یا switch برای هر نوع شیء را از بین میبرد.
  • اضافه کردن موارد جدید را بدون تغییر در کد موجود آسانتر میکند.

بنابراین، پلیمورفیسم به این معنی است که "یک نام، چندین رفتار" — این ویژگی برنامهنویسی ما را سازماندهیشدهتر، مقیاسپذیرتر و نگهداری آن را آسانتر میکند.

در برنامهنویسی تمیز، یکی از اصول مهم این است که کلاسها و توابع باید همبستگی بالا داشته باشند. این به این معنی است که تمام متدهای یک کلاس باید مرتبط باشند و از ویژگیهای خود کلاس استفاده کنند.

زمانی که همبستگی بالا باشد، کلاسها هدف مشخصی دارند که باعث میشود کد قابلفهمتر، قابلاطمینانتر و قابلنگهداریتر باشد.

  • قانون دِمتر بیان میکند:
    "با دوستان خود صحبت کنید، نه با دوستان دوستان خود."
    در عمل، این بدین معناست که نباید بهطور مستقیم به ویژگیها یا متدهای داخلی اشیاء دیگر دسترسی پیدا کنیم. به جای استفاده مستقیم از چیزی مانند user.profile.avatar.url، بهتر است از متدی مانند user.getAvatarUrl() استفاده کنیم.
  • اگر ساختار داخلی profile یا avatar تغییر کند، در حالت اول باید بسیاری از قسمتهای کد را بهروزرسانی کنیم. در حالت دوم، تنها نیاز به بهروزرسانی متد getAvatarUrl() داریم.
    این امر وابستگیها را کاهش داده و نگهداری کد را آسانتر میکند.

بنابراین، همبستگی بالا به این معنی است که کلاسها باید تنها یک وظیفه خاص انجام دهند. قانون دِمتر به کاهش وابستگیها میان بخشهای مختلف کد کمک میکند.

اصل تک مسئولیتی بیان میکند که هر کلاس باید تنها یک مسئولیت داشته باشد. به عبارت دیگر، اگر یک کلاس بیشتر از یک کار انجام میدهد، باید تقسیم شود. این اصل باعث میشود کد قابلفهمتر و قابلنگهداریتر باشد.

علاوه بر این، اصل "برای توسعه باز، برای تغییر بسته" بیان میکند که کلاسها باید برای توسعه باز و برای تغییر بسته باشند. این اصول به ما کمک میکنند تا کدی بنویسیم که هم انعطافپذیر و هم تمیز باشد.

سخن پایانی

در این مقاله، یاد گرفتیم که چگونه توابع و متدهای تمیز بنویسیم. اما نوشتن کد تمیز فقط بخشی از داستان است. گام بعدی یادگیری درباره معماری تمیز است — فهمیدن اینکه هر بخش از کد کجا باید در یک پروژه قرار گیرد. این زمانی است که کد واقعاً شروع به معنی پیدا کردن میکند.

کد تمیزclean code
۰
۰
امیرمحمد ابوالحسنی
امیرمحمد ابوالحسنی
برنامه نویس فرانت‌اند
شاید از این پست‌ها خوشتان بیاید