<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://example.com/blog</id>
    <title>مالوردی Blog</title>
    <updated>2026-06-01T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://example.com/blog"/>
    <subtitle>مالوردی Blog</subtitle>
    <icon>https://example.com/img/favicon.svg</icon>
    <entry>
        <title type="html"><![CDATA[مسئول کیفیت نرم‌افزار چه کسی است؟]]></title>
        <id>https://example.com/blog/who-owns-software-quality</id>
        <link href="https://example.com/blog/who-owns-software-quality"/>
        <updated>2026-06-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[کیفیت نرم‌افزار مسئولیت یک نقش جدا نیست؛ نتیجه‌ی مالکیت روشن، بازخورد سریع و رابطه‌ی سالم بین نقش‌های تیم است.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="مسئول کیفیت نرم‌افزار چه کسی است؟" src="https://example.com/assets/images/who-owns-software-quality-hero-ff24df99b41fa3d96ea5ccaa0f76fc0c.png" width="1491" height="1055" class="img_m9Pm"></p>
<p>یک باگ به production می‌رود. کاربر ناراضی می‌شود، پشتیبانی درگیر می‌شود و تیم فنی دنبال علت می‌گردد. در چنین لحظه‌ای معمولاً اولین سؤال این است: «چرا QA این را نگرفت؟»</p>
<p>این سؤال از یک نگرانی واقعی می‌آید. کسی انتظار دارد قبل از رسیدن خطا به کاربر، یک جایی در مسیر جلوی آن گرفته شود. اما همین سؤال، اگر دقیق بررسی نشود، ما را به یک برداشت ساده‌انگارانه می‌رساند: اینکه کیفیت چیزی است که در انتهای مسیر، توسط یک نقش جدا، به محصول اضافه می‌شود.</p>
<p>به نظرم این برداشت ریشه‌ی خیلی از سوءتفاهم‌های تیم‌های نرم‌افزاری است. این متن علیه QA نیست و قرار نیست تخصص تست و کیفیت را بی‌ارزش کند. مسئله این است که QA نباید جایگزین مسئولیت تیم شود. کیفیت اگر در فهم مسئله، طراحی، کدنویسی، تست، انتشار و بازخورد ساخته نشده باشد، در آخر مسیر فقط دیرتر خراب بودنش را می‌فهمیم.</p>
<p>پس ایده‌ی اصلی این متن ساده است: کیفیت مسئولیت مشترک تیم است؛ اما مسئولیت مشترک نباید به بی‌مالکیتی تبدیل شود. هر نقش باید مالک کیفیت همان تصمیمی باشد که روی آن اثر می‌گذارد.</p>
<!-- -->
<p>کیفیت نرم‌افزار یک ایستگاه پایانی نیست. کیفیت، نتیجه‌ی زنجیره‌ای از تصمیم‌هاست: اینکه مسئله چقدر درست فهمیده شده، راه‌حل چقدر ساده طراحی شده، کد چقدر خواناست، تست‌ها چقدر قابل اعتمادند، انتشار چقدر امن است و تیم بعد از انتشار چقدر سریع بازخورد می‌گیرد.</p>
<p>پس پاسخ کوتاه این است: مسئول کیفیت، کل تیم است.</p>
<p>اما همین پاسخ هم اگر دقیق نشود، خطرناک است. چون «همه مسئول‌اند» گاهی در عمل یعنی «هیچ‌کس مسئول نیست». کیفیت باید مسئولیت مشترک باشد، اما مالکیت آن در هر نقطه باید روشن بماند.</p>
<p><img decoding="async" loading="lazy" alt="نقشه‌ی مسئولیت‌ها در کیفیت نرم‌افزار" src="https://example.com/assets/images/software-quality-responsibility-map-cdf41211c1cacbef79210d017db2023c.png" width="1491" height="1055" class="img_m9Pm"></p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="کیفیت-یعنی-چه">کیفیت یعنی چه؟<a href="https://example.com/blog/who-owns-software-quality#%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به کیفیت یعنی چه؟" title="لینک مستقیم به کیفیت یعنی چه؟" translate="no">​</a></h2>
<p>قبل از اینکه بپرسیم چه کسی مسئول کیفیت است، باید روشن کنیم کیفیت یعنی چه.</p>
<p>کیفیت فقط نبودن باگ نیست. نرم‌افزاری ممکن است بدون خطای فنی جدی کار کند، اما همچنان بی‌کیفیت باشد؛ چون مسئله‌ی اشتباهی را حل می‌کند، تجربه‌ی کاربر بدی دارد، کند است، تغییر دادنش سخت است، خطاهایش دیده نمی‌شود یا بعد از انتشار به‌سختی قابل نگهداشت است.</p>
<p>از این زاویه، کیفیت چند لایه دارد:</p>
<ul>
<li class="">کیفیت محصولی: آیا مسئله‌ی درستی را حل کرده‌ایم؟</li>
<li class="">کیفیت طراحی: آیا راه‌حل ساده، قابل فهم و قابل تغییر است؟</li>
<li class="">کیفیت فنی: آیا کد خوانا، تست‌پذیر و قابل نگهداشت است؟</li>
<li class="">کیفیت عملیاتی: آیا سیستم در اجرا قابل اتکا، قابل مشاهده و قابل بازگشت است؟</li>
<li class="">کیفیت انسانی: آیا تیم می‌تواند درباره‌ی ریسک‌ها صادقانه حرف بزند؟</li>
</ul>
<p>وقتی کیفیت را فقط به «تست شدن» تقلیل می‌دهیم، بخش بزرگی از واقعیت را از دست می‌دهیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="سوءتفاهم-رایج-qa-آخر-مسیر-کیفیت-را-تضمین-میکند">سوءتفاهم رایج: QA آخر مسیر کیفیت را تضمین می‌کند<a href="https://example.com/blog/who-owns-software-quality#%D8%B3%D9%88%D8%A1%D8%AA%D9%81%D8%A7%D9%87%D9%85-%D8%B1%D8%A7%DB%8C%D8%AC-qa-%D8%A2%D8%AE%D8%B1-%D9%85%D8%B3%DB%8C%D8%B1-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%D8%B1%D8%A7-%D8%AA%D8%B6%D9%85%DB%8C%D9%86-%D9%85%DB%8C%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به سوءتفاهم رایج: QA آخر مسیر کیفیت را تضمین می‌کند" title="لینک مستقیم به سوءتفاهم رایج: QA آخر مسیر کیفیت را تضمین می‌کند" translate="no">​</a></h2>
<p>در خیلی از تیم‌ها، مسیر کار شبیه این است: محصول تعریف می‌شود، توسعه‌دهنده پیاده‌سازی می‌کند، بعد QA تست می‌کند و در نهایت محصول منتشر می‌شود.</p>
<p>این مدل در ظاهر مرتب است. نقش‌ها جدا هستند و هرکس ظاهراً کار خودش را دارد. اما مشکل اصلی اینجاست که کیفیت از محل تولید جدا می‌شود.</p>
<p>ابهام نیازمندی، طراحی پیچیده، کد شکننده، تست‌ناپذیری، خطای مدیریت وضعیت، نبودن مشاهده‌پذیری و تصمیم‌های عجولانه معمولاً همان‌جایی ساخته می‌شوند که محصول و کد ساخته می‌شوند؛ نه در مرحله‌ی تست نهایی.</p>
<p>QA می‌تواند بخشی از این مشکلات را کشف کند، اما نمی‌تواند کیفیت را بعد از تولید به محصول تزریق کند. اگر کیفیت در طراحی و توسعه ساخته نشده باشد، QA فقط می‌تواند دیرتر خبر بد را به تیم بدهد.</p>
<p>این تفاوت مهمی است: کشف خطا با ساختن کیفیت یکی نیست.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-فنی-توسعهدهنده-نزدیکترین-فرد-به-کیفیت-است">نگاه فنی: توسعه‌دهنده نزدیک‌ترین فرد به کیفیت است<a href="https://example.com/blog/who-owns-software-quality#%D9%86%DA%AF%D8%A7%D9%87-%D9%81%D9%86%DB%8C-%D8%AA%D9%88%D8%B3%D8%B9%D9%87%D8%AF%D9%87%D9%86%D8%AF%D9%87-%D9%86%D8%B2%D8%AF%DB%8C%DA%A9%D8%AA%D8%B1%DB%8C%D9%86-%D9%81%D8%B1%D8%AF-%D8%A8%D9%87-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به نگاه فنی: توسعه‌دهنده نزدیک‌ترین فرد به کیفیت است" title="لینک مستقیم به نگاه فنی: توسعه‌دهنده نزدیک‌ترین فرد به کیفیت است" translate="no">​</a></h2>
<p>از زاویه‌ی فنی، توسعه‌دهنده نزدیک‌ترین فرد به کیفیت است. کسی که کد را می‌نویسد، بهتر از هرکس می‌داند تصمیم‌های فنی کجا گرفته شده‌اند، پیچیدگی کجا پنهان شده، چه چیزی تست‌پذیر نیست و کدام بخش در آینده شکننده خواهد شد.</p>
<p>کدی که خوانا نیست، با تست نهایی خوانا نمی‌شود. طراحی‌ای که بی‌دلیل پیچیده است، با QA ساده نمی‌شود. خطایی که درست مدیریت نشده، با چند سناریوی دستی قابل اتکا نمی‌شود. نبودن لاگ، متریک و مشاهده‌پذیری هم چیزی نیست که آخر مسیر با یک چک‌لیست حل شود.</p>
<p>به همین دلیل، تست واحد، بازبینی کد، طراحی ساده، مدیریت خطا، مشاهده‌پذیری و بازخورد سریع بخشی از کار توسعه‌اند، نه کارهای اضافه و تجملی.</p>
<p>در کتاب <em>Software Engineering at Google</em> روی همین ایده تأکید می‌شود که تست‌های کوچک و سریع فقط برای پیدا کردن باگ نیستند؛ برای افزایش بهره‌وری مهندس‌اند. چون توسعه‌دهنده می‌تواند مدام بازخورد بگیرد، با اطمینان تغییر بدهد و خطا را نزدیک به همان نقطه‌ای پیدا کند که ساخته شده است.</p>
<p>هرچه بازخورد به کد نزدیک‌تر باشد، اصلاح ارزان‌تر است. هرچه بازخورد دیرتر برسد، هزینه‌ی فهمیدن، بازسازی ذهنی و اصلاح بیشتر می‌شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-تست-تست-بیشتر-همیشه-کیفیت-بیشتر-نیست">نگاه تست: تست بیشتر همیشه کیفیت بیشتر نیست<a href="https://example.com/blog/who-owns-software-quality#%D9%86%DA%AF%D8%A7%D9%87-%D8%AA%D8%B3%D8%AA-%D8%AA%D8%B3%D8%AA-%D8%A8%DB%8C%D8%B4%D8%AA%D8%B1-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%D8%A8%DB%8C%D8%B4%D8%AA%D8%B1-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به نگاه تست: تست بیشتر همیشه کیفیت بیشتر نیست" title="لینک مستقیم به نگاه تست: تست بیشتر همیشه کیفیت بیشتر نیست" translate="no">​</a></h2>
<p>یکی از خطاهای رایج این است که فکر کنیم اگر تست‌های انتهایی بیشتری داشته باشیم، کیفیت بهتر می‌شود.</p>
<p>تست‌های سراسری لازم‌اند. بالاخره باید بفهمیم کاربر در مسیر واقعی با چه چیزی روبه‌رو می‌شود. اما اگر ستون اصلی اطمینان تیم، تست‌های انتهابه‌انتها باشد، معمولاً با سیستمی کند، شکننده و سخت‌عیب‌یاب روبه‌رو می‌شویم.</p>
<p>وقتی یک تست سراسری می‌شکند، همیشه روشن نیست مشکل از کجاست: رابط کاربری، شبکه، داده، سرویس پشتی، زمان‌بندی، وابستگی بیرونی یا خود تست؟ این نوع تست‌ها برای پوشش دادن رفتار کل سیستم مفیدند، اما برای پیدا کردن سریع ریشه‌ی خطا همیشه ابزار خوبی نیستند.</p>
<p>ایده‌ی هرم تست دقیقاً همین را توضیح می‌دهد: بیشتر تست‌ها باید کوچک، سریع و نزدیک به کد باشند؛ بخشی از تست‌ها یکپارچگی اجزای مهم را بررسی کنند؛ و تعداد محدودتری تست، کل مسیر سیستم را از ابتدا تا انتها بسنجد.</p>
<p>پس هدف، زیاد کردن کورکورانه‌ی تست نیست. هدف، ساختن چرخه‌ی بازخورد درست است.</p>
<p><img decoding="async" loading="lazy" alt="هرم تست" src="https://example.com/assets/images/test-pyramid-50150506f5488dc65b93522efb2ace20.png" width="1491" height="1055" class="img_m9Pm"></p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-qa-پلیس-کیفیت-یا-کاتالیزور-کیفیت">نگاه QA: پلیس کیفیت یا کاتالیزور کیفیت؟<a href="https://example.com/blog/who-owns-software-quality#%D9%86%DA%AF%D8%A7%D9%87-qa-%D9%BE%D9%84%DB%8C%D8%B3-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%DB%8C%D8%A7-%DA%A9%D8%A7%D8%AA%D8%A7%D9%84%DB%8C%D8%B2%D9%88%D8%B1-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA" class="hash-link" aria-label="لینک مستقیم به نگاه QA: پلیس کیفیت یا کاتالیزور کیفیت؟" title="لینک مستقیم به نگاه QA: پلیس کیفیت یا کاتالیزور کیفیت؟" translate="no">​</a></h2>
<p>نقد مدل سنتی QA به معنی بی‌ارزش دانستن تخصص کیفیت نیست. برعکس، هرچه تیم کیفیت را جدی‌تر بگیرد، به نگاه انتقادی، سناریوسازی و کشف ریسک بیشتر نیاز دارد. مشکل از جایی شروع می‌شود که QA به جای توانمند کردن تیم، به ایستگاه آخر تأیید تبدیل می‌شود.</p>
<p>در چنین مدلی توسعه‌دهنده ممکن است فکر کند «اگر مشکلی بود QA می‌گیرد». مدیر محصول ممکن است فکر کند «اگر نیازمندی مبهم بود، در تست معلوم می‌شود». مدیر تیم هم ممکن است خیال کند چون یک مرحله‌ی کنترل وجود دارد، پس کیفیت مدیریت شده است.</p>
<p>اما نقش سالم QA چیز دیگری است.</p>
<p>QA می‌تواند کاتالیزور کیفیت باشد؛ یعنی کسی که کمک می‌کند تیم ریسک‌ها را زودتر ببیند، سناریوهای مهم را بهتر طراحی کند، نقاط کور محصول را پیدا کند و چرخه‌ی بازخورد را بهتر کند.</p>
<p>QA خوب فقط دنبال باگ نمی‌گردد. QA خوب سؤال‌های سخت می‌پرسد: اگر کاربر این مسیر را نیمه‌کاره رها کند چه می‌شود؟ اگر سرویس بیرونی کند شود چه می‌شود؟ اگر داده‌ی قدیمی برسد چه می‌شود؟ اگر نیازمندی را اشتباه فهمیده باشیم چه؟ اگر این تصمیم در مقیاس بزرگ تکرار شود چه هزینه‌ای دارد؟</p>
<p>این نگاه، QA را از نقش «تأییدکننده‌ی آخر مسیر» به نقش «کمک‌کننده به تفکر کیفیت‌محور» تبدیل می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-محصول-کیفیت-فقط-مسئلهی-فنی-نیست">نگاه محصول: کیفیت فقط مسئله‌ی فنی نیست<a href="https://example.com/blog/who-owns-software-quality#%D9%86%DA%AF%D8%A7%D9%87-%D9%85%D8%AD%D8%B5%D9%88%D9%84-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%D9%81%D9%82%D8%B7-%D9%85%D8%B3%D8%A6%D9%84%D9%87%DB%8C-%D9%81%D9%86%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به نگاه محصول: کیفیت فقط مسئله‌ی فنی نیست" title="لینک مستقیم به نگاه محصول: کیفیت فقط مسئله‌ی فنی نیست" translate="no">​</a></h2>
<p>بخشی از کیفیت قبل از نوشتن اولین خط کد ساخته یا خراب می‌شود.</p>
<p>اگر مسئله درست فهمیده نشده باشد، اگر نیازمندی مبهم باشد، اگر حالت‌های مرزی دیده نشده باشند، اگر اولویت‌ها نادرست باشند یا اگر معیار آماده بودن روشن نباشد، خروجی نهایی حتی با کد خوب هم می‌تواند بی‌کیفیت باشد.</p>
<p>مثلاً فرض کنیم در یک فرایند خرید، رفتار سیستم هنگام لغو سفارش بعد از اعمال تخفیف روشن نشده باشد. اگر بعداً کاربر پول اشتباه ببیند یا پشتیبانی درگیر اصلاح دستی شود، شاید ظاهر ماجرا شبیه باگ فنی باشد؛ اما ریشه‌ی آن در تصمیم محصولی مبهم است. تست می‌تواند این ابهام را پیدا کند، اما نمی‌تواند جای تعریف درست رفتار محصول را بگیرد.</p>
<p>اینجاست که نقش مدیر محصول و طراح جدی می‌شود. کیفیت محصولی یعنی تیم بداند دقیقاً چه مسئله‌ای را حل می‌کند، برای چه کسی، با چه سطحی از ریسک، و با چه تعریفی از موفقیت.</p>
<p>گاهی چیزی که در پایان به شکل باگ دیده می‌شود، در واقع ریشه‌ی محصولی دارد. در این حالت، سرزنش توسعه‌دهنده یا QA مسئله را حل نمی‌کند. مشکل از جایی شروع شده که تصمیم محصولی شفاف نبوده است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-عملیات-کیفیت-بعد-از-merge-تمام-نمیشود">نگاه عملیات: کیفیت بعد از merge تمام نمی‌شود<a href="https://example.com/blog/who-owns-software-quality#%D9%86%DA%AF%D8%A7%D9%87-%D8%B9%D9%85%D9%84%DB%8C%D8%A7%D8%AA-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%D8%A8%D8%B9%D8%AF-%D8%A7%D8%B2-merge-%D8%AA%D9%85%D8%A7%D9%85-%D9%86%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به نگاه عملیات: کیفیت بعد از merge تمام نمی‌شود" title="لینک مستقیم به نگاه عملیات: کیفیت بعد از merge تمام نمی‌شود" translate="no">​</a></h2>
<p>کیفیت با merge شدن کد تمام نمی‌شود. بخشی از کیفیت تازه بعد از انتشار دیده می‌شود.</p>
<p>آیا سیستم قابل مشاهده است؟ آیا می‌توانیم بفهمیم چه چیزی خراب شده؟ آیا انتشار مرحله‌ای داریم؟ آیا برگشت به نسخه‌ی قبلی آسان است؟ آیا هشدارها معنادارند؟ آیا خطاها به تیمی می‌رسند که می‌تواند آن‌ها را اصلاح کند؟</p>
<p>فرض کنیم کدی که منتشر شده از نظر منطقی درست است، اما بعد از انتشار کندی شدیدی در یک مسیر کاربر ایجاد می‌کند. اگر متریک، لاگ، هشدار و مسیر برگشت امن نداشته باشیم، تیم دیر می‌فهمد، دیر واکنش نشان می‌دهد و کاربر تجربه‌ی بدی می‌گیرد. در این وضعیت، کیفیت فقط به درست بودن کد وابسته نیست؛ به توانایی تیم برای دیدن، فهمیدن و کنترل رفتار سیستم در محیط واقعی هم وابسته است.</p>
<p>یکپارچه‌سازی پیوسته و تحویل پیوسته برای همین مهم‌اند. تغییرات کوچک‌تر، ادغام سریع‌تر، ساخت خودکار و تست خودکار باعث می‌شوند خطاها زودتر دیده شوند و تیم به جای انباشتن ریسک، آن را در جریان کار مدیریت کند.</p>
<p>کیفیت عملیاتی یعنی تیم فقط مسئول ساختن قابلیت نیست؛ مسئول سالم رساندن آن به کاربر و یاد گرفتن از رفتار واقعی سیستم هم هست.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-مدیریتی-کیفیت-بدون-هزینه-ساخته-نمیشود">نگاه مدیریتی: کیفیت بدون هزینه ساخته نمی‌شود<a href="https://example.com/blog/who-owns-software-quality#%D9%86%DA%AF%D8%A7%D9%87-%D9%85%D8%AF%DB%8C%D8%B1%DB%8C%D8%AA%DB%8C-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%D8%A8%D8%AF%D9%88%D9%86-%D9%87%D8%B2%DB%8C%D9%86%D9%87-%D8%B3%D8%A7%D8%AE%D8%AA%D9%87-%D9%86%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به نگاه مدیریتی: کیفیت بدون هزینه ساخته نمی‌شود" title="لینک مستقیم به نگاه مدیریتی: کیفیت بدون هزینه ساخته نمی‌شود" translate="no">​</a></h2>
<p>هیچ تیمی فقط با شعار، مالک کیفیت نمی‌شود.</p>
<p>اگر همیشه سرعت ظاهری مهم‌تر از درستی باشد، کیفیت قربانی می‌شود. اگر برای تست، بازبینی، ساده‌سازی، بازپرداخت بدهی فنی و بهبود ابزار زمان وجود نداشته باشد، کیفیت به حرفی زیبا در جلسه‌ها تبدیل می‌شود.</p>
<p>مدیر تیم مسئول ساختن سیستمی است که در آن کیفیت امکان‌پذیر باشد. یعنی زمان، ابزار، اختیار و معیارهای درست فراهم باشد.</p>
<p>معیارها هم رفتار می‌سازند. اگر فقط تعداد قابلیت‌های تحویلی، سرعت بستن تسک یا تاریخ تحویل را بسنجیم، تیم به سمت تحویل سریع‌تر و سطحی‌تر هل داده می‌شود. اما اگر کیفیت بازبینی، نرخ خطا بعد از انتشار، زمان کشف خطا، زمان بازیابی و میزان بازپرداخت بدهی فنی هم دیده شود، پیام مدیریتی فرق می‌کند: کیفیت بخشی از کار است، نه چیزی که بعداً اگر وقت شد به آن برسیم.</p>
<p>اگر تیم برای گفتن «این آماده نیست» امنیت نداشته باشد، دیر یا زود یاد می‌گیرد ریسک را پنهان کند. اگر خطاها با سرزنش پاسخ داده شوند، تیم به جای یادگیری، دفاعی می‌شود.</p>
<p>پس کیفیت فقط مسئولیت افراد نیست؛ محصول سیستم کاری تیم هم هست.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-انسانی-کیفیت-با-اعتماد-ساخته-میشود">نگاه انسانی: کیفیت با اعتماد ساخته می‌شود<a href="https://example.com/blog/who-owns-software-quality#%D9%86%DA%AF%D8%A7%D9%87-%D8%A7%D9%86%D8%B3%D8%A7%D9%86%DB%8C-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%D8%A8%D8%A7-%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF-%D8%B3%D8%A7%D8%AE%D8%AA%D9%87-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به نگاه انسانی: کیفیت با اعتماد ساخته می‌شود" title="لینک مستقیم به نگاه انسانی: کیفیت با اعتماد ساخته می‌شود" translate="no">​</a></h2>
<p>بحث «مسئول کیفیت کیست؟» فقط بحث فرایند و نقش سازمانی نیست؛ بحث ترس، اعتماد، مالکیت و سرزنش هم هست.</p>
<p>در بسیاری از تیم‌ها، QA به‌مرور تبدیل می‌شود به سپر روانی تیم. توسعه‌دهنده ناخودآگاه فکر می‌کند «اگر مشکلی بود QA می‌گیرد». مدیر محصول خیال می‌کند «اگر نیازمندی مبهم بود، در تست معلوم می‌شود». مدیر تیم هم احساس می‌کند یک مرحله‌ی کنترل آخر مسیر دارد.</p>
<p>این مدل شاید اضطراب تیم را کم کند، اما کیفیت را واقعاً بهتر نمی‌کند. فقط نگرانی را از یک نقش به نقش دیگر منتقل می‌کند.</p>
<p>از طرف دیگر، گفتن اینکه «QA لازم نیست و همه مسئول کیفیت‌اند» هم اگر بدون دقت گفته شود، می‌تواند بی‌رحمانه باشد. تیمی که زمان، ابزار، مهارت و امنیت روانی کافی ندارد، با شعار مالک کیفیت نمی‌شود.</p>
<p>وقتی ضرب‌الاجل سنگین است و فرهنگ تیم سرزنش‌گر است، آدم‌ها کیفیت را قربانی زنده‌ماندن می‌کنند.</p>
<p>کیفیت وقتی ساخته می‌شود که اعضای تیم اجازه داشته باشند درباره‌ی ریسک حرف بزنند، تصمیم‌های عجولانه را نقد کنند، بگویند «این هنوز آماده نیست» و بابت پیدا کردن مشکل تنبیه نشوند.</p>
<p>توسعه‌دهنده بدون زمان و ابزار، مالک کیفیت نمی‌شود. QA بدون احترام، فقط به باگ‌یاب خسته تبدیل می‌شود. مدیر بدون پذیرش هزینه‌ی کیفیت، فقط شعار کیفیت می‌دهد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پس-بالاخره-مسئول-کیفیت-کیست">پس بالاخره مسئول کیفیت کیست؟<a href="https://example.com/blog/who-owns-software-quality#%D9%BE%D8%B3-%D8%A8%D8%A7%D9%84%D8%A7%D8%AE%D8%B1%D9%87-%D9%85%D8%B3%D8%A6%D9%88%D9%84-%DA%A9%DB%8C%D9%81%DB%8C%D8%AA-%DA%A9%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به پس بالاخره مسئول کیفیت کیست؟" title="لینک مستقیم به پس بالاخره مسئول کیفیت کیست؟" translate="no">​</a></h2>
<p>مسئول کیفیت همه هستند، اما نه به معنای بی‌مالکیتی.</p>
<p>هرکس در نقطه‌ای که تصمیم می‌گیرد و اثر می‌گذارد، مسئول کیفیت همان نقطه است.</p>
<p>مدیر محصول مسئول کیفیت فهم مسئله، اولویت‌ها و تعریف درست آماده بودن است.</p>
<p>توسعه‌دهنده مسئول کیفیت فنی راه‌حل، کد، تست‌پذیری، مدیریت خطا و نگهداشت‌پذیری است.</p>
<p>QA مسئول کمک به کشف ریسک، طراحی سناریوهای مهم و بهتر کردن چرخه‌ی بازخورد است.</p>
<p>عملیات و تیم‌های زیرساخت مسئول قابل اتکا بودن مسیر اجرا، انتشار، مشاهده‌پذیری و بازگشت امن‌اند.</p>
<p>مدیر تیم مسئول ساختن محیطی است که در آن کیفیت فقط شعار نباشد و آدم‌ها برای درست کار کردن، زمان و امنیت داشته باشند.</p>
<p>کیفیت را نقش‌ها به‌تنهایی نمی‌سازند. رابطه‌ی سالم بین نقش‌ها، بازخورد سریع و مالکیت روشن است که کیفیت می‌سازد.</p>
<p><img decoding="async" loading="lazy" alt="چه کسی مسئول چیست؟" src="https://example.com/assets/images/who-owns-what-in-quality-c025f49176b2426d8dbcedd6ecc364c8.png" width="1491" height="1055" class="img_m9Pm"></p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/who-owns-software-quality#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>QA نمی‌تواند کیفیت را بعد از تولید به محصول تزریق کند. تست نهایی نمی‌تواند جای طراحی خوب، کد خوانا، نیازمندی شفاف، انتشار امن و فرهنگ سالم را بگیرد.</p>
<p>کیفیت از همان لحظه‌ای ساخته می‌شود که مسئله تعریف می‌شود، تصمیم گرفته می‌شود، کد نوشته می‌شود، تست طراحی می‌شود و تغییر به دست کاربر می‌رسد.</p>
<p>پس پاسخ دقیق‌تر این است:</p>
<p>کیفیت وقتی ساخته می‌شود که هر نقش، مسئول همان تصمیمی باشد که روی کیفیت اثر می‌گذارد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="برای-مطالعهی-بیشتر">برای مطالعه‌ی بیشتر<a href="https://example.com/blog/who-owns-software-quality#%D8%A8%D8%B1%D8%A7%DB%8C-%D9%85%D8%B7%D8%A7%D9%84%D8%B9%D9%87%DB%8C-%D8%A8%DB%8C%D8%B4%D8%AA%D8%B1" class="hash-link" aria-label="لینک مستقیم به برای مطالعه‌ی بیشتر" title="لینک مستقیم به برای مطالعه‌ی بیشتر" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://testing.googleblog.com/2011/01/how-google-tests-software.html" target="_blank" rel="noopener noreferrer" class="">How Google Tests Software - Part One</a></li>
<li class=""><a href="https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html" target="_blank" rel="noopener noreferrer" class="">Just Say No to More End-to-End Tests</a></li>
<li class=""><a href="https://abseil.io/resources/swe-book/html/ch12.html" target="_blank" rel="noopener noreferrer" class="">Software Engineering at Google: Unit Testing</a></li>
<li class=""><a href="https://martinfowler.com/articles/practical-test-pyramid.html" target="_blank" rel="noopener noreferrer" class="">Practical Test Pyramid</a></li>
<li class=""><a href="https://martinfowler.com/articles/continuousIntegration.html" target="_blank" rel="noopener noreferrer" class="">Continuous Integration</a></li>
<li class=""><a href="https://dora.dev/capabilities/test-automation/" target="_blank" rel="noopener noreferrer" class="">DORA: Test Automation</a></li>
<li class=""><a href="https://www.thoughtworks.com/en-br/insights/blog/testing/qa-catalyst-quality-first-software" target="_blank" rel="noopener noreferrer" class="">QA as a catalyst for quality-first software development</a></li>
</ul>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="Software Engineering" term="Software Engineering"/>
        <category label="quality" term="quality"/>
        <category label="qa" term="qa"/>
        <category label="testing" term="testing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[مستند بی‌صاحب از بی‌مستندی بدتر است]]></title>
        <id>https://example.com/blog/technical-team-memory-documentation</id>
        <link href="https://example.com/blog/technical-team-memory-documentation"/>
        <updated>2026-05-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[چرا مستند فنی اگر جای درست، مسئول مشخص و فرایند نگهداشت نداشته باشد، به‌جای کمک به تیم می‌تواند گمراه‌کننده شود.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="حافظه‌ی فنی تیم" src="https://example.com/assets/images/technical-team-memory-cover-a533413ff240a10b5d2ca5286c06ba87.png" width="1672" height="941" class="img_m9Pm"></p>
<p>حرف اصلی این متن ساده است: آن بخش از حافظه‌ی فنی که همراه کد تغییر می‌کند، وقتی قابل اعتماد می‌ماند که در همان مسیری دیده شود که تغییر کد دیده می‌شود؛ کنار کامیت، بازبینی کد، یا سند نسخه‌دار داخل مخزن. باقی مستندها هم باید مسئول مشخص و مخاطب روشن داشته باشند. اگر این شرط‌ها نباشند، مستند به‌جای اینکه فهم سیستم را آسان کند، خودش گیج‌کننده می‌شود.</p>
<p>منظور از مستند بی‌صاحب، سندی است که معلوم نیست چه کسی مسئول درستی امروز آن است، از چه مسیری باید اصلاح شود، و آیا هنوز می‌شود برای تصمیم گرفتن به آن تکیه کرد یا نه.</p>
<p>پس بحث اصلی این نیست که مستند کجا راحت‌تر نوشته می‌شود؛ بحث این است که کجا درست‌تر می‌ماند. سندی که سریع نوشته می‌شود اما همراه تغییرهای واقعی دیده نمی‌شود، دیر یا زود از واقعیت عقب می‌افتد.</p>
<p>در بسیاری از تیم‌ها، بخشی از دانش فنی در جای نامناسب زندگی می‌کند: دلیل یک تنظیم در ذهن یک نفر است، رفتار حساس یک سرویس در کد هست اما توضیحش نیست، و تصمیم‌های قدیمی در صفحه‌هایی مانده‌اند که معلوم نیست هنوز معتبرند یا نه. مسئله فقط کمبود مستند نیست؛ مسئله این است که حافظه‌ی تیم پراکنده، بی‌مسئول و جدا از کار روزمره‌ی توسعه است.</p>
<p>این نوشته از همین نقطه شروع می‌کند: کد چه چیزی را می‌گوید، مستند چه چیزی را باید بگوید، و هر نوع دانش فنی کجا باید بماند تا با تغییر سیستم از واقعیت عقب نیفتد.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="حافظهی-شفاهی-با-رشد-تیم-دوام-نمیآورد">حافظه‌ی شفاهی با رشد تیم دوام نمی‌آورد<a href="https://example.com/blog/technical-team-memory-documentation#%D8%AD%D8%A7%D9%81%D8%B8%D9%87%DB%8C-%D8%B4%D9%81%D8%A7%D9%87%DB%8C-%D8%A8%D8%A7-%D8%B1%D8%B4%D8%AF-%D8%AA%DB%8C%D9%85-%D8%AF%D9%88%D8%A7%D9%85-%D9%86%D9%85%DB%8C%D8%A2%D9%88%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به حافظه‌ی شفاهی با رشد تیم دوام نمی‌آورد" title="لینک مستقیم به حافظه‌ی شفاهی با رشد تیم دوام نمی‌آورد" translate="no">​</a></h2>
<p>در تیم کوچک، پرسیدن از آدم‌ها سریع‌تر از خواندن سند است. یکی می‌داند سرویس چطور بالا می‌آید، یکی دلیل یک رفتار عجیب را یادش هست، یکی می‌داند کدام تنظیم حساس است. این مدل تا وقتی کار می‌کند که تیم کوچک، پایدار و نزدیک باشد.</p>
<p>اما با رشد تیم، همین الگو تبدیل به ریسک می‌شود. نفر جدید برای یک تغییر ساده باید چند نفر را پیدا کند. کسی که دلیل تصمیم را می‌دانست جابه‌جا شده. کد تغییر کرده، اما توضیح تصمیم نه. نتیجه این است که تغییرهای آینده نه بر پایه‌ی فهم مشترک، بلکه بر پایه‌ی حدس انجام می‌شوند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>دانشی که فقط در ذهن آدم‌هاست، مستند نیست؛ وابستگی عملیاتی است.</p></div></div>
<p>مثال ساده‌اش تنظیمی است که همه می‌دانند نباید تغییر کند، اما هیچ‌جا نوشته نشده چرا. چند ماه بعد، یک نفر با نیت ساده‌سازی آن مقدار را عوض می‌کند و تازه معلوم می‌شود آن تنظیم عجیب سپر یک سازگاری قدیمی بوده است. مشکل از آدمی که تغییر داده نیست؛ مشکل از حافظه‌ای است که به او منتقل نشده است.</p>
<p>پس مسئله فقط نوشتن چند صفحه‌ی بیشتر نیست؛ مسئله تبدیل دانش فردی به حافظه‌ای است که تیم بتواند به آن تکیه کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="معیار-مستند-خوب-کاهش-هزینهی-فهم-و-تغییر">معیار مستند خوب: کاهش هزینه‌ی فهم و تغییر<a href="https://example.com/blog/technical-team-memory-documentation#%D9%85%D8%B9%DB%8C%D8%A7%D8%B1-%D9%85%D8%B3%D8%AA%D9%86%D8%AF-%D8%AE%D9%88%D8%A8-%DA%A9%D8%A7%D9%87%D8%B4-%D9%87%D8%B2%DB%8C%D9%86%D9%87%DB%8C-%D9%81%D9%87%D9%85-%D9%88-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1" class="hash-link" aria-label="لینک مستقیم به معیار مستند خوب: کاهش هزینه‌ی فهم و تغییر" title="لینک مستقیم به معیار مستند خوب: کاهش هزینه‌ی فهم و تغییر" translate="no">​</a></h2>
<p>مستند فنی برای رسمی‌تر شدن پروژه نیست. معیار خوب بودن آن این است که هزینه‌ی فهم، استفاده، تغییر و نگهداشت سیستم را کم کند. اگر مستند این هزینه را کم نکند، حتی اگر کامل و مرتب به نظر برسد، ارزش مهندسی کمی دارد.</p>
<p>از این زاویه، مستند خوب چهار کار می‌کند: قراردادها را روشن می‌کند، دلیل تصمیم‌ها را نگه می‌دارد، مسیر شروع را کوتاه می‌کند، و خطاهای تکراری را کم می‌کند. سود آن هم معمولاً بعداً دیده می‌شود؛ نویسنده امروز وقت می‌گذارد، اما خواننده‌ی آینده از حدس و پرس‌وجوی دوباره بی‌نیاز می‌شود.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>اصل مهم</div><div class="admonitionContent_hiHs"><p>مستند خوب سرمایه‌گذاری روی تغییرهای آینده است، نه کاری برای قشنگ‌تر کردن ظاهر پروژه‌ی امروز.</p></div></div>
<p>این سود فقط برای خواننده نیست. نوشتن مستند، خود طراحی را هم آزمون می‌کند. اگر نتوانیم قرارداد یک تابع، رفتار یک سرویس یا دلیل یک تصمیم را روشن توضیح دهیم، شاید مشکل فقط در بیان نیست؛ شاید خود طراحی هنوز مبهم است.</p>
<p>مسئول محتوای فنی باید تیم مهندسی باشد؛ چون زمینه‌ی تصمیم، محدودیت‌های پیاده‌سازی و خطرهای تغییر را بهتر از هر نقش بیرونی می‌شناسد. البته این به معنای نثر شلوغ و سلیقه‌ای نیست، و کمک گرفتن از نویسنده‌ی فنی یا ویراستار می‌تواند کیفیت متن را بهتر کند. مستند هم مثل کد به سبک مشترک نیاز دارد: زبان یکدست، قالب ثابت، واژه‌های روشن، و متن کوتاهی که بتوان سریع مرورش کرد. کامل بودن سند کافی نیست؛ سندی که پیدا کردن جواب را سخت کند، هنوز کارش را خوب انجام نداده است.</p>
<p>پس مستند خوب فقط «درست» نیست؛ باید پیدا شود، سریع خوانده شود، و خواننده را به تصمیم درست برساند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-مستندها-از-واقعیت-عقب-میافتند">چرا مستندها از واقعیت عقب می‌افتند؟<a href="https://example.com/blog/technical-team-memory-documentation#%DA%86%D8%B1%D8%A7-%D9%85%D8%B3%D8%AA%D9%86%D8%AF%D9%87%D8%A7-%D8%A7%D8%B2-%D9%88%D8%A7%D9%82%D8%B9%DB%8C%D8%AA-%D8%B9%D9%82%D8%A8-%D9%85%DB%8C%D8%A7%D9%81%D8%AA%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به چرا مستندها از واقعیت عقب می‌افتند؟" title="لینک مستقیم به چرا مستندها از واقعیت عقب می‌افتند؟" translate="no">​</a></h2>
<p>مستند معمولاً به دو دلیل خراب می‌شود: یا از جایی که تغییر رخ می‌دهد دور است، یا مسئول و مسیر بازبینی ندارد. در هر دو حالت، سند یک روز درست بوده، اما تضمینی برای درست ماندنش وجود ندارد.</p>
<p>ویکی و ابزارهایی مثل Confluence در شروع کار جذاب‌اند: نوشتن در آن‌ها سریع است، لینک دادن آسان است و همه می‌توانند چیزی اضافه کنند. این مزیت مهمی است. ویکی برای دانش بین‌تیمی، تصمیم‌های سازمانی، پرسش‌های پرتکرار، مسیرهای شروع و لینک دادن به سندهای معتبر جای خوبی است.</p>
<p>اما ویکی بهتر است نقشه‌ی راه و فهرست منابع قابل اتکا باشد، نه اینکه خودش همه‌ی آن منابع را دوباره بازنویسی کند. مثلاً صفحه‌ی ویکی می‌تواند بگوید راهنمای اجرای سرویس کجاست، سند طراحی کدام است، و قرارداد API از کجا تولید می‌شود. اگر خودش همان دستورهای اجرا و همان قراردادها را جداگانه نگه دارد، از همان لحظه خطر دو مرجع ناسازگار شروع می‌شود.</p>
<p>مشکل اصلی از جایی شروع می‌شود که دانشی که همراه کد عوض می‌شود فقط در ویکی بماند. کد در مخزن تغییر می‌کند، پیکربندی عوض می‌شود، رابط برنامه‌نویسی شکل تازه‌ای می‌گیرد، اما صفحه‌ی بیرون از این چرخه همان‌طور باقی می‌ماند. این عقب‌افتادن معمولاً از بی‌دقتی یک نفر نمی‌آید؛ از جدا بودن مسیر تغییر کد و مسیر تغییر سند می‌آید.</p>
<p>مثلاً دستور اجرای محلی سرویس در ویکی مانده، اما فایل‌های داخل مخزن عوض شده‌اند. نفر تازه‌وارد طبق ویکی جلو می‌رود، خطا می‌گیرد، از چند نفر می‌پرسد، و در پایان می‌فهمد دستور درست در README مخزن بوده است. این فقط اتلاف وقت نیست؛ یعنی تیم دو روایت از یک واقعیت ساخته است.</p>
<p>در سازمان‌های بزرگ، این وضعیت معمولاً به چند نسخه از یک واقعیت ختم می‌شود: چند راهنما برای یک کار، چند توضیح برای یک رفتار، و چند صفحه که معلوم نیست کدام هنوز معتبر است. راه‌حل این نیست که سند بیشتری بنویسیم؛ باید یکی منبع قابل اتکا باشد و بقیه حذف، ادغام یا صریحاً قدیمی اعلام شوند.</p>
<div class="theme-admonition theme-admonition-danger admonition_xGDO alert alert--danger"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"></path></svg></span>خطر</div><div class="admonitionContent_hiHs"><p>سندی که بیرون از مسیر تغییر می‌ماند، کم‌کم از حافظه‌ی تیم به بایگانی حدس‌ها تبدیل می‌شود.</p></div></div>
<p>خرابی مستند هم باید مثل خرابی محصول دیده شود. اگر راهنمای اجرا غلط است، اگر مثال دیگر کار نمی‌کند، یا اگر صفحه‌ای خواننده را به مسیر اشتباه می‌برد، این یک مسئله‌ی واقعی در نگهداشت سیستم است؛ باید ثبت شود، مسئول داشته باشد و تا اصلاح کامل رها نشود.</p>
<p>پس بحث، حمله به ویکی نیست. بحث تقسیم درست مسئولیت است: ویکی می‌تواند نقشه و درگاه ورود باشد؛ اما رفتاری که با کد عوض می‌شود باید جایی بماند که هنگام تغییر کد دیده و بازبینی شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="کد-چه-میگوید-مستند-چه-باید-بگوید">کد چه می‌گوید، مستند چه باید بگوید؟<a href="https://example.com/blog/technical-team-memory-documentation#%DA%A9%D8%AF-%DA%86%D9%87-%D9%85%DB%8C%DA%AF%D9%88%DB%8C%D8%AF-%D9%85%D8%B3%D8%AA%D9%86%D8%AF-%DA%86%D9%87-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A8%DA%AF%D9%88%DB%8C%D8%AF" class="hash-link" aria-label="لینک مستقیم به کد چه می‌گوید، مستند چه باید بگوید؟" title="لینک مستقیم به کد چه می‌گوید، مستند چه باید بگوید؟" translate="no">​</a></h2>
<p>عبارت «کد خودش مستند است» اگر به معنای خوانا بودن کد باشد، درست است. اما اگر نتیجه بگیریم که دیگر نیازی به توضیح نداریم، خطاست. کد معمولاً نشان می‌دهد سیستم چه می‌کند؛ ولی همیشه نمی‌گوید چرا این راه انتخاب شده، کجا نباید تغییر کند، و چه محدودیتی پشت یک رفتار ظاهراً عجیب وجود دارد.</p>
<p><img decoding="async" loading="lazy" alt="کد چه می‌گوید، مستند چه باید بگوید؟" src="https://example.com/assets/images/code-vs-documentation-roles-30ea10f1251489423ba0f2e723eda325.png" width="1024" height="1535" class="img_m9Pm"></p>
<p>کامنت بد چیزی را تکرار می‌کند که از خود کد پیداست:</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Check if the error code is final.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> error_code </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> FINAL_ERROR_CODES</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">False</span><br></span></code></pre></div></div>
<p>کامنت خوب، زمینه‌ای را ثبت می‌کند که برای تغییر امن لازم است:</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># These errors are final for the old payment provider.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Retrying them can create duplicate settlement records.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> error_code </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> FINAL_ERROR_CODES</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">False</span><br></span></code></pre></div></div>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>قاعده‌ی ساده</div><div class="admonitionContent_hiHs"><p>اگر توضیح فقط رفتار آشکار کد را تکرار می‌کند، حذفش کن. اگر نیت، قرارداد، محدودیت یا دلیل تصمیم را روشن می‌کند، کنار کد نگهش دار.</p></div></div>
<p>نتیجه این نیست که هر خط کد باید توضیح داشته باشد. نتیجه این است که هر دانشی که برای تغییر امن لازم است و از خود کد روشن نیست، باید ثبت شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="حافظهی-فنی-چه-شکلهایی-دارد">حافظه‌ی فنی چه شکل‌هایی دارد؟<a href="https://example.com/blog/technical-team-memory-documentation#%D8%AD%D8%A7%D9%81%D8%B8%D9%87%DB%8C-%D9%81%D9%86%DB%8C-%DA%86%D9%87-%D8%B4%DA%A9%D9%84%D9%87%D8%A7%DB%8C%DB%8C-%D8%AF%D8%A7%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به حافظه‌ی فنی چه شکل‌هایی دارد؟" title="لینک مستقیم به حافظه‌ی فنی چه شکل‌هایی دارد؟" translate="no">​</a></h2>
<p>مستند یک چیز واحد نیست. بخشی از حافظه‌ی فنی در کامنت تابع و قرارداد API می‌ماند، بخشی در README و راهنمای راه‌اندازی، بخشی در سند طراحی، و بخشی در صفحه‌ی شروع یا ویکی. خطا از جایی شروع می‌شود که همه‌ی این‌ها را در یک ظرف بریزیم.</p>
<p>اول باید مخاطب روشن باشد. استفاده‌کننده‌ی API دنبال قرارداد استفاده است. نگه‌دارنده دنبال دلیل پیاده‌سازی و نقاط خطر است. تازه‌وارد مسیر شروع می‌خواهد. تصمیم‌گیر فنی دنبال گزینه‌های ردشده و هزینه‌های هر انتخاب است.</p>
<p><img decoding="async" loading="lazy" alt="هر مخاطب چه می‌خواهد؟" src="https://example.com/assets/images/documentation-audience-questions-851fc9c843e9f94dc3ffb6ab40d7306d.png" width="1448" height="1086" class="img_m9Pm"></p>
<p>از زاویه‌ی رفتار خواننده هم دو حالت مهم داریم. گاهی خواننده جست‌وجوگر است؛ سؤال مشخصی دارد و جواب سریع و دقیق می‌خواهد. گاهی رهگذر است؛ تازه با سیستم روبه‌رو شده و هنوز نمی‌داند باید دنبال چه چیزی بگردد. مستند مرجع برای اولی خوب است، صفحه‌ی شروع و آموزش گام‌به‌گام برای دومی.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>پرسش قبل از نوشتن</div><div class="admonitionContent_hiHs"><p>این سند قرار است تصمیم چه کسی را ساده‌تر کند؟ اگر جواب روشن نیست، خود سند هم روشن نخواهد شد.</p></div></div>
<p>بعد از مخاطب، نوع مستند مهم است. مرجع، آموزش، سند طراحی، توضیح مفهومی و صفحه‌ی شروع هرکدام کار جدا دارند. سندی که هم‌زمان می‌خواهد همه‌ی این کارها را انجام دهد، معمولاً هیچ‌کدام را خوب انجام نمی‌دهد.</p>
<p><img decoding="async" loading="lazy" alt="هر نوع مستند کار خودش را دارد" src="https://example.com/assets/images/documentation-types-and-their-roles-a2b266b945c96da85bb383f84733bd75.png" width="1122" height="1402" class="img_m9Pm"></p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>خطای رایج</div><div class="admonitionContent_hiHs"><p>مستند چندمنظوره معمولاً ظاهراً کامل است، اما در عمل برای خواننده‌ی مشخص کند و مبهم می‌شود.</p></div></div>
<p>مرجع API بهتر است تا حد ممکن از نزدیک‌ترین جای معتبر ساخته شود؛ مثلاً از کامنت و قرارداد کنار خود کد. اگر همان قرارداد را یک‌بار کنار کد و یک‌بار دستی در ویکی بنویسیم، از همان لحظه دو جای بالقوه برای اختلاف ساخته‌ایم.</p>
<p>سند مفهومی کار دیگری دارد. قرار نیست همه‌ی حالت‌ها را پوشش دهد؛ کارش ساختن فهم مشترک است. مرجع باید دقیق و قابل جست‌وجو باشد، سند مفهومی باید خواننده را به فهم کلی برساند. اگر این دو را قاطی کنیم، هم مرجع کند می‌شود، هم توضیح مفهومی سنگین.</p>
<p>صفحه‌ی شروع هم نباید انبار محتوا شود. کارش این است که خواننده را به مسیر درست بفرستد: این‌جا شروع کن، برای مرجع این را ببین، برای طراحی آن را بخوان، برای راه‌اندازی از این راهنما برو. اگر خودش تبدیل به ترکیبی از آموزش، مرجع، تاریخچه و لینک‌های پراکنده شود، دیگر صفحه‌ی شروع نیست؛ یک ویکی کوچک و گیج‌کننده است.</p>
<p>سند طراحی باید پیش از کدنویسی به تیم کمک کند تصمیم را بررسی کند، نه اینکه فقط بعد از پیاده‌سازی گزارش تصمیم باشد. مسئله، هدف و غیرهدف، راه‌حل پیشنهادی، گزینه‌های ردشده، هزینه‌های هر انتخاب، نگرانی‌های امنیتی و عملیاتی، و مسیر جابه‌جایی از وضعیت قدیم به وضعیت جدید باید قبل از شروع کار روشن شوند.</p>
<p>آموزش گام‌به‌گام باید برعکس عمل کند: خواننده را با کمترین حاشیه از صفر به یک نتیجه‌ی واقعی برساند؛ مثل اجرای سرویس، ساخت اولین درخواست یا نوشتن اولین تست. پیش‌نیازها باید معلوم باشند، گام‌ها از چشم خواننده شماره بخورند، و در هر گام خروجی مورد انتظار روشن باشد.</p>
<p>کامنت نزدیک‌ترین بخش حافظه‌ی فنی به کد است. کامنت سطح فایل باید نقش فایل را بگوید. کامنت کلاس باید مسئولیت و رابطه‌ی آن را روشن کند. کامنت تابع باید قرارداد رفتاری را توضیح دهد، نه اینکه نام تابع را تکرار کند. اگر توضیح دادن یک API سخت و طولانی است، شاید مشکل فقط در مستند نیست؛ شاید خود API بیش از حد پیچیده طراحی شده است.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>قاعده‌ی کامنت خوب</div><div class="admonitionContent_hiHs"><p>کامنت خوب یا قرارداد را روشن می‌کند، یا دلیل تصمیم را، یا محدودیتی را که تغییر آینده باید به آن وفادار بماند.</p></div></div>
<p>پس شکل مستند را نباید از روی سلیقه انتخاب کرد. باید دید خواننده دنبال چیست و آن دانش با چه چیزی تغییر می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-خواننده-فقط-انسان-نیست">وقتی خواننده فقط انسان نیست<a href="https://example.com/blog/technical-team-memory-documentation#%D9%88%D9%82%D8%AA%DB%8C-%D8%AE%D9%88%D8%A7%D9%86%D9%86%D8%AF%D9%87-%D9%81%D9%82%D8%B7-%D8%A7%D9%86%D8%B3%D8%A7%D9%86-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به وقتی خواننده فقط انسان نیست" title="لینک مستقیم به وقتی خواننده فقط انسان نیست" translate="no">​</a></h2>
<p>امروز بخشی از خواندن کد و مستند را ابزارهای مبتنی بر مدل زبانی بزرگ (LLM) انجام می‌دهند: برای توضیح کد، تولید تست، بازبینی و بازنویسی. این ابزارها وقتی زمینه‌ی درست ندارند، همان محدودیت انسان را دارند؛ مسیر اجرا را می‌بینند، اما دلیل تصمیم را نمی‌فهمند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>خطای خطرناک</div><div class="admonitionContent_hiHs"><p>وقتی زمینه‌ی درست کنار کد نیست، ابزار هوشمند هم ممکن است با اعتمادبه‌نفس پیشنهاد اشتباه بدهد.</p></div></div>
<p>LLM جای مستند را نمی‌گیرد؛ نبود مستند خوب را پرهزینه‌تر و آشکارتر می‌کند. پس مستند نزدیک به کد فقط برای آدم‌ها نیست. زمینه‌ای است که کیفیت فهم ابزارها را هم بالا می‌برد و احتمال پیشنهادهای ظاهراً منطقی اما نادرست را کم می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="حافظهی-فنی-چطور-زنده-میماند">حافظه‌ی فنی چطور زنده می‌ماند؟<a href="https://example.com/blog/technical-team-memory-documentation#%D8%AD%D8%A7%D9%81%D8%B8%D9%87%DB%8C-%D9%81%D9%86%DB%8C-%DA%86%D8%B7%D9%88%D8%B1-%D8%B2%D9%86%D8%AF%D9%87-%D9%85%DB%8C%D9%85%D8%A7%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به حافظه‌ی فنی چطور زنده می‌ماند؟" title="لینک مستقیم به حافظه‌ی فنی چطور زنده می‌ماند؟" translate="no">​</a></h2>
<p>مستند مهم، مثل کد، باید نگه داشته شود. یعنی مسئول داشته باشد، در تغییرهای مرتبط دیده شود، نسخه بخورد، بازبینی شود و وقتی قدیمی شد، یا به‌روز شود یا روشن کنار گذاشته شود.</p>
<p>بازبینی مستند فقط غلط‌گیری نگارشی نیست. باید سه زاویه را جدا دید: درستی فنی، وضوح برای مخاطب، و کیفیت نوشتار. بازبینی فنی می‌پرسد آیا سند واقعیت سیستم را درست می‌گوید؟ بازبینی مخاطب می‌پرسد آیا خواننده‌ی هدف با این متن به جواب می‌رسد؟ بازبینی نوشتاری می‌پرسد آیا متن روشن، منسجم و بی‌ابهام است؟</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>قاعده‌ی نگهداشت</div><div class="admonitionContent_hiHs"><p>هر سند مهم باید یک مسئول، یک جای رسمی و یک راه روشن برای تغییر داشته باشد.</p></div></div>
<p>برای هر سند فنی مهم باید بتوانیم به چند پرسش ساده جواب بدهیم: این سند کجا منبع قابل اتکا دارد؟ چه کسی مسئول آن است؟ آخرین بار کی و همراه با چه تغییری به‌روز شده؟ اگر غلط بود، از چه مسیری اصلاح می‌شود؟ اگر دیگر معتبر نیست، آیا این را صریح گفته‌ایم؟</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="این-حافظه-کجا-باید-زندگی-کند">این حافظه کجا باید زندگی کند؟<a href="https://example.com/blog/technical-team-memory-documentation#%D8%A7%DB%8C%D9%86-%D8%AD%D8%A7%D9%81%D8%B8%D9%87-%DA%A9%D8%AC%D8%A7-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%B2%D9%86%D8%AF%DA%AF%DB%8C-%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به این حافظه کجا باید زندگی کند؟" title="لینک مستقیم به این حافظه کجا باید زندگی کند؟" translate="no">​</a></h2>
<p>حرف اصلی این نیست که همه‌چیز باید کنار کد باشد و هر ابزار دیگری بی‌ارزش است. حرف دقیق‌تر این است: هر دانش باید جایی زندگی کند که هم خواننده‌ی درست آن را پیدا کند، هم تغییر درست بتواند آن را به‌روز نگه دارد.</p>
<p>اینجا باید بین سه جایگاه فرق بگذاریم. کنار کد یعنی کامنت تابع، توضیح رفتار حساس و قرارداد نزدیک به پیاده‌سازی. داخل مخزن یعنی README، پوشه‌ی docs، راه‌اندازی، تست، ساخت، انتشار و سند طراحی نسخه‌دار. ویکی یا Confluence یعنی دانش بین‌تیمی، سیاست‌ها، گزارش‌ها و مسیرهای شروع که کمتر با تغییر مستقیم کد عوض می‌شوند.</p>
<p><img decoding="async" loading="lazy" alt="هر نوع دانشی جای خودش را دارد" src="https://example.com/assets/images/where-documentation-belongs-c0891108568e825ac650b6885261d564.png" width="1122" height="1402" class="img_m9Pm"></p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>قاعده‌ی جای درست</div><div class="admonitionContent_hiHs"><p>اگر دانشی با کد تغییر می‌کند، تا حد ممکن نزدیک کد نگهش دار. اگر دانشی فقط مسیر پیدا کردن چیزها را نشان می‌دهد، آن را تبدیل به صفحه‌ی شروع کن، نه مرجع اصلی رفتار سیستم.</p></div></div>
<p>پس سؤال درست این نیست که از چه ابزاری استفاده کنیم؟ سؤال درست این است: این دانش با چه چیزی تغییر می‌کند و خواننده کجا دنبالش می‌گردد؟ جواب این دو پرسش معمولاً جای درست سند را نشان می‌دهد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی-حافظهی-فنی-باید-زنده-بماند">جمع‌بندی: حافظه‌ی فنی باید زنده بماند<a href="https://example.com/blog/technical-team-memory-documentation#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C-%D8%AD%D8%A7%D9%81%D8%B8%D9%87%DB%8C-%D9%81%D9%86%DB%8C-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%B2%D9%86%D8%AF%D9%87-%D8%A8%D9%85%D8%A7%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی: حافظه‌ی فنی باید زنده بماند" title="لینک مستقیم به جمع‌بندی: حافظه‌ی فنی باید زنده بماند" translate="no">​</a></h2>
<p>هدف مستند، زیاد کردن متن نیست؛ ساختن حافظه‌ای است که تیم بتواند به آن اعتماد کند. حافظه‌ای که فقط در ذهن آدم‌ها باشد، با رفتن آدم‌ها کم‌رنگ می‌شود. حافظه‌ای که فقط در ویکی دور از کد باشد، با تغییر سیستم عقب می‌افتد. حافظه‌ای که کنار کد، داخل مخزن، کنار تصمیم و در مسیر تغییر باشد، شانس زنده ماندن دارد.</p>
<p>کد حقیقت اجرای سیستم را نشان می‌دهد، اما همه‌ی حقیقت مهندسی را نه. دلیل تصمیم‌ها، قراردادهای پنهان، محدودیت‌های تاریخی، مخاطب مستند و جای درست هر دانشی باید جایی نوشته شود که هم پیدا شود و هم به‌روز بماند.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>جمع‌بندی</div><div class="admonitionContent_hiHs"><p>مستند خوب قرار نیست جای کد را بگیرد؛ قرار است کمک کند کد، تصمیم‌ها و تغییرهای آینده درست‌تر فهمیده شوند.</p></div></div>
<p>حافظه‌ی فنی زنده با متن زیاد ساخته نمی‌شود؛ با منبع قابل اتکا، مسئول مشخص، و نگهداشت پیوسته ساخته می‌شود. پس مسئله این نیست که مستند داشته باشیم یا نه. مسئله این است که مستندمان بخشی از کار مهندسی باشد، نه کاری تزئینی پس از آن. اگر مستند کنار مسیر واقعی تغییر بماند، به حافظه‌ی فنی تیم تبدیل می‌شود؛ اگر جدا بماند، دیر یا زود فقط بایگانی محترمانه‌ای از حدس‌ها خواهد بود.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="Software Engineering" term="Software Engineering"/>
        <category label="documentation" term="documentation"/>
        <category label="code-review" term="code-review"/>
        <category label="llm" term="llm"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[نه به معماری نمایشی]]></title>
        <id>https://example.com/blog/software-engineering-growth-story</id>
        <link href="https://example.com/blog/software-engineering-growth-story"/>
        <updated>2026-05-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[روایتی از بزرگ‌شدن نرم‌افزار؛ از یک API ساده تا صف پیام، Kubernetes، BPMS و هوش مصنوعی، با تأکید بر انتخاب ابزار برای درد واقعی نه نمایش معماری.]]></summary>
        <content type="html"><![CDATA[<p>گاهی یک نرم‌افزار از جای خیلی ساده‌ای آغاز می‌شود: چند صفحه، چند قابلیت روشن، چند کاربر اول، و یک تیم کوچک که فقط می‌خواهد محصولش درست کار کند. اما اگر آن محصول زنده بماند، آرام‌آرام بزرگ می‌شود؛ نیازهای تازه پیدا می‌کند، کاربران بیشتری سراغش می‌آیند، تصمیم‌های قدیمی سنگین‌تر می‌شوند، و چیزهایی که دیروز ساده و کافی بودند، امروز گره‌های تازه می‌سازند. درست همین‌جاست که بسیاری از واژه‌های ظاهراً خشک مهندسی نرم‌افزار، از دل زندگی واقعی یک سیستم معنا پیدا می‌کنند.</p>
<p>من در این نوشته نمی‌خواهم از همان ابتدا سراغ معماری‌های پرزرق‌وبرق بروم و برای هر مسئله‌ای یک ابزار بزرگ روی میز بگذارم. برعکس، می‌خواهم از یک سؤال ساده شروع کنم: اگر واقعاً بخواهیم محصولی را قدم‌به‌قدم بسازیم، چه زمانی به چه چیزی نیاز پیدا می‌کنیم؟ چه زمانی یک برنامه‌ی ساده کافی است؟ چه زمانی همان سادگی، دیگر کمک نمی‌کند و تبدیل به مانع می‌شود؟ چرا روزی به طراحی بهتر رابط‌های برنامه‌نویسی فکر می‌کنیم، روزی به صف پیام، روزی به کانتینر، روزی به مهاجرت داده، و روزی به عملیات یادگیری ماشین؟</p>
<p>این نوشته سفری مرحله‌به‌مرحله در مسیر بزرگ‌شدن یک نرم‌افزار است؛ از روزی که فقط می‌خواهیم چیزی کار کند، تا روزی که باید قابل تغییر، قابل اعتماد و قابل نگه‌داری هم بماند. قرار نیست با واژه‌ها مرعوب شویم؛ قرار است بفهمیم هرکدام کجا به درد می‌خورند و چرا نباید زودتر از موعد سراغشان برویم. خط اصلی این مسیر برای من همین است: نه معماری نمایشی، نه سادگی بی‌مسئولیت.</p>
<p><img decoding="async" loading="lazy" alt="رشد تدریجی یک سامانه‌ی نرم‌افزاری؛ از یک برنامه‌ی ساده تا ساختاری پیچیده‌تر و بالغ‌تر" src="https://example.com/assets/images/software-system-growth-journey-b46590a941e74c152134e45acdef1ab4.png" width="1672" height="941" class="img_m9Pm"></p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-یک-تغییر-کوچک-چند-جای-سیستم-را-میشکند">وقتی یک تغییر کوچک، چند جای سیستم را می‌شکند<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%DB%8C%DA%A9-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1-%DA%A9%D9%88%DA%86%DA%A9-%DA%86%D9%86%D8%AF-%D8%AC%D8%A7%DB%8C-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D8%B1%D8%A7-%D9%85%DB%8C%D8%B4%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی یک تغییر کوچک، چند جای سیستم را می‌شکند" title="لینک مستقیم به وقتی یک تغییر کوچک، چند جای سیستم را می‌شکند" translate="no">​</a></h2>
<p>گاهی ماجرا از یک تغییر خیلی کوچک آغاز می‌شود. مثلاً در پاسخ سرور، نام یک فیلد را عوض می‌کنیم؛ چون در کد تازه، نام جدید تمیزتر و دقیق‌تر به نظر می‌رسد. روی نسخه‌ی وب همه‌چیز درست کار می‌کند. چند بار هم دستی امتحان می‌کنیم و مشکلی نمی‌بینیم. با خودمان می‌گوییم: «چیزی نبود، فقط اسم یک فیلد عوض شد.»</p>
<p>اما چند ساعت بعد پیام‌ها شروع می‌شوند. برنامه‌ی موبایل قیمت محصول را نشان نمی‌دهد. پنل مدیریت در بارگذاری داده‌ها خطا می‌دهد. شاید یک گزارش داخلی هم از همان پاسخ استفاده می‌کرده و حالا خالی مانده است. ناگهان روشن می‌شود چیزی که برای ما «یک تغییر کوچک» بود، برای چند بخش دیگر از سیستم، شکستن یک قول و قرار بوده است.</p>
<p>اینجاست که API یا همان رابط برنامه‌نویسی، از یک جزئیات فنی ساده به یک قرارداد تبدیل می‌شود. تا وقتی فقط خودمان از آن استفاده می‌کنیم، شاید چند مسیر و چند پاسخ ساده به نظر برسد. اما وقتی وب، موبایل، پنل مدیریت، یک تیم دیگر یا یک سرویس بیرونی به آن وابسته می‌شوند، دیگر با یک تکه کد معمولی طرف نیستیم. داریم زبانی مشترک می‌سازیم که دیگران بر پایه‌ی آن کارشان را جلو می‌برند.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>هر API دیر یا زود از «راهی برای گرفتن داده» به «قراردادی میان بخش‌های سیستم» تبدیل می‌شود. هرچه مصرف‌کننده‌های بیشتری به آن وابسته شوند، تغییر دادن آن هم باید سنجیده‌تر باشد.</p></div></div>
<p>رویکرد API-first را می‌شود خیلی ساده این‌طور فهمید: <strong>اول قرارداد را روشن کنیم، بعد سراغ پیاده‌سازی برویم.</strong> یعنی پیش از اینکه با عجله کد بزنیم، کمی مکث کنیم و بپرسیم این بخش از سیستم قرار است با چه کسانی حرف بزند. چه داده‌ای لازم است؟ نام‌ها برای مصرف‌کننده قابل فهم‌اند؟ خطاها چطور برمی‌گردند؟ اگر چیزی تغییر کرد، تکلیف نسخه‌های قبلی چه می‌شود؟ پاسخ امروز فقط نیاز همین صفحه را حل می‌کند، یا برای رشد آرام فردا هم جایی می‌گذارد؟</p>
<p><img decoding="async" loading="lazy" alt="یک تغییر کوچک در API می‌تواند هم‌زمان وب، موبایل و پنل مدیریت را دچار خطا کند" src="https://example.com/assets/images/api-contract-small-change-big-break-9a28f159b5cf71b503ec030c46c6aadf.png" width="1672" height="941" class="img_m9Pm"></p>
<p><em>یک تغییر کوچک در پاسخ سرور، وقتی چند مصرف‌کننده دارد، دیگر فقط یک تغییر کوچک نیست.</em></p>
<p>اینجا بحث سازگاری هم مهم می‌شود. منظور از سازگاری پسرو (Backward Compatibility) این است که نسخه‌های قدیمی‌ترِ مصرف‌کننده‌ها بعد از تغییر API همچنان کار کنند. مثلاً اگر فیلد <code>price</code> را ناگهان حذف کنیم و فقط <code>unit_price</code> را نگه داریم، برنامه‌ی موبایلی که هنوز به‌روزرسانی نشده ممکن است قیمت را گم کند. راه سنجیده‌تر این است که برای مدتی هر دو فیلد را برگردانیم، مصرف‌کننده‌ها را به نام تازه کوچ بدهیم، و بعد از گذر زمان و با اطمینان، فیلد قدیمی را حذف کنیم.</p>
<p>سازگاری پیشرو (Forward Compatibility) از زاویه‌ی دیگری به همین مسئله نگاه می‌کند: آیا مصرف‌کننده‌های امروز می‌توانند با پاسخ‌های کمی تازه‌تر کنار بیایند؟ مثلاً اگر سرور یک فیلد جدید به پاسخ اضافه کرد، آیا برنامه‌ی موبایل فقط آن را نادیده می‌گیرد یا چون چیزی را نمی‌شناسد، خطا می‌دهد؟ API خوب معمولاً به مصرف‌کننده‌ها یاد می‌دهد در برابر چیزهای تازه‌ای که به قرارداد افزوده می‌شوند، شکننده نباشند.</p>
<p><img decoding="async" loading="lazy" alt="مقایسه‌ی سازگاری پسرو، تغییر ناسازگار، و سازگاری پیشرو در API" src="https://example.com/assets/images/api-compatibility-backward-forward-1e41efb9ed4ce01585cfdc789bbb8718.png" width="1672" height="941" class="img_m9Pm"></p>
<p><em>در تغییرهای امن‌تر، اول چیز تازه را اضافه می‌کنیم، سپس مصرف‌کننده‌ها را آرام‌آرام کوچ می‌دهیم، و در پایان نسخه‌ی قدیمی را بازنشسته می‌کنیم.</em></p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>تغییر امن‌تر یعنی تغییر مرحله‌ای</div><div class="admonitionContent_hiHs"><p>در APIهای واقعی، تغییر خوب معمولاً یک حرکت ناگهانی نیست. اول چیز تازه را اضافه می‌کنیم، بعد مصرف‌کننده‌ها را آرام‌آرام به آن کوچ می‌دهیم، بعد رفتار قدیمی را منسوخ اعلام می‌کنیم، و در نهایت، وقتی مطمئن شدیم کسی به آن وابسته نیست، حذفش می‌کنیم.</p></div></div>
<table><thead><tr><th>نوع تغییر</th><th style="text-align:right">معمولاً امن‌تر است؟</th><th>چرا؟</th></tr></thead><tbody><tr><td>افزودن یک فیلد تازه به پاسخ</td><td style="text-align:right">بله</td><td>مصرف‌کننده‌های قدیمی می‌توانند آن را نادیده بگیرند.</td></tr><tr><td>حذف ناگهانی یک فیلد قدیمی</td><td style="text-align:right">نه</td><td>مصرف‌کننده‌های قدیمی ممکن است هنوز به آن وابسته باشند.</td></tr><tr><td>تغییر معنای یک فیلد بدون تغییر نام</td><td style="text-align:right">نه</td><td>ظاهر قرارداد ثابت می‌ماند، اما رفتار واقعی عوض می‌شود.</td></tr><tr><td>افزودن نسخه‌ی تازه در کنار نسخه‌ی قدیمی</td><td style="text-align:right">معمولاً بله</td><td>فرصت مهاجرت مرحله‌ای می‌دهد.</td></tr></tbody></table>
<p>البته این نگاه به معنی سنگین کردن کار از روز اول نیست. قرار نیست محصولی را که هنوز شکل نگرفته، زیر بار سندهای طولانی، جلسه‌های زیاد و طراحی‌های خشک ببریم. گاهی یک قرارداد کوتاه، چند نمونه‌ی روشن از درخواست و پاسخ، نام‌گذاری دقیق، و توافق ساده میان اعضای تیم کافی است. مسئله این نیست که همه‌چیز را بزرگ و تشریفاتی کنیم؛ مسئله این است که بفهمیم کجا داریم چیزی می‌سازیم که دیگران روی آن حساب می‌کنند.</p>
<table><thead><tr><th>نگاه عجولانه</th><th>نگاه سنجیده‌تر</th></tr></thead><tbody><tr><td>فعلاً همین پاسخ کار می‌کند.</td><td>چه کسی قرار است این پاسخ را مصرف کند؟</td></tr><tr><td>نام فیلد را بعداً عوض می‌کنیم.</td><td>تغییر نام فیلد چه چیزی را می‌شکند؟</td></tr><tr><td>خطا را هرطور شد برمی‌گردانیم.</td><td>خطا باید برای مصرف‌کننده قابل فهم باشد.</td></tr><tr><td>مستندات را بعداً می‌نویسیم.</td><td>چند نمونه‌ی روشن از درخواست و پاسخ داریم.</td></tr><tr><td>فقط نیاز امروز مهم است.</td><td>نیاز امروز مهم است، اما راه تغییر فردا هم نباید بسته شود.</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>API-first یعنی از روز اول کار را کند، رسمی و پر از تشریفات کنیم؟ نه. یعنی پیش از پیاده‌سازی، به قرارداد میان بخش‌های سیستم کمی احترام بگذاریم.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه که می‌گوید وقت جدی‌تر گرفتن API رسیده است</summary><div><div class="collapsibleContent_T_pc"><p>اگر برای تغییر دادن یک پاسخ ساده باید چند مصرف‌کننده را بررسی کنیم، با چند نفر هماهنگ شویم، و نگران شکستن بخش‌های دیگر باشیم، آن API دیگر فقط یک جزئیات داخلی نیست. در این نقطه بهتر است آن را مثل یک قرارداد ببینیم: روشن، قابل فهم، تا حد ممکن پایدار، و قابل تغییر با احتیاط.</p></div></div></details>
<p>برای من، درس این بخش همین است: هنوز قرار نیست معماری بزرگی بسازیم، اما باید بفهمیم بعضی چیزها زودتر از بقیه به مرز سیستم تبدیل می‌شوند. API یکی از همان جاهاست؛ جایی که تصمیم‌های کوچک امروز، روی آزادی عمل فردای ما اثر می‌گذارند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-یک-پاسخ-واحد-برای-همه-مناسب-نیست">وقتی یک پاسخ واحد، برای همه مناسب نیست<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%DB%8C%DA%A9-%D9%BE%D8%A7%D8%B3%D8%AE-%D9%88%D8%A7%D8%AD%D8%AF-%D8%A8%D8%B1%D8%A7%DB%8C-%D9%87%D9%85%D9%87-%D9%85%D9%86%D8%A7%D8%B3%D8%A8-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به وقتی یک پاسخ واحد، برای همه مناسب نیست" title="لینک مستقیم به وقتی یک پاسخ واحد، برای همه مناسب نیست" translate="no">​</a></h2>
<p>اوایل کار، معمولاً یک API عمومی برای همه کافی است. نسخه‌ی وب همان داده‌ای را می‌گیرد که لازم دارد، صفحه‌ها را می‌سازد، و کاربر هم بدون دردسر با محصول کار می‌کند. تا وقتی فقط یک نما داریم، این مدل ساده و قابل فهم است. حتی اگر کمی داده‌ی اضافه هم در پاسخ‌ها باشد، هنوز مسئله‌ی بزرگی به نظر نمی‌رسد.</p>
<p>اما محصول که جلوتر می‌رود، نماها هم شبیه هم نمی‌مانند. برنامه‌ی موبایل می‌خواهد پاسخ‌ها سبک‌تر باشند، چون صفحه کوچک‌تر است و شبکه همیشه پایدار نیست. نسخه‌ی وب شاید به داده‌های بیشتری برای ساختن یک صفحه‌ی کامل نیاز داشته باشد. پنل مدیریت هم اصلاً جنس دیگری از اطلاعات می‌خواهد؛ جزئیات بیشتر، وضعیت‌های داخلی، ابزارهای جست‌وجو، و داده‌هایی که نباید در دسترس کاربر عادی باشد.</p>
<p>اینجاست که یک API عمومی کم‌کم زیر فشار قرار می‌گیرد. اگر بخواهیم همه را با همان پاسخ واحد راضی کنیم، یا پاسخ‌ها بیش از حد بزرگ و شلوغ می‌شوند، یا هر نما مجبور می‌شود خودش چندین درخواست بزند و داده‌ها را کنار هم بچیند. نتیجه معمولاً این است: بخشی از پیچیدگی بک‌اند، آرام‌آرام به سمت فرانت‌اند هل داده می‌شود.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>وقتی نیازهای وب، موبایل و پنل مدیریت واقعاً از هم فاصله می‌گیرند، یک پاسخ واحد ممکن است دیگر ساده‌ترین راه نباشد؛ ممکن است فقط ظاهر ساده‌ای داشته باشد و پیچیدگی را به جای دیگری منتقل کند.</p></div></div>
<p>بک‌اند ویژه‌ی نما یا Backend for Frontend، که معمولاً به اختصار BFF گفته می‌شود، پاسخی به همین وضعیت است. ایده‌اش این است که به‌جای ساختن یک API عمومی که قرار است همه‌ی نماها را راضی کند، برای هر تجربه‌ی کاربری مهم، یک لایه‌ی بک‌اند نزدیک‌تر به همان نما بسازیم. این لایه می‌تواند داده‌ها را از چند سرویس یا چند API بگیرد، آن‌ها را به شکل مناسب کنار هم بگذارد، چیزهای اضافه را حذف کند، و پاسخی برگرداند که دقیقاً به درد همان نما بخورد.</p>
<p>مثلاً برنامه‌ی موبایل شاید برای صفحه‌ی سفارش فقط نام محصول، قیمت، وضعیت ارسال و یک دکمه‌ی اقدام لازم داشته باشد. اما پنل مدیریت برای همان سفارش، شناسه‌های داخلی، وضعیت پرداخت، تاریخچه‌ی تغییرات، یادداشت پشتیبانی و چند فیلتر دیگر می‌خواهد. اگر هر دو را با یک پاسخ واحد تغذیه کنیم، یا موبایل داده‌ی زیادی می‌گیرد، یا پنل مدیریت داده‌ی کمی. BFF کمک می‌کند هرکدام چیزی را بگیرند که برای تجربه‌ی خودشان مناسب‌تر است.</p>
<p><img decoding="async" loading="lazy" alt="مقایسه‌ی یک API عمومی با بک‌اند ویژه‌ی نما برای وب، موبایل و پنل مدیریت" src="https://example.com/assets/images/bff-tailored-backend-for-each-frontend-7dcb32993a74c8ac446c918ecd8e1dae.png" width="1536" height="1024" class="img_m9Pm"></p>
<p><em>در این تصویر، اصل مسئله دیده می‌شود: وقتی نیاز نماها از هم فاصله می‌گیرد، یک پاسخ عمومی ممکن است همه را کمی ناراضی کند؛ اما BFF می‌تواند پاسخ هر نما را به شکل مناسب خودش آماده کند.</em></p>
<p>البته اینجا هم همان قاعده‌ی همیشگی برقرار است: نباید زودتر از درد واقعی، درمان پیچیده بیاوریم. اگر محصول کوچک است، فقط یک کلاینت دارد، یا تفاوت نیازها هنوز جدی نشده، ساختن چند لایه‌ی BFF بیشتر از اینکه کمک کند، نگه‌داری را سخت می‌کند. هر لایه‌ی تازه یعنی کد تازه، خطای تازه، آزمون تازه، مالکیت تازه و هماهنگی تازه.</p>
<table><thead><tr><th>وضعیت</th><th>احتمالاً چه کاری بهتر است؟</th></tr></thead><tbody><tr><td>فقط یک نما داریم و نیازها ساده‌اند</td><td>همان API عمومی کافی است.</td></tr><tr><td>وب و موبایل تفاوت‌های کوچک دارند</td><td>شاید کمی بهبود در همان API کافی باشد.</td></tr><tr><td>هر نما داده‌ی متفاوت، شکل متفاوت و سرعت متفاوت می‌خواهد</td><td>BFF می‌تواند ارزشمند شود.</td></tr><tr><td>هر صفحه برای خودش BFF جدا می‌خواهد</td><td>احتمالاً داریم بیش از حد خرد می‌کنیم.</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>BFF یعنی برای هر صفحه یا هر دکمه، یک بک‌اند جدا بسازیم؟ نه. BFF زمانی معنا دارد که تفاوت تجربه‌ها واقعی، تکرارشونده و پرهزینه شده باشد؛ نه وقتی فقط می‌خواهیم معماری را پیچیده‌تر نشان بدهیم.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه که می‌گوید شاید وقت BFF رسیده است</summary><div><div class="collapsibleContent_T_pc"><p>اگر فرانت‌اند برای ساختن یک صفحه باید چندین API را صدا بزند، داده‌های زیادی را دور بریزد، پاسخ‌ها را خودش به هم بچسباند، و مدام درگیر جزئیات داخلی بک‌اند شود، احتمالاً بخشی از مسئولیت اشتباه جابه‌جا شده است. در این نقطه، یک لایه‌ی BFF می‌تواند پیچیدگی را به جایی برگرداند که بهتر می‌تواند آن را مدیریت کند.</p></div></div></details>
<p>برای من، BFF یعنی احترام گذاشتن به این واقعیت که همه‌ی مصرف‌کننده‌ها یکسان نیستند. وب، موبایل و پنل مدیریت شاید از یک محصول حرف بزنند، اما تجربه‌ی یکسانی نمی‌سازند. پس گاهی لازم است بک‌اند هم به‌جای یک پاسخ عمومی برای همه، پاسخ‌هایی نزدیک‌تر به نیاز هر نما فراهم کند؛ البته فقط وقتی این تفاوت واقعاً به اندازه‌ی کافی جدی شده باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-ورودی-سیستم-شلوغ-میشود-و-سرویسها-هم-با-هم-حرف-دارند">وقتی ورودی سیستم شلوغ می‌شود و سرویس‌ها هم با هم حرف دارند<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D9%88%D8%B1%D9%88%D8%AF%DB%8C-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D8%B4%D9%84%D9%88%D8%BA-%D9%85%DB%8C%D8%B4%D9%88%D8%AF-%D9%88-%D8%B3%D8%B1%D9%88%DB%8C%D8%B3%D9%87%D8%A7-%D9%87%D9%85-%D8%A8%D8%A7-%D9%87%D9%85-%D8%AD%D8%B1%D9%81-%D8%AF%D8%A7%D8%B1%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی ورودی سیستم شلوغ می‌شود و سرویس‌ها هم با هم حرف دارند" title="لینک مستقیم به وقتی ورودی سیستم شلوغ می‌شود و سرویس‌ها هم با هم حرف دارند" translate="no">​</a></h2>
<p>تا اینجا محصول چند نمای متفاوت پیدا کرده است: وب، موبایل و پنل مدیریت. هرکدام هم نیاز خودش را دارد و کم‌کم مسیرهای بیشتری به بک‌اند باز شده‌اند. اول شاید همه‌چیز ساده به نظر برسد؛ چند درخواست از بیرون می‌آید و چند پاسخ برمی‌گردد. اما بعد از مدتی سؤال‌های تازه‌ای پیدا می‌شوند: چه کسی حق دارد وارد سیستم شود؟ اگر یک کاربر یا یک ربات بیش از اندازه درخواست فرستاد چه کنیم؟ درخواست‌ها را کجا ثبت و ردیابی کنیم؟ نسخه‌های مختلف API را چطور مدیریت کنیم؟</p>
<p>اگر برای هر سرویس جداگانه همین منطق‌ها را بنویسیم، خیلی زود با تکرار و آشفتگی روبه‌رو می‌شویم. یک سرویس احراز هویت را یک‌جور انجام می‌دهد، سرویس دیگر محدودسازی درخواست را جور دیگری پیاده می‌کند، و لاگ‌ها هم هرکدام شکل خودشان را دارند. نتیجه این می‌شود که ورودی سیستم به جای یک مسیر قابل فهم، تبدیل می‌شود به چند در کوچک و پراکنده که هرکدام قانون خودش را دارد.</p>
<p><img decoding="async" loading="lazy" alt="پیش از API Gateway، کلاینت‌های مختلف مستقیم و پراکنده به چند بخش بک‌اند وصل می‌شوند" src="https://example.com/assets/images/before-api-gateway-scattered-entrypoints-3fcaa4fa068bad9b7b36360a0ec5c23d.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>وقتی هر نما مستقیم به چند مسیر و سرویس وصل می‌شود، کنترل ورودی‌ها سخت‌تر و پراکنده‌تر می‌شود.</em></p>
<p>اینجاست که API Gateway معنا پیدا می‌کند. Gateway را می‌توان مثل در ورودی آگاهانه‌ی سیستم دید؛ جایی که درخواست‌های بیرونی از آن عبور می‌کنند و بعد به سرویس مناسب می‌رسند. این لایه می‌تواند بخشی از کارهای مشترک را متمرکزتر کند: احراز هویت، محدودسازی نرخ درخواست، مسیریابی، ثبت لاگ، کنترل دسترسی و گاهی تبدیل شکل درخواست یا پاسخ.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>API Gateway برای مدیریت ورودی‌های بیرونی سیستم است. یعنی جایی میان کلاینت‌ها و سرویس‌های داخلی می‌نشیند تا هر سرویس مجبور نباشد همه‌ی دغدغه‌های مشترک ورودی را دوباره از نو حل کند.</p></div></div>
<p><img decoding="async" loading="lazy" alt="API Gateway مثل در ورودی سیستم، درخواست‌های وب، موبایل و پنل مدیریت را دریافت و به سرویس مناسب هدایت می‌کند" src="https://example.com/assets/images/api-gateway-single-entrypoint-e396a56d6305e21ffc81feab8cb3fec4.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در این مدل، درخواست‌های بیرونی اول از یک نقطه‌ی مشخص عبور می‌کنند و بعد به سرویس مناسب می‌رسند.</em></p>
<p>اینجا ممکن است یک سؤال طبیعی پیش بیاید: مگر در بخش قبل نگفتیم BFF هم بین فرانت‌اند و بک‌اند می‌نشیند؟ پس Gateway چه فرقی با BFF دارد؟ فرق اصلی در نیت و مسئولیت آن‌هاست. BFF نزدیک به تجربه‌ی کاربری است و می‌پرسد «این نما دقیقاً چه داده‌ای و با چه شکلی لازم دارد؟» اما Gateway نزدیک به مرز ورودی سیستم است و می‌پرسد «این درخواست اصلاً اجازه‌ی ورود دارد؟ به کدام سرویس باید برود؟ چطور محدود، ثبت و ردیابی شود؟»</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>فرق BFF و Gateway</div><div class="admonitionContent_hiHs"><p>BFF پاسخ را برای نیاز یک نما شکل می‌دهد؛ Gateway ورود درخواست‌ها به سیستم را مدیریت می‌کند. ممکن است در یک معماری هر دو را داشته باشیم: کلاینت‌ها اول از Gateway عبور کنند و بعد، بسته به نیاز، به BFF وب، BFF موبایل یا سرویس‌های داخلی برسند.</p></div></div>
<table><thead><tr><th>پرسش</th><th>BFF</th><th>API Gateway</th></tr></thead><tbody><tr><td>به چه چیزی نزدیک‌تر است؟</td><td>تجربه‌ی کاربری و نیاز هر نما</td><td>مرز ورودی سیستم</td></tr><tr><td>دغدغه‌ی اصلی چیست؟</td><td>شکل‌دهی پاسخ مناسب برای وب، موبایل یا پنل مدیریت</td><td>ورود امن، مسیریابی، محدودسازی و ثبت درخواست‌ها</td></tr><tr><td>منطق کسب‌وکار کجا باید باشد؟</td><td>تا حد ممکن نه در BFF؛ فقط ترکیب و آماده‌سازی داده‌ی نما</td><td>نباید در Gateway پخش شود؛ Gateway جای منطق محصول نیست</td></tr><tr><td>چه زمانی معنا پیدا می‌کند؟</td><td>وقتی نیاز نماها واقعاً متفاوت شده باشد</td><td>وقتی ورودی‌ها زیاد، حساس یا پراکنده شده باشند</td></tr></tbody></table>
<p>اما این تمرکز یک دام هم دارد. هرچه چیزهای بیشتری را از یک نقطه عبور می‌دهیم، باید بیشتر مراقب باشیم همان نقطه به گلوگاه یا محل خوابیدن کل سیستم تبدیل نشود. Gateway قرار نیست یک سرور تنها و قهرمان باشد که اگر از کار افتاد، همه‌ی مسیرهای ورود به سیستم هم از کار بیفتند. Gateway یک نقش معماری است، نه الزاماً یک نمونه‌ی منفرد.</p>
<div class="theme-admonition theme-admonition-caution admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>تمرکز همیشه بی‌هزینه نیست</div><div class="admonitionContent_hiHs"><p>وقتی ورودی‌های سیستم را از یک Gateway عبور می‌دهیم، کنترل و نظم بیشتری به دست می‌آوریم؛ اما هم‌زمان باید مراقب باشیم Gateway به «نقطه‌ی شکست واحد» (Single Point of Failure) تبدیل نشود. در عمل، Gateway معمولاً باید چند نمونه‌ی فعال داشته باشد، پشت بارپخش‌کننده قرار بگیرد، پایش و هشدار درست داشته باشد، و در برابر افزایش ناگهانی درخواست‌ها تاب‌آور باشد.</p></div></div>
<p>داستان همین‌جا تمام نمی‌شود. فرض کنیم محصول بزرگ‌تر شده و دیگر پشت صحنه فقط یک بک‌اند ساده نداریم. سرویس سفارش با سرویس پرداخت حرف می‌زند، پرداخت با کیف پول، سفارش با اعلان، و گزارش‌گیری با چند بخش دیگر. حالا مسئله فقط «ورود درخواست از بیرون» نیست؛ مسئله‌ی تازه این است که سرویس‌های داخلی چگونه با هم حرف بزنند.</p>
<p>اینجا سؤال‌ها عوض می‌شوند. اگر سرویس سفارش کند شد، از کجا بفهمیم مشکل از خودش بوده یا از پرداخت؟ اگر ارتباط بین دو سرویس شکست، تکرار درخواست چگونه انجام شود؟ ارتباط داخلی سرویس‌ها امن است؟ اگر بخواهیم فقط بخشی از ترافیک را به نسخه‌ی تازه‌ی یک سرویس بفرستیم، این کار کجا مدیریت شود؟ آیا همه‌ی این منطق‌ها باید در کد تک‌تک سرویس‌ها تکرار شوند؟</p>
<p>Service Mesh برای چنین مسئله‌ای مطرح می‌شود. اگر API Gateway بیشتر به در ورودی شهر شبیه باشد، Service Mesh بیشتر شبیه شبکه‌ی خیابان‌ها و چراغ‌ها و تابلوهایی است که رفت‌وآمد درون شهر را قابل کنترل و قابل مشاهده می‌کند. این لایه روی ارتباط میان سرویس‌های داخلی تمرکز دارد: ردیابی درخواست‌ها، کنترل ترافیک، امنیت ارتباط سرویس به سرویس، سیاست‌های تکرار درخواست، و مشاهده‌پذیری بهتر.</p>
<p><img decoding="async" loading="lazy" alt="Service Mesh روی ارتباط میان سرویس‌های داخلی تمرکز دارد؛ جایی که سرویس‌ها مدام با هم گفت‌وگو می‌کنند" src="https://example.com/assets/images/service-mesh-internal-service-traffic-cf1b0e73cb14340a7dc33366e6cf9749.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>اگر Gateway ورودی سیستم را سامان می‌دهد، Service Mesh گفت‌وگوی درونی سرویس‌ها را قابل مشاهده‌تر و قابل کنترل‌تر می‌کند.</em></p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>زیاده‌روی رایج</div><div class="admonitionContent_hiHs"><p>Service Mesh را نباید فقط چون مدرن و جذاب است وارد سیستم کنیم. اگر چند سرویس محدود داریم و ارتباط‌ها ساده‌اند، این لایه می‌تواند خودش به منبع تازه‌ای از پیچیدگی تبدیل شود.</p></div></div>
<p>برای ساده‌تر شدن تفاوت این سه، می‌شود این‌طور نگاه کرد:</p>
<table><thead><tr><th>مفهوم</th><th>بیشتر کجا می‌نشیند؟</th><th>مسئله‌ی اصلی که حل می‌کند</th><th>خطر مهم اگر بد طراحی شود</th></tr></thead><tbody><tr><td>BFF</td><td>نزدیک به هر نمای کاربری</td><td>آماده‌سازی پاسخ مناسب برای وب، موبایل یا پنل مدیریت</td><td>ممکن است منطق محصول را تکه‌تکه و پخش کند.</td></tr><tr><td>API Gateway</td><td>میان کلاینت‌ها و سرویس‌های داخلی</td><td>مدیریت ورودی بیرونی، مسیریابی، احراز هویت، محدودسازی درخواست</td><td>می‌تواند گلوگاه یا نقطه‌ی شکست واحد شود.</td></tr><tr><td>Service Mesh</td><td>میان خود سرویس‌های داخلی</td><td>مدیریت ارتباط سرویس به سرویس، ردیابی، امنیت داخلی، کنترل ترافیک</td><td>می‌تواند پیچیدگی عملیاتی و عیب‌یابی را بیشتر کند.</td></tr></tbody></table>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی هنوز به Service Mesh نیاز نداریم؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر تعداد سرویس‌ها کم است، ارتباط‌ها ساده‌اند، مشاهده‌پذیری پایه‌ای داریم و مشکل جدی در مدیریت ترافیک داخلی نداریم، احتمالاً Service Mesh زود است. در چنین مرحله‌ای، ساده نگه داشتن معماری ارزشمندتر از افزودن یک لایه‌ی عملیاتی تازه است.</p></div></div></details>
<p>برای من، تفاوت اصلی این سه در محل درد است. اگر درد ما در تفاوت نیاز نماهاست، BFF می‌تواند کمک کند. اگر درد ما در ورودی سیستم است، یعنی کلاینت‌ها زیاد شده‌اند، احراز هویت و مسیریابی و کنترل درخواست‌ها پراکنده شده، Gateway قابل بررسی است. اگر درد ما در درون سیستم است، یعنی سرویس‌ها زیاد شده‌اند و گفت‌وگوی میان آن‌ها سخت، کند یا نامرئی شده، آن وقت Service Mesh معنا پیدا می‌کند.</p>
<p>پس باز هم همان قاعده‌ی کلی تکرار می‌شود: ابزار را از روی اسمش انتخاب نکنیم؛ از روی دردی انتخاب کنیم که واقعاً در سیستم پیدا شده است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-زبان-کسبوکار-در-کد-گم-میشود">وقتی زبان کسب‌وکار در کد گم می‌شود<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%B2%D8%A8%D8%A7%D9%86-%DA%A9%D8%B3%D8%A8%D9%88%DA%A9%D8%A7%D8%B1-%D8%AF%D8%B1-%DA%A9%D8%AF-%DA%AF%D9%85-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی زبان کسب‌وکار در کد گم می‌شود" title="لینک مستقیم به وقتی زبان کسب‌وکار در کد گم می‌شود" translate="no">​</a></h2>
<p>اوایل کار، ثبت سفارش شاید خیلی ساده به نظر برسد. کاربر چیزی را انتخاب می‌کند، پرداخت انجام می‌شود، سفارش ثبت می‌شود و تمام. در چنین مرحله‌ای، چند تابع ساده و چند مسیر روشن شاید کاملاً کافی باشند. هنوز نه وضعیت‌های زیادی داریم، نه قانون‌های ریز و درشت، نه استثناهایی که هر هفته تغییر کنند.</p>
<p>اما محصول که رشد می‌کند، سفارش دیگر فقط «سفارش» نیست. تخفیف اضافه می‌شود، لغو سفارش می‌آید، بازپرداخت مطرح می‌شود، موجودی باید کنترل شود، وضعیت پرداخت اهمیت پیدا می‌کند، پشتیبانی می‌خواهد بعضی چیزها را دستی تغییر دهد، و برای هرکدام هم چند قاعده‌ی کوچک اما مهم داریم. کم‌کم می‌بینیم چیزی که در ظاهر یک قابلیت ساده بود، در عمل تبدیل شده به مجموعه‌ای از تصمیم‌های کسب‌وکاری.</p>
<p>مشکل از جایی شروع می‌شود که این تصمیم‌ها در کد پخش می‌شوند. کمی از منطق سفارش در کنترلر است، کمی در مدل پایگاه داده، کمی در سرویس پرداخت، کمی در پنل مدیریت، و کمی هم در یک تابع کمکی قدیمی که معلوم نیست دقیقاً چرا نوشته شده است. حالا اگر بخواهیم یک قانون کوچک را تغییر دهیم، باید چند جا را بگردیم و امیدوار باشیم چیزی از قلم نیفتاده باشد.</p>
<p><img decoding="async" loading="lazy" alt="وقتی منطق دامنه در بخش‌های مختلف کد پخش می‌شود، فهمیدن و تغییر دادن رفتار سیستم سخت‌تر می‌شود" src="https://example.com/assets/images/domain-logic-scattered-across-code-50d1294f414c2908d20b8e9dd89c290a.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در این وضعیت، مشکل فقط زیاد شدن کد نیست؛ مشکل این است که زبان کسب‌وکار در میان جزئیات فنی گم شده است.</em></p>
<p>اینجاست که طراحی دامنه‌محور یا Domain Driven Design، که معمولاً DDD گفته می‌شود، معنا پیدا می‌کند. من DDD را پیش از آنکه یک مجموعه اصطلاح یا الگوی پیاده‌سازی بدانم، تلاشی برای جدی گرفتن زبان مسئله می‌فهمم. یعنی اگر در کسب‌وکار ما مفاهیمی مثل سفارش، پرداخت، بازپرداخت، تخفیف، موجودی و تسویه مهم‌اند، در کد هم باید جای روشن و قابل فهم داشته باشند.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>DDD می‌گوید کد نباید فقط بازتاب جدول‌ها، کنترلرها و مسیرهای فنی باشد. کد باید تا حد ممکن زبان مسئله را هم نشان دهد؛ همان واژه‌ها، همان قاعده‌ها و همان مرزهایی که اهل کسب‌وکار با آن‌ها فکر می‌کنند.</p></div></div>
<p>در نگاه دامنه‌محور، به جای اینکه قانون‌های مهم را در گوشه‌وکنار سیستم پخش کنیم، تلاش می‌کنیم هسته‌ی مسئله را بهتر بشناسیم و مدل کنیم. مثلاً سفارش فقط یک ردیف در جدول نیست؛ رفتاری دارد، وضعیت دارد، قاعده دارد. پرداخت فقط یک فراخوانی به سرویس بیرونی نیست؛ نتیجه، شکست، بازگشت و اثر روی سفارش دارد. تخفیف فقط یک عدد کم‌شده از قیمت نیست؛ قانون اعتبار، زمان، محدودیت و شرایط استفاده دارد.</p>
<p><img decoding="async" loading="lazy" alt="در مدل دامنه‌ی متمرکز، مفاهیم اصلی کسب‌وکار جای روشن‌تری دارند و لایه‌های بیرونی دور آن‌ها قرار می‌گیرند" src="https://example.com/assets/images/domain-model-centered-business-logic-e1f1a0328b0a504120f2f956e82b098a.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>وقتی مدل دامنه روشن‌تر باشد، تغییر دادن یک قانون کسب‌وکاری کمتر شبیه جست‌وجو در تاریکی می‌شود.</em></p>
<p>البته باید مراقب باشیم DDD را هم به یک نمایش معماری تبدیل نکنیم. طراحی دامنه‌محور یعنی از روز اول ده‌ها کلاس، اصطلاح، پوشه و مراسم پیچیده بسازیم؟ نه. اگر مسئله ساده است و قانون‌های کسب‌وکار هنوز کم و پایدارند، ساختار ساده می‌تواند کاملاً کافی باشد. DDD وقتی ارزشمند می‌شود که دامنه واقعاً پیچیده، تغییرپذیر و پرقاعده شده باشد؛ جایی که فهم مشترک از مسئله، خودش تبدیل به بخشی از کیفیت نرم‌افزار می‌شود.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یک پیوند نزدیک</div><div class="admonitionContent_hiHs"><p>این بحث به نقد ساده‌انگارانه از «مسئولیت واحد» هم نزدیک است. اگر فقط از روی اسم کلاس‌ها و تابع‌ها درباره‌ی مسئولیت حرف بزنیم، ممکن است مسئله‌ی اصلی را نبینیم. مسئولیت در عمل به این برمی‌گردد که تغییرها از کجا می‌آیند و کدام قاعده‌های کسب‌وکار باید کنار هم فهمیده و نگه‌داری شوند. من درباره‌ی این زاویه، جداگانه در نوشته‌ی <a class="" href="https://example.com/blog/solid-single-responsibility-critique">نقدی بر اصل مسئولیت واحد</a> حرف زده‌ام.</p></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>DDD یعنی هر پروژه‌ای را از روز اول با واژه‌های سنگین، لایه‌های زیاد و مدل‌های پیچیده شروع کنیم؟ نه. DDD یعنی وقتی مسئله‌ی کسب‌وکار جدی و پیچیده شد، اجازه ندهیم زبان آن در میان جزئیات فنی گم شود.</p></div></div>
<p>برای تشخیص اینکه هنوز ساختار ساده کافی است یا باید جدی‌تر به دامنه فکر کنیم، این مقایسه کمک می‌کند:</p>
<table><thead><tr><th>وضعیت</th><th>احتمالاً چه نگاهی بهتر است؟</th></tr></thead><tbody><tr><td>محصول تازه است و قانون‌های کسب‌وکار کم‌اند</td><td>ساده نگه داشتن ساختار کافی است.</td></tr><tr><td>چند مفهوم کسب‌وکاری مدام در حال تغییرند</td><td>باید نام‌ها و مرزهای دامنه را جدی‌تر بگیریم.</td></tr><tr><td>یک قانون در چند جای کد تکرار شده است</td><td>نشانه‌ی پخش شدن منطق دامنه است.</td></tr><tr><td>تغییر یک قاعده‌ی کوچک چند بخش نامرتبط را درگیر می‌کند</td><td>مدل دامنه احتمالاً جای روشن و متمرکزی ندارد.</td></tr><tr><td>تیم فنی و تیم کسب‌وکار از واژه‌های متفاوت برای یک چیز استفاده می‌کنند</td><td>نیاز به زبان مشترک جدی‌تر شده است.</td></tr></tbody></table>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه‌ی ساده که می‌گوید دامنه را خوب مدل نکرده‌ایم</summary><div><div class="collapsibleContent_T_pc"><p>اگر برای توضیح یک قانون کسب‌وکاری، مجبوریم اول مسیر کنترلر، شکل جدول، چند شرط پراکنده و چند تابع کمکی را توضیح دهیم، احتمالاً مدل دامنه‌ی ما به زبان مسئله نزدیک نیست. در چنین وضعیتی، کد شاید کار کند، اما فهم آن به مرور سخت و پرهزینه می‌شود.</p></div></div></details>
<p>برای من، DDD قبل از آنکه جواب آماده باشد، یک یادآوری مهم است: نرم‌افزار فقط با فریم‌ورک و پایگاه داده و API ساخته نمی‌شود؛ با فهم درست مسئله هم ساخته می‌شود. هرچه قواعد کسب‌وکار مهم‌تر و تغییرپذیرتر شوند، لازم است این فهم در خود کد هم دیده شود، نه فقط در ذهن چند نفر یا در چند سند پراکنده.</p>
<p>وقتی این را بپذیریم، پرسش بعدی طبیعی می‌شود: اگر منطق دامنه قلب سیستم است، چطور نگذاریم زیر فشار پایگاه داده، فریم‌ورک، API بیرونی یا ابزارهای دیگر له شود؟ این همان جایی است که معماری شش‌ضلعی وارد داستان می‌شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-قلب-سیستم-نباید-اسیر-ابزارهای-اطرافش-شود">وقتی قلب سیستم نباید اسیر ابزارهای اطرافش شود<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D9%82%D9%84%D8%A8-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D9%86%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A7%D8%B3%DB%8C%D8%B1-%D8%A7%D8%A8%D8%B2%D8%A7%D8%B1%D9%87%D8%A7%DB%8C-%D8%A7%D8%B7%D8%B1%D8%A7%D9%81%D8%B4-%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی قلب سیستم نباید اسیر ابزارهای اطرافش شود" title="لینک مستقیم به وقتی قلب سیستم نباید اسیر ابزارهای اطرافش شود" translate="no">​</a></h2>
<p>در بخش قبل گفتیم منطق دامنه باید جای روشن‌تری در کد داشته باشد. یعنی اگر سفارش، پرداخت، بازپرداخت و تخفیف برای کسب‌وکار مهم‌اند، نباید در کنترلرها، مدل‌های پایگاه داده و چند تابع پراکنده گم شوند. اما این فقط نصف ماجراست. حتی اگر مفاهیم دامنه را خوب پیدا کنیم، باز هم ممکن است آن‌ها را زیر آوار ابزارها دفن کنیم.</p>
<p>فرض کنیم می‌خواهیم قانون لغو سفارش را تغییر دهیم. از نظر کسب‌وکار، قانون شاید ساده باشد: اگر سفارش هنوز وارد مرحله‌ی ارسال نشده، لغو مجاز است؛ اگر ارسال آغاز شده، باید چند شرط دیگر بررسی شود. اما وقتی وارد کد می‌شویم، می‌بینیم برای آزمودن همین قانون ساده باید پایگاه داده بالا باشد، فریم‌ورک وب اجرا شود، سرویس پرداخت شبیه‌سازی شود، تنظیمات محیطی آماده باشد و چند جزئیات فنی دیگر هم همراهش بیاید. اینجا درد اصلی روشن می‌شود: منطق دامنه را داریم، اما هنوز آزاد نیست.</p>
<p><img decoding="async" loading="lazy" alt="وقتی منطق دامنه به ابزارهای بیرونی گره می‌خورد، تغییر و آزمون یک قانون ساده هم سخت می‌شود" src="https://example.com/assets/images/domain-logic-coupled-to-tools-be04d4ca6d4c7a60ea09fc807afd3ebe.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در این وضعیت، منطق سفارش در مرکز دیده می‌شود، اما از هر طرف به کنترلر، پایگاه داده، صف پیام، پنل مدیریت و سرویس پرداخت کشیده شده است.</em></p>
<p>معماری شش‌ضلعی یا Hexagonal Architecture پاسخی به همین فشار است. ایده‌ی اصلی‌اش این نیست که حتماً کد را به شکل یک شش‌ضلعی واقعی بچینیم یا از روز اول پوشه‌های زیاد بسازیم. حرف اصلی ساده‌تر است: هسته‌ی سیستم، یعنی منطق اصلی کسب‌وکار، باید تا حد ممکن مستقل از ابزارهای بیرونی بماند. پایگاه داده، فریم‌ورک وب، API پرداخت، صف پیام و رابط کاربری مهم‌اند، اما نباید شکل فکر کردن دامنه را تعیین کنند.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>معماری شش‌ضلعی می‌گوید دامنه باید در مرکز بماند و ابزارها از بیرون به آن وصل شوند. یعنی ابزارها باید در خدمت منطق دامنه باشند، نه اینکه منطق دامنه را شبیه خودشان کنند.</p></div></div>
<p>نسبت این بحث با طراحی دامنه‌محور مهم است. DDD کمک می‌کند بفهمیم «قلب مسئله» چیست و چه واژه‌ها و قاعده‌هایی برای کسب‌وکار مهم‌اند. معماری شش‌ضلعی کمک می‌کند از همان قلب محافظت کنیم تا با تغییر پایگاه داده، فریم‌ورک وب، سرویس پرداخت یا پیام‌رسان، منطق اصلی سیستم مدام زخمی نشود. پس این دو، دو جواب جدا برای یک مسئله نیستند؛ یکی بیشتر به فهم و مدل‌کردن دامنه کمک می‌کند، دیگری به مرزبندی و محافظت از آن مدل در برابر ابزارها.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>فرق DDD و معماری شش‌ضلعی</div><div class="admonitionContent_hiHs"><p>DDD بیشتر می‌پرسد: «مسئله‌ی اصلی کسب‌وکار چیست و با چه زبان و مدلی باید آن را بفهمیم؟» معماری شش‌ضلعی بیشتر می‌پرسد: «حالا که این مدل را جدی گرفتیم، چطور نگذاریم به دیتابیس، فریم‌ورک، API بیرونی و جزئیات زیرساختی گره بخورد؟»</p></div></div>
<p>برای توضیح ساده‌ی این معماری، دو واژه‌ی مهم داریم: درگاه و سازگارکننده. درگاه یا Port یعنی هسته‌ی سیستم می‌گوید من برای انجام کارم به چه قابلیتی نیاز دارم؛ مثلاً ذخیره‌ی سفارش، گرفتن نتیجه‌ی پرداخت، ارسال پیام یا انتشار یک رخداد. سازگارکننده یا Adapter یعنی بخش بیرونی می‌آید و آن نیاز را با ابزار واقعی برآورده می‌کند؛ مثلاً با PostgreSQL، Redis، Kafka، یک API پرداخت یا هر ابزار دیگری.</p>
<p><img decoding="async" loading="lazy" alt="در معماری شش‌ضلعی، هسته‌ی دامنه در مرکز می‌ماند و ابزارهای بیرونی از راه درگاه‌ها و سازگارکننده‌ها به آن وصل می‌شوند" src="https://example.com/assets/images/hexagonal-architecture-domain-core-ports-adapters-edbcd9ee2cc950949dbd90e948932f60.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در این تصویر، دامنه در مرکز است. رابط وب، پایگاه داده، صف پیام و API پرداخت دور آن قرار گرفته‌اند، نه در دل آن.</em></p>
<p>مثلاً هسته‌ی سفارش لازم نیست بداند داده‌ها دقیقاً در کدام جدول ذخیره می‌شوند. کافی است بگوید «من به چیزی نیاز دارم که سفارش را ذخیره کند». بعد یک سازگارکننده‌ی واقعی می‌تواند این نیاز را با پایگاه داده پیاده کند. اگر روزی روش ذخیره‌سازی عوض شد، نباید قانون لغو سفارش یا منطق بازپرداخت را از نو بنویسیم. تغییر باید بیشتر در لبه‌ی سیستم رخ دهد، نه در قلب آن.</p>
<p>این جداسازی چند فایده‌ی مهم دارد. آزمون منطق دامنه ساده‌تر می‌شود، چون برای بررسی یک قانون کسب‌وکاری لازم نیست همه‌ی ابزارهای بیرونی را بالا بیاوریم. تغییر ابزارها هم کم‌خطرتر می‌شود، چون هسته‌ی سیستم کمتر به جزئیات آن‌ها وابسته است. از همه مهم‌تر، مرز ذهنی سیستم روشن‌تر می‌شود: می‌فهمیم کدام بخش واقعاً منطق کسب‌وکار است و کدام بخش فقط راهی برای اتصال این منطق به جهان بیرون.</p>
<table><thead><tr><th>وضعیت</th><th>اگر مرزها مبهم باشند</th><th>اگر مرز شش‌ضلعی روشن‌تر باشد</th></tr></thead><tbody><tr><td>تغییر قانون لغو سفارش</td><td>درگیر کنترلر، دیتابیس و سرویس پرداخت می‌شویم.</td><td>بیشتر در هسته‌ی دامنه تغییر می‌دهیم.</td></tr><tr><td>آزمودن منطق بازپرداخت</td><td>به فریم‌ورک، دیتابیس و تنظیمات محیطی وابسته می‌شویم.</td><td>می‌توانیم منطق را با جایگزین‌های ساده‌تر بیازماییم.</td></tr><tr><td>تعویض ابزار ذخیره‌سازی</td><td>خطر دست‌زدن به منطق کسب‌وکار زیاد می‌شود.</td><td>تغییر بیشتر در سازگارکننده‌ی ذخیره‌سازی رخ می‌دهد.</td></tr><tr><td>اتصال به سرویس پرداخت جدید</td><td>ممکن است قواعد پرداخت در همه‌جا پخش شوند.</td><td>سازگارکننده‌ی تازه می‌تواند پشت همان درگاه بنشیند.</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>معماری شش‌ضلعی یعنی برای هر پروژه‌ی کوچک، از روز اول چندین لایه، درگاه، سازگارکننده و پوشه بسازیم؟ نه. اگر محصول هنوز ساده است و منطق کسب‌وکار کم و پایدار است، این حجم از جداسازی می‌تواند خودش هزینه‌ی اضافی بسازد. ارزش این معماری وقتی بیشتر می‌شود که دامنه مهم، ابزارها متنوع، و تغییرات بیرونی پرهزینه شده باشند.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه که می‌گوید مرز دامنه خوب محافظت نشده است</summary><div><div class="collapsibleContent_T_pc"><p>اگر برای تست یک قانون ساده‌ی کسب‌وکار باید وب‌سرور، پایگاه داده، صف پیام و چند سرویس بیرونی را همزمان درگیر کنیم، احتمالاً منطق دامنه بیش از حد به ابزارها چسبیده است. در چنین وضعیتی، مشکل فقط کندی تست نیست؛ مشکل این است که فهم و تغییر قلب سیستم به جزئیات بیرونی وابسته شده است.</p></div></div></details>
<p>برای من، معماری شش‌ضلعی ادامه‌ی طبیعی DDD است. DDD کمک می‌کند بفهمیم چه چیزی قلب مسئله است؛ معماری شش‌ضلعی کمک می‌کند آن قلب را از فشار ابزارها دور نگه داریم. نه چون ابزارها بی‌اهمیت‌اند، بلکه چون ابزارها باید قابل تعویض، قابل آزمون و در خدمت دامنه باشند.</p>
<p>وقتی هسته‌ی دامنه جای روشن‌تری پیدا می‌کند، پرسش بعدی هم آرام‌آرام پیدا می‌شود: آیا همیشه یک مدل واحد برای خواندن و نوشتن کافی است؟ یا گاهی فشار گزارش‌گیری، نمایش داده و تغییرات پیچیده باعث می‌شود مدل خواندن و نوشتن را جدا ببینیم؟ این همان جایی است که کم‌کم به CQRS نزدیک می‌شویم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-خواندن-و-نوشتن-نیازهای-متفاوت-پیدا-میکنند">وقتی خواندن و نوشتن نیازهای متفاوت پیدا می‌کنند<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%AE%D9%88%D8%A7%D9%86%D8%AF%D9%86-%D9%88-%D9%86%D9%88%D8%B4%D8%AA%D9%86-%D9%86%DB%8C%D8%A7%D8%B2%D9%87%D8%A7%DB%8C-%D9%85%D8%AA%D9%81%D8%A7%D9%88%D8%AA-%D9%BE%DB%8C%D8%AF%D8%A7-%D9%85%DB%8C%DA%A9%D9%86%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی خواندن و نوشتن نیازهای متفاوت پیدا می‌کنند" title="لینک مستقیم به وقتی خواندن و نوشتن نیازهای متفاوت پیدا می‌کنند" translate="no">​</a></h2>
<p>تا اینجا تلاش کرده‌ایم قلب سیستم را بهتر بشناسیم و از ابزارهای اطرافش جدا نگه داریم. با طراحی دامنه‌محور فهمیدیم سفارش، پرداخت، بازپرداخت و تخفیف فقط چند جدول نیستند؛ هرکدام رفتار و قاعده دارند. با معماری شش‌ضلعی هم گفتیم این منطق نباید به فریم‌ورک، پایگاه داده یا API بیرونی گره بخورد. اما وقتی سیستم بزرگ‌تر می‌شود، فشار تازه‌ای پیدا می‌شود: آیا همان مدلی که برای نوشتن و اجرای قانون مناسب است، برای خواندن، گزارش‌گیری و نمایش سریع هم مناسب می‌ماند؟</p>
<p>فرض کنیم برای ثبت سفارش، مدل نسبتاً خوبی داریم. وقتی کاربر سفارش ثبت می‌کند، موجودی بررسی می‌شود، وضعیت پرداخت درست پیش می‌رود، تخفیف اعتبارسنجی می‌شود و قانون‌های دامنه رعایت می‌شوند. این بخش از سیستم باید دقیق و سخت‌گیر باشد؛ چون دارد وضعیت واقعی سیستم را تغییر می‌دهد.</p>
<p>اما از سمت دیگر، نیازهای خواندن آرام‌آرام زیاد می‌شوند. برنامه‌ی موبایل فقط یک فهرست سبک و سریع از سفارش‌های کاربر می‌خواهد. پنل مدیریت می‌خواهد سفارش‌ها را بر اساس وضعیت پرداخت، شهر، روش ارسال، کد تخفیف، مبلغ، زمان ثبت و وضعیت پشتیبانی جست‌وجو کند. داشبورد مدیریتی تعداد سفارش‌های امروز، مبلغ کل، سفارش‌های لغوشده، میانگین زمان ارسال و نرخ بازپرداخت را می‌خواهد. این‌ها بیشتر از اینکه دنبال اجرای قانون باشند، دنبال نمای مناسب، سریع و قابل جست‌وجو از داده‌ها هستند.</p>
<p><img decoding="async" loading="lazy" alt="وقتی یک مدل واحد همزمان زیر فشار ثبت سفارش، پرداخت، لغو، گزارش، داشبورد و جست‌وجو قرار می‌گیرد" src="https://example.com/assets/images/single-model-under-read-write-pressure-39fcab7d1942c9fc97c8a026dc5b922c.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>یک مدل واحد می‌تواند مدتی پاسخ‌گو باشد، اما وقتی نیازهای خواندن و نوشتن از دو جهت متفاوت رشد می‌کنند، همان مدل کم‌کم زیر فشار می‌رود.</em></p>
<p>اینجاست که CQRS مطرح می‌شود. CQRS کوتاه‌شده‌ی Command Query Responsibility Segregation است و می‌توان آن را «جداسازی مسئولیت فرمان و پرس‌وجو» ترجمه کرد. فرمان یعنی کاری که وضعیت سیستم را تغییر می‌دهد؛ مثل ثبت سفارش، پرداخت، لغو سفارش یا بازپرداخت. پرس‌وجو یعنی کاری که فقط داده را می‌خواند؛ مثل نمایش فهرست سفارش‌ها، گرفتن جزئیات سفارش، ساختن گزارش یا نمایش داشبورد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>CQRS می‌گوید گاهی بهتر است مسیر نوشتن و مسیر خواندن را جدا ببینیم. نوشتن بیشتر درگیر درستی، قواعد دامنه و تغییر وضعیت است؛ خواندن بیشتر درگیر سرعت، شکل مناسب داده، جست‌وجو و نمایش است.</p></div></div>
<p>در ساده‌ترین برداشت، CQRS نمی‌گوید حتماً باید دو پایگاه داده، صف پیام، Kafka یا Event Sourcing داشته باشیم. گاهی جداسازی فقط در سطح کد است: یک مسیر برای فرمان‌ها و تغییر وضعیت، یک مسیر برای پرس‌وجوها و خواندن داده. در مرحله‌های پیچیده‌تر، ممکن است مدل خواندن جدا بسازیم؛ مثلاً جدولی آماده برای گزارش یا نمایی سبک برای موبایل. در سیستم‌های بزرگ‌تر، حتی ممکن است پایگاه داده‌ی خواندن و نوشتن هم جدا شوند. اما این‌ها پله‌های مختلف‌اند، نه تعریف اجباری CQRS.</p>
<p><img decoding="async" loading="lazy" alt="در CQRS مسیر فرمان‌ها برای نوشتن و تغییر وضعیت از مسیر پرس‌وجوها برای خواندن و نمایش جدا می‌شود" src="https://example.com/assets/images/cqrs-command-query-separation-1d26dd1e396d75f23f117940a1a4391d.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در این مدل، سمت فرمان مسئول تغییر وضعیت و رعایت قواعد دامنه است؛ سمت پرس‌وجو مسئول ساختن نماهای مناسب برای خواندن، گزارش و جست‌وجو.</em></p>
<p>نسبت CQRS با DDD و معماری شش‌ضلعی هم باید روشن باشد. DDD کمک می‌کند بفهمیم منطق دامنه چیست. معماری شش‌ضلعی کمک می‌کند این منطق را از ابزارهای بیرونی جدا نگه داریم. CQRS زمانی مطرح می‌شود که فشار خواندن و نوشتن از هم فاصله می‌گیرد. پس مسئله‌ی CQRS این نیست که «دامنه کجا باشد»، بلکه این است که «آیا مدل و مسیر خواندن باید همان مدل و مسیر نوشتن باشد؟»</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>فرق مسئله‌ها</div><div class="admonitionContent_hiHs"><p>DDD بیشتر درباره‌ی فهم و مدل‌کردن زبان کسب‌وکار است. معماری شش‌ضلعی درباره‌ی محافظت از هسته‌ی دامنه در برابر ابزارهاست. CQRS درباره‌ی تفاوت نیازهای خواندن و نوشتن است. این سه می‌توانند کنار هم بیایند، اما هرکدام به درد متفاوتی پاسخ می‌دهند.</p></div></div>
<p>این جداسازی می‌تواند چند فایده داشته باشد. مدل نوشتن می‌تواند تمیزتر و نزدیک‌تر به قواعد دامنه بماند، چون مجبور نیست همه‌ی نیازهای گزارش‌گیری و نمایش را هم در خود جا بدهد. مدل خواندن هم می‌تواند برای سرعت و شکل مناسب داده ساخته شود، بدون اینکه منطق اصلی سفارش و پرداخت را آلوده کند. برای مثال، ممکن است سمت نوشتن با مفهوم سفارش و وضعیت‌هایش کار کند، اما سمت خواندن یک نمای آماده داشته باشد که دقیقاً برای فهرست موبایل یا داشبورد مدیریتی طراحی شده است.</p>
<table><thead><tr><th>وضعیت</th><th>مدل واحد احتمالاً کافی است؟</th><th>CQRS چه زمانی ارزشمند می‌شود؟</th></tr></thead><tbody><tr><td>خواندن و نوشتن ساده و کم‌ترافیک‌اند</td><td>بله</td><td>معمولاً نیازی نیست.</td></tr><tr><td>گزارش‌ها و جست‌وجوها زیاد و پیچیده شده‌اند</td><td>شاید نه</td><td>وقتی کوئری‌ها مدل نوشتن را سنگین و آشفته می‌کنند.</td></tr><tr><td>مدل نوشتن پر از فیلدها و نیازهای نمایشی شده است</td><td>نه همیشه</td><td>وقتی نیازهای خواندن دارند منطق دامنه را آلوده می‌کنند.</td></tr><tr><td>خواندن باید بسیار سریع و متناسب با نماهای مختلف باشد</td><td>نه همیشه</td><td>وقتی نمای خواندن جدا می‌تواند فشار را کم کند.</td></tr><tr><td>داده‌ی خواندن ممکن است کمی با تأخیر به‌روز شود</td><td>بستگی دارد</td><td>وقتی تأخیر کوتاه پذیرفتنی است، جداسازی آسان‌تر می‌شود.</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>CQRS یعنی حتماً معماری بزرگ، دو پایگاه داده، Event Sourcing و کلی زیرساخت تازه؟ نه. CQRS از یک ایده‌ی ساده شروع می‌شود: فرمان و پرس‌وجو نیازهای متفاوتی دارند. اینکه این جداسازی فقط در کد باشد یا تا سطح پایگاه داده و پیام‌رسان جلو برود، به اندازه و درد واقعی سیستم بستگی دارد.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه که می‌گوید شاید CQRS زود است</summary><div><div class="collapsibleContent_T_pc"><p>اگر هنوز گزارش‌ها ساده‌اند، ترافیک خواندن و نوشتن پایین است، و همان مدل داده بدون فشار جدی هم برای تغییر وضعیت و هم برای نمایش کافی است، آوردن CQRS احتمالاً زود است. در این مرحله، جداسازی بیش از حد می‌تواند فهم سیستم را سخت‌تر کند و هزینه‌ی نگه‌داری را بالا ببرد.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه که می‌گوید شاید وقت فکر کردن به CQRS رسیده است</summary><div><div class="collapsibleContent_T_pc"><p>اگر هر نیاز نمایشی تازه باعث تغییر در مدل نوشتن می‌شود، گزارش‌ها کوئری‌های سنگین و شکننده می‌سازند، پنل مدیریت و داشبوردها مدام شکل متفاوتی از داده می‌خواهند، و منطق دامنه کم‌کم با نیازهای خواندن قاطی شده است، احتمالاً باید دست‌کم به جداسازی مسیر فرمان و پرس‌وجو فکر کنیم.</p></div></div></details>
<p>برای من، CQRS یک یادآوری مهم است: همه‌ی نیازهای سیستم از یک جنس نیستند. ثبت سفارش و لغو سفارش باید دقیق، قانون‌مند و محافظه‌کار باشد. فهرست سفارش‌ها و گزارش مدیریتی باید سریع، قابل جست‌وجو و مناسب نمایش باشد. اگر این دو نیاز هنوز ساده‌اند، یک مدل واحد کافی است. اما وقتی از هم فاصله گرفتند، جدا دیدن آن‌ها می‌تواند سیستم را قابل فهم‌تر و قابل تغییرتر کند.</p>
<p>این جداسازی آرام‌آرام ما را به یک سؤال بعدی می‌رساند: اگر تغییرهای مهم سیستم را فقط به‌صورت وضعیت نهایی ذخیره نکنیم و خود رخدادهای مهم را هم نگه داریم چه می‌شود؟ اینجا وارد بحث Event Sourcing می‌شویم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-سیستم-باید-خبر-بدهد-نه-اینکه-همه-را-مستقیم-صدا-بزند">وقتی سیستم باید خبر بدهد، نه اینکه همه را مستقیم صدا بزند<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%AE%D8%A8%D8%B1-%D8%A8%D8%AF%D9%87%D8%AF-%D9%86%D9%87-%D8%A7%DB%8C%D9%86%DA%A9%D9%87-%D9%87%D9%85%D9%87-%D8%B1%D8%A7-%D9%85%D8%B3%D8%AA%D9%82%DB%8C%D9%85-%D8%B5%D8%AF%D8%A7-%D8%A8%D8%B2%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی سیستم باید خبر بدهد، نه اینکه همه را مستقیم صدا بزند" title="لینک مستقیم به وقتی سیستم باید خبر بدهد، نه اینکه همه را مستقیم صدا بزند" translate="no">​</a></h2>
<p>تا اینجا چند بار با یک الگوی تکرارشونده روبه‌رو شدیم: هرچه محصول رشد می‌کند، یک تصمیم ساده‌ی دیروز کم‌کم زیر فشار نیازهای تازه قرار می‌گیرد. در CQRS گفتیم خواندن و نوشتن ممکن است نیازهای متفاوتی پیدا کنند. حالا می‌خواهیم از زاویه‌ی دیگری به رشد سیستم نگاه کنیم: وقتی یک اتفاق در سیستم می‌افتد، چند بخش دیگر باید از آن باخبر شوند؟</p>
<p>فرض کنیم کاربر سفارشی ثبت می‌کند و پرداخت هم موفق می‌شود. در نگاه ساده، شاید بگوییم سفارش ثبت شد و تمام. اما در محصول واقعی، این اتفاق فقط برای سرویس سفارش مهم نیست. موجودی باید کم شود، برای کاربر اعلان فرستاده شود، داشبورد فروش باید به‌روز شود، گزارش مالی بعداً باید آن را حساب کند، و شاید سیستم امتیازدهی هم بخواهد به رفتار کاربر واکنش نشان دهد.</p>
<p>اگر سرویس سفارش بخواهد همه‌ی این کارها را خودش و به‌صورت مستقیم انجام دهد، خیلی زود گرفتار زنجیره‌ای از وابستگی‌ها می‌شود. باید سرویس اعلان را بشناسد، سرویس موجودی را صدا بزند، گزارش مالی را خبر کند، داشبورد را به‌روز کند و با سیستم امتیازدهی هم هماهنگ شود. حالا اگر یکی از این بخش‌ها کند یا خراب شود، مسیر اصلی ثبت سفارش هم ممکن است آسیب ببیند.</p>
<p><img decoding="async" loading="lazy" alt="قبل از معماری رویدادمحور، سرویس سفارش برای انجام واکنش‌های بعدی مستقیم به چند بخش دیگر وصل می‌شود" src="https://example.com/assets/images/direct-service-calls-after-order-created-b7d1f9e9d159c445de965da3af372c8d.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>وقتی یک سرویس مجبور است همه‌ی واکنش‌های بعد از یک اتفاق را خودش مدیریت کند، وابستگی‌ها زیاد و شکننده می‌شوند.</em></p>
<p>معماری رویدادمحور یا Event-Driven Architecture از همین‌جا معنا پیدا می‌کند. ایده‌اش این است که به‌جای اینکه سرویس سفارش همه را مستقیم صدا بزند، فقط یک خبر رسمی منتشر کند: «سفارش ثبت شد». بعد هر بخشی که به این اتفاق علاقه دارد، واکنش خودش را انجام می‌دهد. سرویس اعلان پیام می‌فرستد، سرویس موجودی مقدار کالا را کم می‌کند، داشبورد داده‌ی خودش را به‌روز می‌کند و گزارش مالی هم مسیر خودش را می‌رود.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>در معماری رویدادمحور، یک بخش لازم نیست همه‌ی واکنش‌های بعد از یک اتفاق را مستقیم مدیریت کند. کافی است رخداد مهم را منتشر کند و بخش‌های علاقه‌مند، مستقل‌تر به آن واکنش نشان دهند.</p></div></div>
<p>رویداد با دستور مستقیم فرق دارد. وقتی می‌گوییم «سفارش ثبت شد»، داریم درباره‌ی چیزی خبر می‌دهیم که قبلاً رخ داده است. اما وقتی می‌گوییم «به کاربر پیامک بفرست»، داریم به یک بخش دیگر دستور می‌دهیم کاری انجام دهد. این تفاوت کوچک، در طراحی سیستم مهم است. رخداد، سرویس منتشرکننده را از دانستن جزئیات واکنش‌های بعدی آزادتر می‌کند.</p>
<p><img decoding="async" loading="lazy" alt="در معماری رویدادمحور، سرویس سفارش رویداد ثبت سفارش را منتشر می‌کند و چند مصرف‌کننده مستقل به آن واکنش نشان می‌دهند" src="https://example.com/assets/images/event-driven-order-created-consumers-af3da53f27f8e2b4ce9eab662d9655e5.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>سرویس سفارش لازم نیست همه‌ی مصرف‌کننده‌ها را بشناسد؛ فقط خبر رسمی اتفاق را منتشر می‌کند.</em></p>
<p>برای رساندن این رخدادها معمولاً یک پیام‌رسان یا واسط ارتباطی در میان قرار می‌گیرد؛ چیزی که پیام را از تولیدکننده می‌گیرد و به مصرف‌کننده‌ها می‌رساند. در این بخش لازم نیست وارد جزئیات ابزارهایی مثل Kafka یا RabbitMQ شویم؛ فعلاً همین قدر کافی است که بدانیم پیام‌رسان کمک می‌کند تولیدکننده‌ی رخداد و مصرف‌کننده‌های آن کمتر به هم قفل شوند. جزئیات صف پیام، الگوهای مصرف، تکرار پیام و تفاوت ابزارها را جداگانه در بخش Message Queue باز می‌کنیم.</p>
<p>نکته‌ی مهم این است که معماری رویدادمحور فقط مزیت نیست؛ پیچیدگی تازه هم می‌آورد. در مدل مستقیم، مسیر اجرا معمولاً واضح‌تر است: این سرویس آن سرویس را صدا زد و پاسخ گرفت. اما در مدل رویدادمحور، یک رخداد منتشر می‌شود و چند مصرف‌کننده بعداً واکنش نشان می‌دهند. ممکن است پیام دیر برسد، دوباره برسد، مصرف‌کننده‌ای موقتاً از کار بیفتد، یا فهمیدن مسیر کامل یک اتفاق سخت‌تر شود. پس آزادی بیشتر، با مسئولیت عملیاتی بیشتر همراه است.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>رویدادمحور کردن سیستم یعنی هر تغییر کوچک را تبدیل به رخداد کنیم و همه‌چیز را از مسیر پیام‌ها بگذرانیم؟ نه. اگر سیستم کوچک است و واکنش‌ها ساده و کم‌اند، فراخوانی مستقیم می‌تواند خواناتر و کم‌هزینه‌تر باشد. معماری رویدادمحور وقتی ارزشمند می‌شود که یک اتفاق برای چند بخش مهم باشد و نخواهیم همه‌ی آن بخش‌ها مستقیم و هم‌زمان به هم گره بخورند.</p></div></div>
<p>اینجا بد نیست خیلی کوتاه مرز CDC را هم روشن کنیم. گرفتن تغییرات داده یا Change Data Capture، که معمولاً CDC گفته می‌شود، یعنی تغییرهای پایگاه داده را دنبال کنیم و آن‌ها را به جریان پیام یا رخداد تبدیل کنیم. مثلاً وقتی ردیفی در جدول سفارش‌ها اضافه می‌شود یا وضعیت سفارشی تغییر می‌کند، CDC می‌تواند این تغییر را بخواند و برای بخش‌های دیگر منتشر کند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>CDC کجای داستان می‌نشیند؟</div><div class="admonitionContent_hiHs"><p>در معماری رویدادمحور، رخداد می‌تواند آگاهانه از دل منطق دامنه منتشر شود؛ مثلاً سرویس سفارش بعد از ثبت موفق سفارش، رخداد «سفارش ثبت شد» را منتشر کند. CDC اما معمولاً از تغییرهای پایگاه داده خبر می‌سازد. پس CDC می‌تواند راهی عملی برای وصل کردن سیستم‌های قدیمی یا داده‌محور به جریان پیام‌ها باشد، اما همان Event Sourcing نیست.</p></div></div>
<p>برای اینکه مرز این مفهوم‌ها با بخش‌های قبلی و بعدی روشن بماند، می‌شود این‌طور نگاه کرد:</p>
<table><thead><tr><th>مفهوم</th><th>پرسش اصلی</th><th>چیزی که در این بخش نباید با آن قاطی شود</th></tr></thead><tbody><tr><td>CQRS</td><td>آیا خواندن و نوشتن نیازهای متفاوتی دارند؟</td><td>قرار نیست هر جداسازی خواندن و نوشتن حتماً رویدادمحور باشد.</td></tr><tr><td>معماری رویدادمحور</td><td>وقتی اتفاقی افتاد، چه بخش‌هایی باید باخبر شوند؟</td><td>قرار نیست رخدادها الزاماً منبع اصلی حقیقت باشند.</td></tr><tr><td>CDC</td><td>اگر تغییر در پایگاه داده رخ داد، چطور دیگران باخبر شوند؟</td><td>CDC با رخداد دامنه و Event Sourcing یکی نیست.</td></tr><tr><td>Event Sourcing</td><td>اگر خود رخدادها منبع اصلی حقیقت باشند چه؟</td><td>این پرسش را در بخش بعدی باز می‌کنیم.</td></tr></tbody></table>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه که می‌گوید شاید معماری رویدادمحور کمک کند</summary><div><div class="collapsibleContent_T_pc"><p>اگر بعد از یک اتفاق مهم، چند بخش مستقل باید واکنش نشان دهند و اضافه‌کردن هر واکنش تازه باعث تغییر در سرویس اصلی می‌شود، احتمالاً وابستگی‌ها زیادی مستقیم شده‌اند. در این نقطه، انتشار رخداد می‌تواند کمک کند سرویس اصلی فقط خبر اتفاق را بدهد و واکنش‌های بعدی در بخش‌های جدا انجام شوند.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>یک نشانه که می‌گوید شاید هنوز زود است</summary><div><div class="collapsibleContent_T_pc"><p>اگر فقط دو بخش ساده با هم حرف می‌زنند، مسیر اجرا باید کاملاً هم‌زمان و قابل پیش‌بینی باشد، و پیچیدگی عملیاتی پیام‌رسان برای تیم سنگین است، رویدادمحوری کامل ممکن است بیشتر از آنکه کمک کند، فهم و عیب‌یابی سیستم را سخت کند.</p></div></div></details>
<p>برای من، معماری رویدادمحور یعنی پذیرفتن اینکه بعضی اتفاق‌ها فقط متعلق به یک بخش نیستند. سفارش ثبت می‌شود، اما اعلان، موجودی، داشبورد و گزارش هم از آن تأثیر می‌گیرند. به‌جای اینکه سرویس سفارش همه را مستقیم بشناسد، می‌تواند خبر رسمی اتفاق را منتشر کند و بقیه‌ی بخش‌ها مستقل‌تر واکنش نشان دهند.</p>
<p>تا اینجا رخدادها را مثل خبرهایی دیدیم که بین بخش‌های سیستم جابه‌جا می‌شوند. اما اگر این رخدادها فقط پیام گذرا نباشند و خودشان منبع اصلی حقیقت سیستم شوند چه؟ این پرسش ما را به Event Sourcing می‌رساند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-پیامها-باید-امن-و-قابلاعتماد-جابهجا-شوند">وقتی پیام‌ها باید امن و قابل‌اعتماد جابه‌جا شوند<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D9%BE%DB%8C%D8%A7%D9%85%D9%87%D8%A7-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A7%D9%85%D9%86-%D9%88-%D9%82%D8%A7%D8%A8%D9%84%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF-%D8%AC%D8%A7%D8%A8%D9%87%D8%AC%D8%A7-%D8%B4%D9%88%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی پیام‌ها باید امن و قابل‌اعتماد جابه‌جا شوند" title="لینک مستقیم به وقتی پیام‌ها باید امن و قابل‌اعتماد جابه‌جا شوند" translate="no">​</a></h2>
<p>در بخش قبل گفتیم معماری رویدادمحور کمک می‌کند سرویس‌ها به‌جای صدا زدن مستقیم یکدیگر، خبر اتفاق‌های مهم را منتشر کنند. سرویس سفارش لازم نیست خودش اعلان بفرستد، موجودی کم کند، داشبورد را به‌روز کند و گزارش مالی بسازد. می‌تواند بگوید «سفارش ثبت شد» و بخش‌های علاقه‌مند، هرکدام واکنش خودشان را انجام دهند.</p>
<p>اما این حرف یک پرسش عملی مهم را باز می‌گذارد: این پیام‌ها قرار است از کجا عبور کنند؟ اگر سرویس اعلان لحظه‌ای از کار افتاده باشد چه؟ اگر سرویس گزارش مالی کندتر از بقیه پردازش کند چه؟ اگر پیام به مصرف‌کننده رسید اما پردازش آن شکست خورد چه؟ اینجاست که صف پیام یا Message Queue وارد داستان می‌شود.</p>
<p>صف پیام را می‌توان مثل یک فضای میانی میان تولیدکننده و مصرف‌کننده دید. تولیدکننده پیام را در صف می‌گذارد و لازم نیست منتظر بماند همه‌ی مصرف‌کننده‌ها همان لحظه کارشان را انجام دهند. مصرف‌کننده‌ها هم با سرعت و توان خودشان پیام‌ها را می‌خوانند و پردازش می‌کنند. این فاصله‌ی کوچک، در سیستم‌های بزرگ ارزش زیادی دارد؛ چون دو طرف را از وضعیت لحظه‌ای هم آزادتر می‌کند.</p>
<p><img decoding="async" loading="lazy" alt="صف پیام تولیدکننده را از مصرف‌کننده‌ها جدا می‌کند و پیام‌ها را برای پردازش مستقل نگه می‌دارد" src="https://example.com/assets/images/message-queue-decouples-producer-consumers-f5c41cd6b604296a6fc39693a8a75939.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>تولیدکننده پیام را در صف می‌گذارد و مصرف‌کننده‌ها مستقل از هم آن را پردازش می‌کنند.</em></p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>صف پیام کمک می‌کند تولیدکننده و مصرف‌کننده کمتر به زمان، سرعت و وضعیت لحظه‌ای هم وابسته باشند. تولیدکننده پیام را تحویل می‌دهد؛ مصرف‌کننده‌ها بعداً آن را می‌خوانند، پردازش می‌کنند و نتیجه‌ی کار خودشان را جلو می‌برند.</p></div></div>
<p>در ساده‌ترین شکل، چند نقش اصلی داریم. تولیدکننده یا Producer بخشی است که پیام را می‌فرستد؛ مثلاً سرویس سفارش. مصرف‌کننده یا Consumer بخشی است که پیام را می‌خواند و کاری انجام می‌دهد؛ مثلاً سرویس اعلان یا گزارش مالی. خود صف یا موضوع، جایی است که پیام‌ها در آن قرار می‌گیرند. بعضی ابزارها بیشتر با واژه‌ی صف شناخته می‌شوند، بعضی با مفهوم جریان یا موضوع، اما ایده‌ی پایه یکی است: پیام‌ها باید جایی قرار بگیرند تا بین بخش‌های سیستم جابه‌جا شوند.</p>
<p>اینجا تفاوت Message Queue با معماری رویدادمحور هم مهم است. معماری رویدادمحور می‌گوید بخش‌های سیستم می‌توانند با خبر دادن درباره‌ی اتفاق‌ها با هم هماهنگ شوند. صف پیام بیشتر درباره‌ی زیرساخت رساندن آن خبرهاست. یعنی EDA بیشتر سبک طراحی ارتباط است، اما Message Queue ابزار و سازوکاری است برای اینکه این ارتباط عملی‌تر، قابل تحمل‌تر و قابل بازیابی‌تر شود.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>فرق با بخش قبل</div><div class="admonitionContent_hiHs"><p>در بخش Event-Driven Architecture پرسیدیم «وقتی اتفاقی افتاد، چه بخش‌هایی باید باخبر شوند؟» در این بخش می‌پرسیم «این خبر چطور قابل اعتماد جابه‌جا شود، اگر مصرف‌کننده کند بود یا پردازش شکست خورد چه کنیم؟»</p></div></div>
<p>یکی از نکته‌های مهم در صف پیام، تأیید پردازش است. مصرف‌کننده معمولاً بعد از اینکه پیام را گرفت و کارش را انجام داد، به صف اعلام می‌کند که پیام با موفقیت پردازش شده است. اگر پردازش شکست بخورد، سیستم می‌تواند پیام را دوباره برای تلاش بعدی نگه دارد یا بعد از چند بار شکست، آن را به جای جداگانه‌ای بفرستد تا بعداً بررسی شود. به این جای جداگانه معمولاً صف پیام‌های مشکل‌دار یا Dead Letter Queue گفته می‌شود.</p>
<p><img decoding="async" loading="lazy" alt="وقتی پردازش پیام شکست می‌خورد، سیستم می‌تواند تلاش مجدد انجام دهد یا پیام را به صف پیام‌های مشکل‌دار بفرستد" src="https://example.com/assets/images/message-queue-retry-and-dead-letter-16221191e8d85a3e57015d02bb10a8ea.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>همه‌ی خطاها نباید مسیر اصلی را متوقف کنند؛ بعضی پیام‌ها می‌توانند دوباره پردازش شوند یا برای بررسی جدا شوند.</em></p>
<p>اما صف پیام هم جادو نمی‌کند. وقتی پیام‌ها غیرهم‌زمان جابه‌جا می‌شوند، باید با چند واقعیت کنار بیاییم. ممکن است یک پیام بیش از یک بار به مصرف‌کننده برسد، پس مصرف‌کننده باید تا حد ممکن در برابر پردازش تکراری مقاوم باشد. ممکن است ترتیب پیام‌ها مهم باشد، پس باید بدانیم ابزار و طراحی ما چه تضمینی درباره‌ی ترتیب می‌دهد. ممکن است مصرف‌کننده عقب بماند و صف بزرگ شود. ممکن است پیام‌ها در یک بخش پردازش شوند و در بخش دیگر هنوز نه، و این یعنی سیستم برای مدتی در وضعیت کاملاً هم‌زمان و یکدست نیست.</p>
<table><thead><tr><th>مسئله</th><th>پرسشی که باید از خودمان بپرسیم</th></tr></thead><tbody><tr><td>پیام تکراری</td><td>اگر همین پیام دوبار برسد، آیا نتیجه خراب می‌شود؟</td></tr><tr><td>ترتیب پیام‌ها</td><td>آیا ترتیب رخدادها برای این مصرف‌کننده مهم است؟</td></tr><tr><td>شکست پردازش</td><td>بعد از شکست، پیام باید دوباره امتحان شود یا کنار گذاشته شود؟</td></tr><tr><td>عقب‌ماندن مصرف‌کننده</td><td>اگر مصرف‌کننده کند شد، صف چقدر می‌تواند رشد کند؟</td></tr><tr><td>مشاهده‌پذیری</td><td>از کجا بفهمیم پیام کجا گیر کرده یا چند بار شکست خورده است؟</td></tr></tbody></table>
<p>در این فضا نام ابزارهایی مثل RabbitMQ و Kafka زیاد شنیده می‌شود. برای فهم اولیه، می‌شود خیلی ساده گفت RabbitMQ بیشتر با الگوی صف، تحویل پیام و پردازش کارها شناخته می‌شود. Kafka بیشتر شبیه یک جریان پایدار از پیام‌ها و رخدادهاست که پیام‌ها را برای مدتی نگه می‌دارد و مصرف‌کننده‌های مختلف می‌توانند از آن بخوانند. این توضیح کامل نیست، اما برای جایگاه ذهنی کافی است: هر دو برای جابه‌جایی پیام استفاده می‌شوند، اما نگاه و نقطه‌ی قوتشان یکسان نیست.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>صف پیام یعنی سیستم خودبه‌خود ساده‌تر و مطمئن‌تر می‌شود؟ نه. صف پیام وابستگی مستقیم را کمتر می‌کند و تحمل خطا را بهتر می‌کند، اما عیب‌یابی، پایش، ترتیب پیام‌ها، پیام‌های تکراری و وضعیت‌های نیمه‌کاره را هم وارد داستان می‌کند.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی هنوز به صف پیام نیاز نداریم؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر دو بخش ساده با هم ارتباط مستقیم دارند، پاسخ هم‌زمان لازم است، ترافیک پایین است و شکست یک بخش مسیر پیچیده‌ای ایجاد نمی‌کند، اضافه کردن صف پیام ممکن است زود باشد. در چنین مرحله‌ای، ارتباط مستقیم شاید خواناتر و کم‌هزینه‌تر باشد.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی صف پیام ارزشمندتر می‌شود؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر یک اتفاق باید چند مصرف‌کننده‌ی مستقل را خبر کند، مصرف‌کننده‌ها سرعت‌های متفاوت دارند، بعضی کارها می‌توانند با تأخیر انجام شوند، یا نمی‌خواهیم خطای یک مصرف‌کننده مسیر اصلی را خراب کند، صف پیام می‌تواند کمک کند. البته به شرطی که برای پایش، تلاش مجدد و پیام‌های مشکل‌دار هم فکر کرده باشیم.</p></div></div></details>
<p>برای من، صف پیام یعنی پذیرفتن اینکه همه‌ی بخش‌های سیستم لازم نیست هم‌زمان و مستقیم به هم قفل باشند. بعضی کارها می‌توانند کمی دیرتر، مستقل‌تر و با امکان تلاش دوباره انجام شوند. این استقلال ارزشمند است، اما فقط وقتی که هزینه‌های عملیاتی آن را هم بپذیریم.</p>
<p>تا اینجا فقط درباره‌ی مسیر جابه‌جایی پیام‌ها حرف زدیم: پیام از کجا عبور کند، اگر مصرف‌کننده کند بود چه شود، اگر پردازش شکست خورد چطور دوباره تلاش کنیم، و پیام‌های مشکل‌دار را کجا نگه داریم. بخش بعدی یک پرسش جداست: گاهی خود رخدادها فقط پیام عبوری نیستند و برای فهم تاریخچه‌ی سیستم اهمیت پیدا می‌کنند. آنجا وارد Event Sourcing می‌شویم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-فقط-وضعیت-فعلی-کافی-نیست">وقتی فقط وضعیت فعلی کافی نیست<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D9%81%D9%82%D8%B7-%D9%88%D8%B6%D8%B9%DB%8C%D8%AA-%D9%81%D8%B9%D9%84%DB%8C-%DA%A9%D8%A7%D9%81%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به وقتی فقط وضعیت فعلی کافی نیست" title="لینک مستقیم به وقتی فقط وضعیت فعلی کافی نیست" translate="no">​</a></h2>
<p>در بخش‌های قبل، رخدادها را از دو زاویه دیدیم. در معماری رویدادمحور، رخداد راهی بود برای خبر دادن به بخش‌های دیگر سیستم. در صف پیام، درباره‌ی این حرف زدیم که پیام‌ها و رخدادها چطور میان تولیدکننده و مصرف‌کننده جابه‌جا شوند. اما Event Sourcing پرسش دیگری دارد: اگر خود رخدادها فقط پیام عبوری نباشند و تاریخچه‌ی رسمی سیستم را بسازند چه؟</p>
<p>فرض کنیم کاربری به پشتیبانی پیام می‌دهد و می‌گوید: «من سفارشم را لغو کردم، ولی چرا فقط بخشی از پولم برگشته؟» پشتیبانی وارد پنل می‌شود و فقط یک چیز می‌بیند: وضعیت سفارش «لغوشده» است. این اطلاعات بد نیست، اما کافی هم نیست. برای فهمیدن ماجرا باید بدانیم سفارش چه زمانی ثبت شد، پرداخت چه زمانی انجام شد، لغو قبل از ارسال بود یا بعد از آن، لغو را خود کاربر انجام داد یا پشتیبانی، قانون بازپرداخت در آن لحظه چه بوده، و آیا بازپرداخت کامل شده یا فقط آغاز شده است.</p>
<p>اینجا مشکل روشن می‌شود: وضعیت فعلی فقط آخر داستان را نشان می‌دهد، نه مسیر رسیدن به آن را.</p>
<p><img decoding="async" loading="lazy" alt="وقتی فقط وضعیت فعلی سفارش را داریم، بسیاری از پرسش‌های پشتیبانی و حسابرسی بی‌پاسخ می‌مانند" src="https://example.com/assets/images/current-state-loses-history-6fda74826a7f9630bc8f3cbae72bbfd6.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>اگر فقط بدانیم سفارش «لغوشده» است، هنوز نمی‌دانیم چه اتفاق‌هایی باعث رسیدن سفارش به این وضعیت شده‌اند.</em></p>
<p>در مدل معمول، بیشتر با وضعیت فعلی کار می‌کنیم. مثلاً در جدول سفارش، یک فیلد داریم که می‌گوید سفارش الان پرداخت‌شده، ارسال‌شده یا لغوشده است. اما در Event Sourcing، منبع اصلی حقیقت فقط وضعیت نهایی نیست؛ رخدادهایی است که در طول زمان اتفاق افتاده‌اند. یعنی به جای اینکه فقط بگوییم سفارش الان چه وضعیتی دارد، تاریخچه‌ی اتفاق‌ها را نگه می‌داریم.</p>
<p>برای یک سفارش، این رخدادها می‌توانند چنین چیزهایی باشند: سفارش ثبت شد، پرداخت انجام شد، ارسال آغاز شد، سفارش لغو شد، بازپرداخت صادر شد. بعد وضعیت فعلی سفارش از روی همین رخدادها ساخته می‌شود. در این نگاه، وضعیت فعلی نتیجه‌ی تاریخچه است؛ نه جایگزین تاریخچه.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>در Event Sourcing، رخدادها منبع اصلی حقیقت‌اند. یعنی سیستم می‌تواند وضعیت فعلی را با خواندن و اعمال کردن رخدادهای گذشته بازسازی کند.</p></div></div>
<p><img decoding="async" loading="lazy" alt="در Event Sourcing وضعیت فعلی از روی زنجیره‌ای از رخدادهای ذخیره‌شده ساخته می‌شود" src="https://example.com/assets/images/event-sourcing-events-build-current-state-bd409d9e9e573d97df968fcb13f1b9b6.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>رخدادها فقط خبرهای پراکنده نیستند؛ تاریخچه‌ای هستند که وضعیت فعلی از روی آن‌ها ساخته می‌شود.</em></p>
<p>مرز این مفهوم با فصل‌های قبلی مهم است. در معماری رویدادمحور، شاید رخداد را برای خبر دادن به سرویس‌های دیگر منتشر کنیم. در صف پیام، درباره‌ی رساندن و پردازش همین پیام‌ها حرف زدیم. اما در Event Sourcing، رخدادها نقش عمیق‌تری دارند: آن‌ها حافظه‌ی رسمی سیستم‌اند. حتی اگر هیچ سرویس دیگری رخدادها را مصرف نکند، خود سیستم می‌تواند برای بازسازی وضعیت، حسابرسی یا تحلیل خطا به آن‌ها تکیه کند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>فرق با رخدادمحوری و صف پیام</div><div class="admonitionContent_hiHs"><p>رخدادمحوری می‌پرسد «چه بخش‌هایی باید از یک اتفاق باخبر شوند؟» صف پیام می‌پرسد «این خبر چطور قابل اعتماد جابه‌جا شود؟» Event Sourcing می‌پرسد «آیا خود رخدادها منبع حقیقت و تاریخچه‌ی رسمی سیستم هستند؟»</p></div></div>
<p>Event Sourcing را نباید با لاگ معمولی هم یکی گرفت. لاگ عملیاتی معمولاً برای مشاهده، عیب‌یابی و فهم رفتار سیستم نوشته می‌شود. اما رخداد در Event Sourcing بخشی از مدل اصلی سیستم است. اگر رخدادهای سفارش را از دست بدهیم، فقط چند خط گزارش را از دست نداده‌ایم؛ بخشی از حقیقت سیستم را از دست داده‌ایم.</p>
<p>همچنین Event Sourcing الزاماً همان CQRS نیست. ممکن است سیستمی CQRS داشته باشد، اما رخدادها را منبع حقیقت نداند. ممکن است Event Sourcing داشته باشیم و بعد برای خواندن سریع‌تر، مدل‌های خواندن جدا بسازیم؛ در این حالت این دو کنار هم می‌آیند. اما یکی تعریف دیگری نیست. CQRS درباره‌ی جداسازی خواندن و نوشتن است؛ Event Sourcing درباره‌ی این است که وضعیت از روی رخدادهای ذخیره‌شده ساخته شود.</p>
<table><thead><tr><th>مفهوم</th><th>پرسش اصلی</th><th>اشتباه رایج</th></tr></thead><tbody><tr><td>CQRS</td><td>آیا خواندن و نوشتن نیازهای متفاوتی دارند؟</td><td>فکر کنیم هر CQRS یعنی Event Sourcing.</td></tr><tr><td>معماری رویدادمحور</td><td>چه بخش‌هایی باید از اتفاق‌ها باخبر شوند؟</td><td>فکر کنیم هر رخداد منتشرشده منبع حقیقت است.</td></tr><tr><td>صف پیام</td><td>پیام‌ها چطور قابل اعتماد جابه‌جا شوند؟</td><td>فکر کنیم وجود Kafka یا RabbitMQ یعنی Event Sourcing داریم.</td></tr><tr><td>Event Sourcing</td><td>آیا رخدادها تاریخچه‌ی رسمی و منبع حقیقت سیستم‌اند؟</td><td>فکر کنیم Event Sourcing فقط یک لاگ مفصل‌تر است.</td></tr></tbody></table>
<p>این سبک چند مزیت جدی دارد. برای حسابرسی و پشتیبانی، تاریخچه‌ی دقیق‌تری از اتفاق‌ها داریم. اگر لازم باشد بفهمیم چرا یک سفارش به وضعیت فعلی رسیده، می‌توانیم مسیر رخدادها را دنبال کنیم. اگر باگی در محاسبه‌ی وضعیت پیدا شود، گاهی می‌توان با منطق اصلاح‌شده، وضعیت را از روی رخدادهای گذشته دوباره ساخت. همچنین می‌توان از همان تاریخچه برای ساختن نماهای خواندن، گزارش‌ها یا تحلیل‌های تازه استفاده کرد.</p>
<p>اما این مزایا ارزان به دست نمی‌آیند. ترتیب رخدادها مهم می‌شود. نسخه‌بندی رخدادها دردسر دارد؛ چون رخدادهای قدیمی ممکن است با شکل جدید کد یکی نباشند. بازسازی وضعیت باید دقیق و قابل اعتماد باشد. حذف یا اصلاح داده‌ها ساده نیست، چون تاریخچه بخشی از حقیقت سیستم است. خواندن داده هم گاهی مستقیم و ساده مثل خواندن یک ردیف از جدول نیست و به مدل‌های خواندن جدا نیاز پیدا می‌کند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>Event Sourcing را نباید فقط برای خاص‌تر کردن معماری وارد سیستم کنیم. اگر مسئله‌ی ما CRUD ساده است، تاریخچه‌ی دقیق برای تصمیم‌های کسب‌وکار مهم نیست، و نگه‌داری رخدادها ارزش روشن ندارد، این الگو می‌تواند بیش از آنکه کمک کند، مدل ذهنی و پیاده‌سازی را سنگین کند.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی Event Sourcing می‌تواند ارزشمند باشد؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی تاریخچه‌ی دقیق برای حسابرسی، پشتیبانی، بازسازی وضعیت، تحلیل خطا یا فهم تصمیم‌های کسب‌وکار مهم است، Event Sourcing می‌تواند گزینه‌ی جدی‌تری باشد. مثلاً در سیستم‌هایی که مسیر رسیدن به یک وضعیت به اندازه‌ی خود وضعیت اهمیت دارد، نگه داشتن رخدادها ارزش واقعی پیدا می‌کند.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی احتمالاً زیاده‌روی است؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر فقط چند موجودیت ساده داریم، بیشتر عملیات‌ها CRUD معمولی‌اند، تاریخچه‌ی دقیق ارزش کسب‌وکاری ندارد، و تیم هنوز درگیر ساده‌سازی نیازهای پایه است، Event Sourcing احتمالاً زود است. در چنین مرحله‌ای، یک مدل وضعیت ساده همراه با ثبت تغییرات مهم ممکن است کافی‌تر و خواناتر باشد.</p></div></div></details>
<p>برای من، Event Sourcing یعنی به رسمیت شناختن این نکته که گاهی «چه اتفاقی افتاد» مهم‌تر از «الان در چه وضعیتی هستیم» است. وضعیت فعلی لازم است، اما همیشه کافی نیست. وقتی تاریخچه بخشی از حقیقت سیستم می‌شود، رخدادها دیگر فقط پیام نیستند؛ حافظه‌ی رسمی سیستم‌اند.</p>
<p>تا اینجا بیشتر درباره‌ی شکل ارتباط و مدل‌سازی رفتار سیستم حرف زدیم. از اینجا به بعد کم‌کم باید به سراغ زیرساخت و اجرای قابل تکرار برویم: اینکه محیط‌ها، تنظیمات، سرویس‌ها و منابع زیرساختی را چطور قابل بازسازی و قابل کنترل نگه داریم. این همان جایی است که Infrastructure as Code وارد داستان می‌شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-روی-سیستم-من-کار-میکند-دیگر-کافی-نیست">وقتی «روی سیستم من کار می‌کند» دیگر کافی نیست<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%B1%D9%88%DB%8C-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D9%85%D9%86-%DA%A9%D8%A7%D8%B1-%D9%85%DB%8C%DA%A9%D9%86%D8%AF-%D8%AF%DB%8C%DA%AF%D8%B1-%DA%A9%D8%A7%D9%81%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به وقتی «روی سیستم من کار می‌کند» دیگر کافی نیست" title="لینک مستقیم به وقتی «روی سیستم من کار می‌کند» دیگر کافی نیست" translate="no">​</a></h2>
<p>تقریباً هر تیم نرم‌افزاری، دیر یا زود، با این جمله روبه‌رو می‌شود: «روی سیستم من کار می‌کند.» برنامه روی لپ‌تاپ توسعه‌دهنده اجرا می‌شود، آزمون‌ها سبزند، همه‌چیز ظاهراً درست است؛ اما وقتی همان کد به محیط تست یا تولید می‌رود، خطا می‌دهد. چرا؟ شاید نسخه‌ی Python فرق دارد، شاید یک کتابخانه‌ی سیستمی نصب نیست، شاید متغیر محیطی جا افتاده، شاید مسیر فایل‌ها فرق می‌کند، یا شاید نسخه‌ی PostgreSQL و Redis با محیط توسعه یکی نیست.</p>
<p>اینجا یک نکته‌ی مهم روشن می‌شود: کد برنامه فقط خودش نیست. برنامه همراه با نسخه‌ی زبان، وابستگی‌ها، کتابخانه‌های سیستمی، تنظیمات، مسیرها، دستور اجرا و حتی فرض‌های پنهانی درباره‌ی محیط اجرا معنی پیدا می‌کند. اگر این محیط در هرجا کمی فرق کند، همان کد می‌تواند رفتار متفاوتی داشته باشد.</p>
<p><img decoding="async" loading="lazy" alt="تفاوت محیط توسعه و تولید باعث می‌شود یک کد یکسان رفتار متفاوتی داشته باشد" src="https://example.com/assets/images/works-on-my-machine-environment-drift-979d3cd9192710f3d1c4edba9b85a8eb.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>مسئله همیشه خود کد نیست؛ گاهی محیط اجرا عوض شده و همان تغییر کوچک کافی است تا برنامه در محیط دیگر خراب شود.</em></p>
<p>این درد تازه نیست. پیش از فراگیر شدن کانتینرها، یکی از راه‌حل‌های رایج این بود که محیط کامل‌تری را با ماشین مجازی یا VM بسازیم. ماشین مجازی کمک می‌کرد یک سیستم‌عامل کامل‌تر و جداگانه‌تر داشته باشیم، اما هزینه‌اش هم کم نبود: سنگین‌تر بود، بالا آمدنش زمان بیشتری می‌گرفت، و برای هر برنامه انگار یک رایانه‌ی کوچک جداگانه روشن می‌کردیم.</p>
<p>کانتینرها از همین‌جا جذاب شدند. ایده‌ی جداسازی سبک‌تر فرایندها در لینوکس قدیمی‌تر از Docker بود، اما Docker حدود سال ۲۰۱۳ این تجربه را برای توسعه‌دهنده‌ها بسیار ساده‌تر و فراگیرتر کرد. جذابیت Docker این بود که یک زبان روزمره به تیم‌ها داد: یک فایل بنویس، وابستگی‌ها و دستور اجرا را مشخص کن، image بساز، و همان image را در محیط‌های مختلف اجرا کن. به زبان ساده، Docker مشکل «این برنامه دقیقاً با چه محیطی اجرا می‌شود؟» را قابل کنترل‌تر کرد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>کانتینر کمک می‌کند برنامه و وابستگی‌های اجرایی‌اش را در یک بسته‌ی قابل حمل‌تر قرار دهیم. هدف این نیست که همه‌ی تفاوت‌های محیطی جادویی از بین بروند؛ هدف این است که اجرای برنامه قابل تکرارتر، شفاف‌تر و کمتر وابسته به تنظیمات دستی هر ماشین باشد.</p></div></div>
<p>فرض کنیم سرویس سفارش با Python نوشته شده است. بدون کانتینر، ممکن است روی هر سرور دستی Python نصب کنیم، پکیج‌ها را نصب کنیم، تنظیمات را بچینیم و امیدوار باشیم همه‌چیز شبیه محیط توسعه است. با Docker معمولاً یک <code>Dockerfile</code> می‌نویسیم و در آن مشخص می‌کنیم از چه نسخه‌ای از Python استفاده شود، چه وابستگی‌هایی نصب شود، کد کجا کپی شود و دستور اجرای برنامه چیست. بعد از روی آن یک image ساخته می‌شود.</p>
<p><img decoding="async" loading="lazy" alt="رابطه‌ی Dockerfile، image، container و registry" src="https://example.com/assets/images/docker-image-container-registry-flow-b3a4358dba438301314ee7b4891e923e.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>از Dockerfile یک image ساخته می‌شود؛ image مثل بسته‌ی آماده و ثابت برنامه است، container اجرای زنده‌ی همان image است، و registry جایی است که imageها را در آن نگه‌داری و جابه‌جا می‌کنیم.</em></p>
<p>چند واژه‌ی پایه‌ای اینجا مهم‌اند:</p>
<table><thead><tr><th>مفهوم</th><th>توضیح ساده</th></tr></thead><tbody><tr><td>Image</td><td>بسته‌ی آماده‌ی برنامه و وابستگی‌های اجرایی‌اش</td></tr><tr><td>Container</td><td>اجرای زنده‌ی یک image</td></tr><tr><td>Dockerfile</td><td>دستورالعمل ساخت image</td></tr><tr><td>Registry</td><td>جایی برای نگه‌داری و انتشار imageها؛ مثل Docker Hub یا registry داخلی</td></tr><tr><td>Volume</td><td>راهی برای وصل کردن داده‌ی بیرونی یا ماندگار به container</td></tr></tbody></table>
<p>تا اینجا Docker و کانتینر کمک می‌کنند برنامه را تمیزتر و قابل حمل‌تر اجرا کنیم. اما این پایان داستان نیست. وقتی فقط یک سرویس کوچک داریم، شاید Docker یا Docker Compose برای توسعه و حتی بعضی استقرارهای ساده کافی باشد. Docker Compose ابزاری است که اجازه می‌دهد چند کانتینر مرتبط را با یک فایل کنار هم تعریف و اجرا کنیم؛ مثلاً برنامه، پایگاه داده و Redis را برای محیط توسعه با یک دستور بالا بیاوریم. Compose کمک می‌کند اجرای چندتکه‌ی یک برنامه ساده‌تر شود، اما معمولاً برای مدیریت جدی سرویس‌ها در مقیاس تولید، جای Kubernetes را نمی‌گیرد.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Docker Compose کجای داستان است؟</div><div class="admonitionContent_hiHs"><p>اگر Docker می‌گوید «یک برنامه را داخل container اجرا کن»، Docker Compose می‌گوید «چند container مرتبط را کنار هم اجرا کن». برای توسعه، تست محلی و استقرارهای ساده می‌تواند بسیار مفید باشد؛ اما چیزهایی مثل self-healing، rollout تدریجی، service discovery در مقیاس بزرگ و مدیریت چند ماشین، همان جایی است که کم‌کم پای Kubernetes وسط می‌آید.</p></div></div>
<p>وقتی محصول بزرگ‌تر می‌شود و چند سرویس داریم، مسئله‌ها عوض می‌شوند: سرویس سفارش، پرداخت، کاربر، اعلان، گزارش، workerها، پایگاه داده، صف پیام و چند نسخه‌ی هم‌زمان از سرویس‌ها.</p>
<p>حالا پرسش‌های تازه‌ای داریم. اگر کانتینر سرویس سفارش خراب شد، چه کسی دوباره آن را بالا بیاورد؟ اگر ترافیک زیاد شد، چه کسی تعداد نمونه‌ها را بیشتر کند؟ اگر نسخه‌ی جدید منتشر کردیم، چطور آرام‌آرام جایگزین نسخه‌ی قدیمی شود؟ سرویس پرداخت چطور آدرس پایدار سرویس سفارش را پیدا کند؟ تنظیمات غیرحساس و رمزها کجا نگه‌داری شوند؟ از کجا بفهمیم یک نمونه سالم است یا باید از مسیر ترافیک خارج شود؟</p>
<p>اینجا Kubernetes وارد داستان می‌شود. Kubernetes را گوگل در سال ۲۰۱۴ معرفی کرد و طراحی آن از تجربه‌ی داخلی گوگل در اجرای workloadهای بزرگ، به‌ویژه سامانه‌هایی مثل Borg، الهام گرفته بود. نسخه‌ی ۱.۰ آن در سال ۲۰۱۵ منتشر شد و بعدتر به یکی از پایه‌های مهم دنیای cloud-native تبدیل شد. اگر Docker به توسعه‌دهنده‌ها گفت «برنامه را قابل بسته‌بندی‌تر کن»، Kubernetes گفت «حالا تعداد زیادی از این بسته‌ها را در یک محیط واقعی مدیریت کن».</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>سیر تحول خیلی خلاصه</div><div class="admonitionContent_hiHs"><p>ماشین مجازی می‌گفت: محیط کامل‌تری را با خودت ببر. کانتینر گفت: سبک‌تر و توسعه‌دهنده‌پسندتر بسته‌بندی کن. Docker Compose گفت: چند کانتینر مرتبط را راحت‌تر کنار هم اجرا کن. Kubernetes گفت: حالا تعداد زیادی اجرای کانتینری را در چند ماشین، با بازیابی، مقیاس‌دهی و به‌روزرسانی مدیریت کن.</p></div></div>
<p>Kubernetes یک هماهنگ‌کننده یا orchestrator است. یعنی ما وضعیت مطلوب را به آن می‌گوییم، و Kubernetes تلاش می‌کند آن وضعیت را حفظ کند. مثلاً می‌گوییم از سرویس سفارش همیشه سه نمونه در حال اجرا باشد. اگر یکی از آن‌ها خراب شود، Kubernetes تلاش می‌کند نمونه‌ی تازه‌ای بالا بیاورد. اگر نسخه‌ی جدید منتشر کنیم، می‌تواند نسخه‌ی قدیمی را کم‌کم با نسخه‌ی تازه جایگزین کند. اگر بخواهیم سرویس‌ها همدیگر را پیدا کنند، برایشان آدرس پایدار فراهم می‌کند.</p>
<p><img decoding="async" loading="lazy" alt="Kubernetes اجرای چند سرویس کانتینری را در یک خوشه مدیریت می‌کند" src="https://example.com/assets/images/kubernetes-orchestrates-containers-67e7029bf198d243a1e1f6a85d944eb3.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>کانتینرها برنامه را قابل بسته‌بندی‌تر می‌کنند؛ Kubernetes اجرای تعداد زیادی کانتینر را در محیط واقعی هماهنگ می‌کند.</em></p>
<p>برای فهم اولیه‌ی Kubernetes، این چند مفهوم کافی است:</p>
<table><thead><tr><th>مفهوم</th><th>توضیح ساده</th></tr></thead><tbody><tr><td>Pod</td><td>کوچک‌ترین واحد اجرایی در Kubernetes؛ معمولاً جایی که کانتینر برنامه اجرا می‌شود</td></tr><tr><td>Deployment</td><td>تعریف می‌کند چند نمونه از برنامه اجرا شود و به‌روزرسانی چگونه انجام شود</td></tr><tr><td>Service</td><td>آدرس پایدار برای دسترسی به Podها، حتی اگر خود Podها تغییر کنند</td></tr><tr><td>ConfigMap</td><td>نگه‌داری تنظیمات غیرحساس برنامه</td></tr><tr><td>Secret</td><td>نگه‌داری داده‌های حساس مثل رمز، توکن و کلیدها</td></tr><tr><td>Health Check</td><td>راهی برای تشخیص اینکه برنامه سالم است یا باید از مدار خارج شود</td></tr><tr><td>Rollout</td><td>جایگزین کردن تدریجی نسخه‌ی قدیمی با نسخه‌ی جدید</td></tr></tbody></table>
<p>یک مثال ساده را در نظر بگیریم. ما سرویس سفارش را containerize می‌کنیم و image آن را در registry می‌گذاریم. بعد در Kubernetes یک Deployment تعریف می‌کنیم که بگوید سه نمونه از این سرویس اجرا شود. یک Service هم تعریف می‌کنیم تا بقیه‌ی سرویس‌ها لازم نباشد آدرس تک‌تک Podها را بدانند. تنظیماتی مثل نام صف یا آدرس سرویس‌های داخلی می‌تواند در ConfigMap بیاید، و چیزهایی مثل رمز اتصال به پایگاه داده در Secret نگه‌داری شود. اگر یکی از Podها خراب شود، Kubernetes تلاش می‌کند نمونه‌ی جایگزین بالا بیاورد. اگر نسخه‌ی تازه‌ای از image منتشر کنیم، Deployment می‌تواند rollout انجام دهد.</p>
<p>این‌ها کمک می‌کنند اجرای سرویس‌ها از حالت «چند دستور دستی روی چند ماشین» به وضعیتی نزدیک‌تر شود که قابل تعریف، قابل پیگیری و قابل تکرار است. اما نباید دچار خیال‌پردازی شویم: Kubernetes پیچیدگی عملیاتی جدی دارد. خود خوشه باید نصب، نگه‌داری، پایش، امن‌سازی و به‌روزرسانی شود. اگر تیم هنوز یک برنامه‌ی ساده دارد، شاید Docker Compose، یک ماشین مجازی، یک سکوی ساده‌ی میزبانی یا حتی یک PaaS انتخاب بهتری باشد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>Kubernetes جایزه‌ی بلوغ فنی نیست که هر پروژه‌ای باید زود به آن برسد. اگر تعداد سرویس‌ها کم است، ترافیک ساده است، rollout و مقیاس‌دهی پیچیده نداریم، و تیم هنوز هزینه‌ی عملیاتی Kubernetes را نمی‌پذیرد، آوردن آن می‌تواند بیش از آنکه کمک کند، سیستم را سنگین‌تر کند.</p></div></div>
<table><thead><tr><th>وضعیت</th><th>انتخاب محتمل‌تر</th></tr></thead><tbody><tr><td>یک برنامه‌ی ساده برای تیم کوچک</td><td>اجرای مستقیم، Docker Compose یا یک PaaS ساده</td></tr><tr><td>چند سرویس با نیاز به محیط‌های مشابه</td><td>Docker، Docker Compose و registry داخلی می‌توانند کمک کنند</td></tr><tr><td>سرویس‌های متعدد با rollout، scaling و self-healing</td><td>Kubernetes می‌تواند ارزشمند شود</td></tr><tr><td>چند کار کوتاه و پراکنده</td><td>شاید Serverless یا Job/CronJob ساده مناسب‌تر باشد</td></tr><tr><td>نیاز به کنترل کامل روی شبکه، منابع و استقرار</td><td>Kubernetes گزینه‌ی جدی‌تری است، اما با هزینه‌ی عملیاتی بیشتر</td></tr></tbody></table>
<p>برای من، کانتینرها پاسخ به یک درد خیلی انسانی‌اند: «چرا این برنامه اینجا کار می‌کند و آنجا نه؟» و Kubernetes پاسخ به درد مرحله‌ی بعدی است: «حالا که همه‌چیز را کانتینری کردیم، چه کسی این همه اجرا را در محیط واقعی زنده، سالم، قابل کشف و قابل به‌روزرسانی نگه دارد؟» این دو یکی نیستند، اما به هم وصل‌اند. Docker بیشتر درباره‌ی بسته‌بندی و اجرای تکرارپذیر است؛ Docker Compose اجرای چند کانتینر مرتبط را ساده‌تر می‌کند؛ Kubernetes بیشتر درباره‌ی هماهنگ‌کردن و نگه‌داری تعداد زیادی اجرای کانتینری در محیط واقعی است.</p>
<p>تا اینجا درباره‌ی سرویس‌هایی حرف زدیم که واقعاً باید اجرا شوند، نسخه داشته باشند، در دسترس بمانند و در محیط‌های مختلف قابل تکرار باشند. اما همه‌ی کارهای سیستم از جنس سرویس دائمی نیستند. بعضی کارها فقط گاهی و در واکنش به یک رخداد، پیام، زمان‌بندی یا وبهوک اجرا می‌شوند. آیا برای آن‌ها هم باید Deployment یا worker همیشه‌روشن داشته باشیم؟ این همان پرسشی است که ما را به Serverless می‌رساند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-بعضی-کارها-ارزش-سرویس-دائمی-ندارند">وقتی بعضی کارها ارزش سرویس دائمی ندارند<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%A8%D8%B9%D8%B6%DB%8C-%DA%A9%D8%A7%D8%B1%D9%87%D8%A7-%D8%A7%D8%B1%D8%B2%D8%B4-%D8%B3%D8%B1%D9%88%DB%8C%D8%B3-%D8%AF%D8%A7%D8%A6%D9%85%DB%8C-%D9%86%D8%AF%D8%A7%D8%B1%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی بعضی کارها ارزش سرویس دائمی ندارند" title="لینک مستقیم به وقتی بعضی کارها ارزش سرویس دائمی ندارند" translate="no">​</a></h2>
<p>تا اینجا درباره‌ی سرویس‌هایی حرف زده‌ایم که باید قابل اجرا، قابل بسته‌بندی و قابل مدیریت باشند. اما همه‌ی کارهای سیستم از جنس سرویس دائمی نیستند. بعضی کارها کوتاه، پراکنده و واکنشی‌اند: رسیدگی به یک وبهوک پرداخت، ساخت تصویر بندانگشتی بعد از آپلود فایل، تولید یک گزارش سبک زمان‌بندی‌شده، پاک‌سازی داده‌های موقت، یا ارسال اعلان بعد از رسیدن یک پیام.</p>
<p>سؤال اصلی این فصل این است: آیا برای هر کار کوتاه و مقطعی باید یک سرویس یا worker همیشه‌روشن نگه داریم؟ گاهی جواب بله است، اما همیشه نه. اگر کاری فقط هنگام رسیدن یک رخداد یا زمان‌بندی خاص اجرا می‌شود، شاید بتوان آن را به شکل تابع یا واحد اجرایی کوتاه‌عمر اجرا کرد؛ چیزی که فقط هنگام نیاز روشن می‌شود و بعد از پایان کار، دیگر لازم نیست مثل یک سرویس دائمی زنده بماند.</p>
<p><img decoding="async" loading="lazy" alt="گاهی برای چند کار کوچک و پراکنده، یک سرویس دائمی و همیشه روشن نگه می‌داریم" src="https://example.com/assets/images/always-on-service-for-small-jobs-bd3c79764ea5e91262e6014f25beeae7.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>وقتی کارها کوتاه و پراکنده‌اند، نگه داشتن یک سرویس همیشه‌روشن ممکن است بیش از نیاز واقعی هزینه و پیچیدگی بسازد.</em></p>
<p>معماری بی‌سرور یا Serverless Architecture از همین پرسش شروع می‌شود. نامش کمی گمراه‌کننده است: بی‌سرور یعنی سروری وجود ندارد؟ نه. سرور هست، اما ما به‌صورت مستقیم آن را مدیریت نمی‌کنیم. در این مدل، معمولاً منطق کوچک و مشخصی را به شکل تابع تعریف می‌کنیم و سکوی اجرا آن را هنگام نیاز اجرا می‌کند: با یک درخواست HTTP، با یک پیام در صف، با یک زمان‌بندی، با آپلود فایل، یا با رسیدن یک وبهوک.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>Serverless برای کارهایی جذاب است که کوتاه، مستقل، رخدادمحور یا زمان‌بندی‌شده‌اند و نمی‌خواهیم برای آن‌ها یک سرویس همیشه‌روشن نگه داریم. تمرکز از «نگه‌داری یک سرویس دائمی» به «اجرای کد هنگام نیاز» جابه‌جا می‌شود.</p></div></div>
<p>برای ملموس‌تر شدن، فرض کنید سرویس پرداخت بعد از موفق شدن پرداخت، یک وبهوک به سیستم ما می‌فرستد. در مدل معمول، ممکن است یک سرویس همیشه‌روشن داشته باشیم که منتظر این درخواست‌ها بماند. در مدل Serverless، می‌توانیم یک تابع کوچک تعریف کنیم که فقط وقتی این وبهوک رسید اجرا شود: امضای درخواست را بررسی کند، تکراری نبودن پیام را بسنجد، وضعیت پرداخت را ذخیره کند، و اگر لازم بود یک پیام برای ادامه‌ی پردازش منتشر کند. بعد از تمام شدن کار، تابع هم تمام می‌شود.</p>
<p>در عمل، این مدل را با ابزارها و سکوهای مختلفی می‌بینیم. در دنیای ابری، نام‌هایی مثل AWS Lambda، Google Cloud Functions، Azure Functions، Cloudflare Workers، Vercel Functions و Netlify Functions زیاد شنیده می‌شوند. در فضای نزدیک به Kubernetes هم ابزارهایی مثل Knative و OpenFaaS مطرح می‌شوند. هدف این نوشته مقایسه‌ی این ابزارها نیست؛ فقط می‌خواهیم جای ذهنی‌شان روشن شود: این‌ها کمک می‌کنند کدی کوچک را در واکنش به یک محرک اجرا کنیم، بدون اینکه خودمان مستقیماً یک سرویس دائمی برای آن نگه داریم.</p>
<table><thead><tr><th>کار نمونه</th><th>محرک اجرا</th><th>شکل رایج پیاده‌سازی</th></tr></thead><tbody><tr><td>بررسی وبهوک پرداخت</td><td>رسیدن یک درخواست HTTP</td><td>یک تابع Serverless پشت یک endpoint</td></tr><tr><td>ساخت تصویر بندانگشتی</td><td>بارگذاری فایل در فضای ذخیره‌سازی</td><td>تابعی که با رخداد آپلود اجرا می‌شود</td></tr><tr><td>ارسال اعلان</td><td>رسیدن پیام در صف</td><td>تابعی که پیام را مصرف و اعلان را ارسال می‌کند</td></tr><tr><td>گزارش سبک شبانه</td><td>زمان‌بندی یا Cron</td><td>تابع زمان‌بندی‌شده</td></tr><tr><td>پاک‌سازی داده‌های موقت</td><td>اجرای دوره‌ای</td><td>تابعی کوچک برای حذف یا آرشیو داده‌ها</td></tr></tbody></table>
<p><img decoding="async" loading="lazy" alt="در Serverless محرک‌هایی مثل وبهوک، آپلود فایل، زمان‌بندی یا پیام صف می‌توانند تابع‌های کوتاه‌عمر را اجرا کنند" src="https://example.com/assets/images/serverless-functions-triggered-by-events-0c25aa3646022d2976026d0afc818483.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در این مدل، تابع فقط هنگام نیاز اجرا می‌شود و بعد از پایان کار، لازم نیست به‌عنوان یک سرویس دائمی روشن بماند.</em></p>
<p>اینجا ممکن است یک پرسش طبیعی پیش بیاید: «خب فرق این با Celery چیست؟ Celery هم کار پس‌زمینه اجرا می‌کند.» شباهتشان این است که هر دو می‌توانند برای کارهای غیرهم‌زمان و خارج از مسیر اصلی درخواست استفاده شوند؛ مثلاً ارسال ایمیل، پردازش فایل یا اجرای یک کار زمان‌بر. اما تفاوت اصلی در مدل اجرا و مالکیت زیرساخت است. در Celery معمولاً خودمان workerها را اجرا و مدیریت می‌کنیم، یک broker مثل Redis یا RabbitMQ داریم، و ظرفیت، استقرار، پایش و مقیاس‌دهی workerها با خودمان است. در Serverless، اجرای تابع و بخشی از مقیاس‌دهی را به سکوی اجرا می‌سپاریم.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>فرق Serverless با Celery</div><div class="admonitionContent_hiHs"><p>Celery بیشتر یک چارچوب اجرای کارهای پس‌زمینه در برنامه‌ی خودمان است؛ یعنی worker داریم، broker داریم، و زیرساخت اجرای آن را خودمان نگه می‌داریم. Serverless بیشتر یک مدل اجرای تابع روی سکوی ابری یا سکوی اجرای بیرونی است؛ تابع هنگام محرک اجرا می‌شود و مدیریت مستقیم سرور و مقیاس‌دهی کمتر بر عهده‌ی تیم برنامه است.</p></div></div>
<p>بحث هزینه اینجا مهم است، اما باید با احتیاط درباره‌اش حرف زد. گاهی Serverless برای کارهای کم‌تعداد، کوتاه و پراکنده جذاب است، چون به جای روشن نگه داشتن یک سرویس دائمی، بیشتر هنگام اجرا هزینه می‌دهیم. اگر کاری روزی چند بار اجرا شود یا فقط هنگام رخدادهای خاص لازم باشد، این مدل می‌تواند به‌صرفه‌تر باشد.</p>
<p>اما این همیشه به معنی ارزان‌تر بودن نیست. اگر تابع‌ها بسیار پرتعداد اجرا شوند، زمان اجرای طولانی داشته باشند، داده‌ی زیادی جابه‌جا کنند، یا زنجیره‌ای از تابع‌های کوچک پشت سر هم راه بیفتد، هزینه می‌تواند غافلگیرکننده شود. حتی اگر هزینه‌ی مالی خوب به نظر برسد، هزینه‌ی خطایابی، مشاهده‌پذیری، وابستگی به ارائه‌دهنده و پیچیدگی عملیاتی هم باید حساب شود.</p>
<div class="theme-admonition theme-admonition-caution admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هزینه همیشه فقط پول نیست</div><div class="admonitionContent_hiHs"><p>Serverless ممکن است هزینه‌ی اجرای کارهای کوتاه و پراکنده را کم کند، اما می‌تواند هزینه‌ی فهم، پایش و خطایابی سیستم را بالا ببرد. پس انتخاب آن باید بر اساس الگوی مصرف، زمان اجرا، حجم داده و توان عملیاتی تیم باشد؛ نه فقط جذابیت ظاهری مدل پرداخت.</p></div></div>
<p>Serverless چند چالش فنی هم دارد. یکی از شناخته‌شده‌ترین آن‌ها شروع سرد یا Cold Start است؛ یعنی تابعی که مدتی اجرا نشده، ممکن است در نخستین اجرا کمی دیرتر آماده شود. محدودیت زمان اجرا هم مهم است؛ بسیاری از بسترهای Serverless برای پردازش‌های خیلی طولانی مناسب نیستند. از طرف دیگر، اگر تابع‌ها زیاد و پراکنده شوند، دنبال کردن مسیر یک خطا از میان چند تابع کوچک سخت‌تر می‌شود. همچنین ممکن است به قراردادها و ابزارهای یک ارائه‌دهنده‌ی خاص وابسته شویم.</p>
<table><thead><tr><th>وضعیت</th><th>Serverless مناسب‌تر است؟</th><th>چرا؟</th></tr></thead><tbody><tr><td>کار کوتاه، مستقل و رخدادمحور است</td><td>معمولاً بله</td><td>لازم نیست سرویس دائمی نگه داریم.</td></tr><tr><td>کار کم‌تعداد و پراکنده است</td><td>معمولاً بله</td><td>هزینه بیشتر هنگام اجرا پرداخت می‌شود.</td></tr><tr><td>کار پرترافیک و دائماً فعال است</td><td>نه همیشه</td><td>سرویس دائمی یا worker دائمی ممکن است قابل پیش‌بینی‌تر و حتی ارزان‌تر باشد.</td></tr><tr><td>پردازش طولانی یا سنگین است</td><td>با احتیاط</td><td>محدودیت زمان اجرا و هزینه می‌تواند مشکل‌ساز شود.</td></tr><tr><td>کار به وضعیت درونی پیچیده نیاز دارد</td><td>با احتیاط</td><td>Serverless برای کارهای کوتاه و کم‌وضعیت مناسب‌تر است.</td></tr><tr><td>تیم کنترل کامل روی workerها می‌خواهد</td><td>نه همیشه</td><td>Celery یا سرویس worker معمولی شاید مناسب‌تر باشد.</td></tr><tr><td>تیم به مشاهده‌پذیری دقیق نیاز دارد</td><td>با طراحی دقیق</td><td>خطایابی میان چند تابع پراکنده می‌تواند سخت‌تر شود.</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>Serverless یعنی کل سیستم را از روز اول به چند تابع کوچک تبدیل کنیم؟ نه. خیلی از سرویس‌ها باید همیشه در دسترس، قابل کنترل، قابل مشاهده و دارای چرخه‌ی عمر روشن باشند. Serverless برای بعضی کارها عالی است، اما برای همه‌ی مسئله‌ها نه.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی Serverless انتخاب خوبی است؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی کاری کوتاه، مستقل، کم‌وضعیت، رخدادمحور یا زمان‌بندی‌شده داریم، Serverless می‌تواند انتخاب خوبی باشد. مثلاً پردازش یک فایل بعد از آپلود، ارسال اعلان بعد از یک رخداد، پاک‌سازی دوره‌ای داده‌های موقت یا واکنش به یک وبهوک پرداخت.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی Celery یا worker دائمی ممکن است مناسب‌تر باشد؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر کارها پیوسته و پرتعدادند، کنترل دقیق روی workerها می‌خواهیم، کارها به محیط داخلی برنامه خیلی وابسته‌اند، یا تیم از قبل broker، پایش و مقیاس‌دهی workerها را خوب مدیریت می‌کند، Celery یا یک سرویس worker معمولی می‌تواند ساده‌تر و قابل پیش‌بینی‌تر باشد.</p></div></div></details>
<p>برای من، Serverless یعنی لازم نیست برای هر کار کوچک، یک چراغ همیشه روشن نگه داریم. بعضی کارها فقط وقتی باید اجرا شوند که چیزی اتفاق افتاده، پیامی رسیده، زمانی فرا رسیده یا فایلی بارگذاری شده است. اما همین آزادی اگر بدون فهم هزینه، محدودیت و مشاهده‌پذیری بیاید، می‌تواند سیستم را پراکنده و سخت‌فهم کند.</p>
<p>وقتی سرویس‌های دائمی، کارهای کوتاه‌عمر و منابع اجرایی زیاد می‌شوند، یک پرسش تازه پیدا می‌شود: این همه تعریف، تنظیم، دسترسی، صف، تابع، سرویس و محیط را چطور قابل تکرار و قابل بازبینی نگه داریم؟ این همان جایی است که Infrastructure as Code وارد داستان می‌شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-زیرساخت-هم-باید-قابل-بازبینی-باشد">وقتی زیرساخت هم باید قابل بازبینی باشد<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%B2%DB%8C%D8%B1%D8%B3%D8%A7%D8%AE%D8%AA-%D9%87%D9%85-%D8%A8%D8%A7%DB%8C%D8%AF-%D9%82%D8%A7%D8%A8%D9%84-%D8%A8%D8%A7%D8%B2%D8%A8%DB%8C%D9%86%DB%8C-%D8%A8%D8%A7%D8%B4%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی زیرساخت هم باید قابل بازبینی باشد" title="لینک مستقیم به وقتی زیرساخت هم باید قابل بازبینی باشد" translate="no">​</a></h2>
<p>تا اینجا درباره‌ی سرویس‌ها، کانتینرها، Kubernetes و Serverless حرف زدیم. کم‌کم سیستم ما فقط چند فایل کد نیست؛ پایگاه داده دارد، صف پیام دارد، شبکه دارد، کلاستر دارد، دسترسی دارد، تنظیمات محیطی دارد و برای هر محیط، از آزمایشی تا تولید، باید شکل نسبتاً قابل اعتمادی داشته باشد.</p>
<p>اما اینجا یک درد آشنا دوباره برمی‌گردد. یک نفر می‌خواهد محیط آزمایشی را شبیه تولید بسازد و می‌پرسد: نسخه‌ی پایگاه داده دقیقاً چیست؟ صف پیام با چه تنظیماتی ساخته شده؟ چه کسی این دسترسی را به سرویس پرداخت داده؟ چرا در تولید این متغیر محیطی هست ولی در آزمایشی نیست؟ اگر فردا کل محیط را از دست بدهیم، می‌توانیم دوباره آن را بسازیم؟</p>
<p>اگر پاسخ این پرسش‌ها در چند کلیک دستی، چند دستور پراکنده، حافظه‌ی آدم‌ها و مستندات قدیمی پخش شده باشد، زیرساخت خودش تبدیل به منبع خطا می‌شود. همان‌طور که کد بدون نسخه‌بندی و بازبینی خطرناک است، زیرساختی هم که معلوم نیست چه کسی، چه چیزی را، کجا و چرا تغییر داده، دیر یا زود دردسر می‌سازد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>Infrastructure as Code یعنی زیرساخت را با فایل‌هایی تعریف کنیم که قابل نسخه‌بندی، بازبینی، تکرار و بازسازی‌اند؛ نه اینکه بخش مهمی از سیستم فقط در پنل‌ها، دستورهای دستی و حافظه‌ی افراد زندگی کند.</p></div></div>
<p>زیرساخت به‌مثابه کد یا Infrastructure as Code، که معمولاً IaC گفته می‌شود، از همین نیاز می‌آید. ایده این است که به‌جای ساختن دستی منابع، وضعیت مطلوب زیرساخت را در فایل‌هایی تعریف کنیم: این پایگاه داده را می‌خواهیم، این شبکه باید وجود داشته باشد، این صف پیام با این تنظیمات ساخته شود، این سرویس این دسترسی را داشته باشد، و این کلاستر Kubernetes با این ویژگی‌ها آماده شود.</p>
<p>وقتی این تعریف‌ها وارد Git می‌شوند، تغییر زیرساخت هم از حالت «کار پنهان در پنل و ترمینال» بیرون می‌آید. می‌توان تغییر را دید، بررسی کرد، درباره‌اش نظر داد، تاریخچه‌اش را فهمید و در بسیاری از موارد دوباره اجرا کرد. هدف این نیست که زیرساخت جادویی شود؛ هدف این است که تصمیم‌های زیرساختی از حافظه‌ی افراد بیرون بیاید و به دارایی قابل نگه‌داری تیم تبدیل شود.</p>
<p><img decoding="async" loading="lazy" alt="زیرساخت به‌مثابه کد، تعریف منابع را وارد Git، بازبینی و اجرای قابل تکرار می‌کند" src="https://example.com/assets/images/infrastructure-as-code-repeatable-environments-237087102bce037807ab4f4c5eef5765.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>وقتی زیرساخت با کد تعریف می‌شود، محیط‌ها کمتر به حافظه‌ی افراد و کلیک‌های دستی وابسته می‌مانند.</em></p>
<p>اینجا باید یک مرز مهم را روشن کنیم: IaC فقط «اتوماسیون» نیست. اتوماسیون می‌تواند یک اسکریپت باشد که چند دستور را پشت سر هم اجرا می‌کند. IaC معمولاً یک قدم جلوتر می‌رود و می‌گوید وضعیت مطلوب زیرساخت چیست و ابزار تلاش می‌کند محیط را به آن وضعیت نزدیک کند. البته همه‌ی ابزارها دقیقاً یکسان کار نمی‌کنند، اما در نگاه کلی، تفاوت این است: اسکریپت بیشتر می‌گوید «چه کارهایی انجام بده»، IaC بیشتر تلاش می‌کند بگوید «زیرساخت در نهایت باید چه شکلی باشد».</p>
<p>چند خانواده ابزار در این فضا زیاد دیده می‌شوند:</p>
<table><thead><tr><th>ابزار یا خانواده</th><th>معمولاً برای چه چیزی به کار می‌آید؟</th></tr></thead><tbody><tr><td>Terraform</td><td>تعریف و مدیریت منابعی مثل شبکه، پایگاه داده، صف، load balancer و منابع ابری</td></tr><tr><td>Pulumi</td><td>تعریف زیرساخت با زبان‌های برنامه‌نویسی عمومی‌تر مثل TypeScript یا Python</td></tr><tr><td>Ansible</td><td>پیکربندی ماشین‌ها، نصب بسته‌ها و اجرای کارهای عملیاتی</td></tr><tr><td>Kubernetes manifests</td><td>تعریف مستقیم منابع Kubernetes مثل Deployment، Service، ConfigMap و Secret</td></tr><tr><td>Helm</td><td>بسته‌بندی، نصب و نسخه‌بندی برنامه‌ها روی Kubernetes</td></tr><tr><td>Kustomize</td><td>تنظیم و تغییر manifestهای Kubernetes برای محیط‌های مختلف</td></tr><tr><td>Vault</td><td>نگه‌داری و کنترل دسترسی به رازها، کلیدها و داده‌های حساس</td></tr></tbody></table>
<p>این ابزارها جای هم نیستند. Terraform معمولاً برای ساخت و مدیریت منابع زیرساختی بیرون یا پیرامون برنامه به کار می‌آید؛ مثلاً شبکه، پایگاه داده، صف یا منابع ابری. Helm و Kustomize بیشتر در فضای Kubernetes کمک می‌کنند برنامه‌ها و منابع داخل کلاستر را تعریف و تنظیم کنیم. Ansible بیشتر به پیکربندی ماشین‌ها و اجرای کارهای عملیاتی نزدیک است. Vault هم مسئله‌ی دیگری را هدف می‌گیرد: رازها و داده‌های حساس را چطور امن، کنترل‌شده و قابل ردیابی نگه داریم.</p>
<p>Helm را می‌توان مثل یک بسته‌بند برای برنامه‌های Kubernetes دید. وقتی یک برنامه فقط یک Deployment ندارد و همراه خودش Service، ConfigMap، Ingress، Secret و چند مقدار قابل تنظیم دارد، نوشتن و نگه‌داری manifestهای خام می‌تواند سخت و تکراری شود. Helm این منابع را در قالب chart بسته‌بندی می‌کند و اجازه می‌دهد برای محیط‌های مختلف، مقدارهای متفاوت بدهیم؛ مثلاً تعداد replica، آدرس سرویس‌ها یا تنظیمات منابع.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Helm کجای داستان است؟</div><div class="admonitionContent_hiHs"><p>Terraform معمولاً منابع زیرساختی را می‌سازد یا مدیریت می‌کند؛ Helm معمولاً برنامه را روی Kubernetes نصب و تنظیم می‌کند؛ Argo CD می‌تواند مراقب باشد چیزی که در Git تعریف شده، واقعاً روی کلاستر هم اجرا شود. این سه ابزار ممکن است کنار هم استفاده شوند، اما نقششان یکی نیست.</p></div></div>
<p>برای اینکه ماجرا ملموس‌تر شود، فرض کنید سرویس سفارش به یک پایگاه داده، یک صف پیام، چند متغیر محیطی، چند Secret و یک Deployment روی Kubernetes نیاز دارد. بدون IaC، ممکن است بخشی از این‌ها در پنل ابری ساخته شود، بخشی با دستور دستی، بخشی با فایل‌های پراکنده و بخشی هم در ذهن افراد بماند. با IaC، تلاش می‌کنیم همین نیازها را در فایل‌هایی قابل بازبینی تعریف کنیم؛ فایل‌هایی که تغییراتشان دیده می‌شود و می‌توان درباره‌شان تصمیم گرفت.</p>
<p>اما یک نقطه‌ی حساس در این میان وجود دارد: رازها. رمز اتصال به پایگاه داده، کلید دسترسی به سرویس بیرونی، توکن‌ها و گواهی‌ها نباید مثل تنظیمات معمولی در repository پخش شوند. Kubernetes چیزی به نام Secret دارد، اما این به‌تنهایی پاسخ کامل مدیریت رازها نیست. Secret می‌گوید این داده‌ی حساس باید به شکل یک منبع جدا در کلاستر وجود داشته باشد؛ اما اینکه رازها از کجا بیایند، چه کسی به آن‌ها دسترسی داشته باشد، چطور بچرخند، چطور audit شوند و چطور وارد کلاستر شوند، مسئله‌ی بزرگ‌تری است.</p>
<p>اینجا ابزارهایی مثل Vault وارد می‌شوند. Vault کمک می‌کند رازها در جای متمرکزتر و کنترل‌شده‌تری نگه‌داری شوند، دسترسی‌ها با policy مشخص شوند، بعضی رازها به‌صورت پویا ساخته شوند، و مسیر استفاده از آن‌ها قابل ردیابی‌تر باشد. در عمل، تیم‌ها ممکن است Vault را کنار Kubernetes، External Secrets Operator، Argo CD یا ابزارهای مشابه استفاده کنند تا رازها بدون اینکه مستقیم در Git ذخیره شوند، به برنامه‌ها برسند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>رازها را با تنظیمات معمولی قاطی نکنیم</div><div class="admonitionContent_hiHs"><p>ConfigMap برای تنظیمات غیرحساس است؛ Secret برای داده‌ی حساس در Kubernetes است؛ Vault یا ابزارهای مشابه برای مدیریت جدی‌تر چرخه‌ی عمر رازها، کنترل دسترسی، چرخش و audit استفاده می‌شوند. IaC نباید باعث شود رمزها و کلیدها راحت‌تر و سریع‌تر وارد Git شوند.</p></div></div>
<p>وقتی پای Kubernetes وسط است، یک پرسش عملی‌تر هم پیدا می‌شود: اگر تعریف Deployment و Service و ConfigMap داخل Git است، چه کسی مطمئن شود وضعیت واقعی کلاستر شبیه همین تعریف‌هاست؟ اگر کسی دستی در کلاستر تعداد replica را تغییر داد چه؟ اگر نسخه‌ای که واقعاً در حال اجراست با نسخه‌ی داخل Git فرق داشت چه؟</p>
<p>اینجا GitOps وارد داستان می‌شود.</p>
<p>GitOps را می‌توان این‌طور ساده فهمید: Git منبع حقیقت وضعیت مطلوب سیستم است، و یک ابزار تلاش می‌کند وضعیت واقعی محیط را با چیزی که در Git تعریف شده هماهنگ نگه دارد. یعنی Git فقط محل نگه‌داری کد برنامه نیست؛ دفتر رسمی تصمیم‌های اجرایی و زیرساختی هم می‌شود.</p>
<p>یکی از ابزارهای شناخته‌شده در این فضا Argo CD است. Argo CD معمولاً در کنار Kubernetes استفاده می‌شود و manifestها، Helm chartها یا تنظیمات Kustomize داخل Git را با وضعیت واقعی کلاستر مقایسه می‌کند. اگر اختلافی باشد، می‌تواند آن را نشان دهد و در صورت تنظیم، محیط را دوباره با Git همگام کند.</p>
<p><img decoding="async" loading="lazy" alt="GitOps با Argo CD وضعیت مطلوب داخل Git را با وضعیت واقعی Kubernetes مقایسه و همگام می‌کند" src="https://example.com/assets/images/gitops-argo-sync-flow-dd04fe2b02efd73cddcd7d556adead9c.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در GitOps، Git منبع حقیقت وضعیت مطلوب است و ابزاری مثل Argo CD مراقب است کلاستر از آن وضعیت منحرف نشود.</em></p>
<p>تفاوت IaC و GitOps هم مهم است. IaC بیشتر درباره‌ی این است که منابع و زیرساخت را چگونه با کد تعریف کنیم. GitOps بیشتر درباره‌ی این است که محیط واقعی چگونه با تعریف‌های داخل Git همگام بماند. Argo CD هم یکی از ابزارهایی است که این ایده را، به‌ویژه در Kubernetes، عملی می‌کند.</p>
<table><thead><tr><th>مفهوم</th><th>پرسش اصلی</th></tr></thead><tbody><tr><td>Infrastructure as Code</td><td>زیرساخت و منابع را چطور به‌صورت فایل، قابل نسخه‌بندی و بازبینی تعریف کنیم؟</td></tr><tr><td>GitOps</td><td>چطور کاری کنیم محیط واقعی با تعریف‌های داخل Git هماهنگ بماند؟</td></tr><tr><td>Argo CD</td><td>چطور منابع Kubernetes را با Git مقایسه و همگام کنیم؟</td></tr><tr><td>Terraform</td><td>منابع زیرساختی مثل شبکه، دیتابیس، صف و منابع ابری را چطور تعریف و مدیریت کنیم؟</td></tr><tr><td>Helm / Kustomize</td><td>منابع Kubernetes را چطور بسته‌بندی یا برای محیط‌های مختلف تنظیم کنیم؟</td></tr><tr><td>Vault</td><td>رازها، کلیدها و داده‌های حساس را چطور امن‌تر و کنترل‌شده‌تر مدیریت کنیم؟</td></tr></tbody></table>
<p>البته IaC و GitOps هم جادو نیستند. اگر secretها را بد مدیریت کنیم، اگر reviewها سطحی باشند، اگر کسی مستقیم در محیط تولید تغییر بدهد، اگر sync خودکار بدون کنترل روی production فعال شود، یا اگر ساختار repository شلوغ و نامفهوم شود، همین ابزارها هم می‌توانند دردسر تازه بسازند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>IaC و GitOps تغییر بد را خوب نمی‌کنند؛ فقط مسیر تغییر را شفاف‌تر و قابل پیگیری‌تر می‌کنند. اگر طراحی زیرساخت بد باشد، IaC همان طراحی بد را منظم‌تر و سریع‌تر تکثیر می‌کند. اگر تغییر خطرناک بدون بازبینی از Git به production برود، همچنان خطرناک است.</p></div></div>
<p>چند پرسش خوب پیش از جدی گرفتن IaC و GitOps این‌هاست:</p>
<table><thead><tr><th>پرسش</th><th>چرا مهم است؟</th></tr></thead><tbody><tr><td>آیا محیط واقعی با فایل‌های Git فرق کرده است؟</td><td>این همان drift است و باید دیده یا اصلاح شود.</td></tr><tr><td>secretها کجا و چطور نگه‌داری می‌شوند؟</td><td>نباید داده‌ی حساس بی‌محافظ وارد Git شود.</td></tr><tr><td>قبل از apply یا sync چه چیزی بازبینی می‌شود؟</td><td>تغییر زیرساخت باید مثل تغییر کد review شود.</td></tr><tr><td>rollback چطور انجام می‌شود؟</td><td>برگشت از تغییر بد باید از قبل قابل تصور باشد.</td></tr><tr><td>چه کسی اجازه‌ی تغییر production را دارد؟</td><td>IaC بدون مرز دسترسی، خطرناک‌تر می‌شود.</td></tr><tr><td>ساختار repository قابل فهم است؟</td><td>اگر کسی نتواند مسیر تغییر را بفهمد، GitOps فقط ظاهر منظم دارد.</td></tr></tbody></table>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی شاید هنوز IaC کامل زود باشد؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر یک محیط بسیار ساده داریم، منابع کم‌اند، تغییرها به‌ندرت رخ می‌دهند و تیم هنوز در حال کشف نیازهای پایه است، شاید چند اسکریپت ساده و مستندات دقیق برای شروع کافی باشد. اما هرچه تعداد محیط‌ها، منابع و افراد بیشتر شود، هزینه‌ی تغییر دستی و حافظه‌محور بالا می‌رود.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی GitOps ارزشمندتر می‌شود؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی چند تیم روی یک یا چند کلاستر Kubernetes کار می‌کنند، نسخه‌ها زیاد تغییر می‌کنند، drift میان Git و محیط واقعی دردسرساز شده، یا لازم است مسیر تغییرات production شفاف و قابل audit باشد، GitOps می‌تواند ارزش جدی پیدا کند.</p></div></div></details>
<p>برای من، Infrastructure as Code یعنی زیرساخت را از قلمرو حافظه‌ی افراد و کلیک‌های دستی بیرون بیاوریم و به چیزی تبدیل کنیم که تیم بتواند آن را ببیند، نقد کند، تغییر دهد و دوباره بسازد. GitOps هم یک قدم جلوتر می‌پرسد: حالا که وضعیت مطلوب در Git است، چطور مطمئن شویم محیط واقعی از آن دور نشده است؟ و مدیریت رازها یادآوری می‌کند که قابل بازبینی کردن زیرساخت نباید به قیمت افشا کردن داده‌های حساس تمام شود.</p>
<p>تا اینجا درباره‌ی ساخت، اجرا و نگه‌داری زیرساخت حرف زدیم. اما وقتی محصول رشد می‌کند، پرسش دیگری هم ظاهر می‌شود: آیا همه‌ی مشتری‌ها، سازمان‌ها یا گروه‌های کاربران از یک سیستم مشترک استفاده می‌کنند؟ داده‌ها، تنظیمات و منابع آن‌ها چطور از هم جدا می‌شود؟ اینجا وارد Multi-tenancy می‌شویم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-یک-سیستم-باید-میزبان-چند-مشتری-باشد">وقتی یک سیستم باید میزبان چند مشتری باشد<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%DB%8C%DA%A9-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D8%A8%D8%A7%DB%8C%D8%AF-%D9%85%DB%8C%D8%B2%D8%A8%D8%A7%D9%86-%DA%86%D9%86%D8%AF-%D9%85%D8%B4%D8%AA%D8%B1%DB%8C-%D8%A8%D8%A7%D8%B4%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی یک سیستم باید میزبان چند مشتری باشد" title="لینک مستقیم به وقتی یک سیستم باید میزبان چند مشتری باشد" translate="no">​</a></h2>
<p>تا اینجا درباره‌ی طراحی سرویس‌ها، ارتباط میان آن‌ها، اجرای کانتینری، Serverless و زیرساخت قابل بازبینی حرف زدیم. حالا فرض کنیم محصول واقعاً رشد کرده است. دیگر فقط یک تیم یا یک سازمان از آن استفاده نمی‌کند. چند شرکت، چند مدرسه، چند فروشگاه یا چند مشتری سازمانی می‌خواهند از همان سامانه استفاده کنند.</p>
<p>در نگاه اول، شاید ساده به نظر برسد: همان سیستم را برای چند مشتری باز می‌کنیم. اما خیلی زود پرسش‌های جدی‌تر پیدا می‌شوند. داده‌ی هر مشتری کجا نگه‌داری می‌شود؟ کاربران هر مشتری چطور از هم جدا می‌شوند؟ تنظیمات هر مشتری کجا ذخیره می‌شود؟ اگر یک مشتری پرترافیک شد، آیا روی بقیه اثر می‌گذارد؟ اگر یک query اشتباه نوشته شود، آیا ممکن است داده‌ی مشتری دیگری نمایش داده شود؟</p>
<p>اینجاست که مفهوم Multi-tenancy یا معماری چندمستاجری وارد داستان می‌شود. در این مدل، یک سامانه به چند tenant خدمت می‌دهد. tenant می‌تواند یک شرکت، سازمان، تیم، مدرسه، فروشگاه یا هر واحد مستقلی باشد که داده، کاربر، تنظیمات و مرزهای خودش را دارد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>Multi-tenancy یعنی یک سامانه بتواند به چند مشتری یا سازمان خدمت بدهد، بی‌آنکه مرز داده، دسترسی، تنظیمات و منابع آن‌ها با هم قاطی شود.</p></div></div>
<p>ساده‌ترین راه این است که برای هر مشتری یک نسخه‌ی کامل و جدا از سیستم بالا بیاوریم. این کار از نظر جداسازی ذهنی ساده‌تر است، اما با زیاد شدن مشتری‌ها هزینه‌ی نگه‌داری بالا می‌رود: چند deployment، چند دیتابیس، چند تنظیم، چند migration، چند بکاپ و چند مسیر پایش. از آن طرف، اگر همه‌ی مشتری‌ها را روی یک سیستم کاملاً مشترک بیاوریم، هزینه‌ی عملیاتی کمتر می‌شود، اما خطرها و پیچیدگی‌های تازه‌ای وارد می‌شود.</p>
<p>پس چندمستاجری یک انتخاب صفر و یکی نیست؛ یک طیف است. هرچه اشتراک منابع بیشتر شود، هزینه و عملیات ممکن است ساده‌تر شود، اما مسئولیت ما برای جلوگیری از نشت داده، اثرگذاری مشتری‌ها روی هم و پیچیدگی مجوزها بیشتر می‌شود.</p>
<p>یکی از اولین جاهایی که این تصمیم خودش را نشان می‌دهد، مدل جداسازی داده است. سه مدل رایج‌تر را می‌شود این‌طور دید:</p>
<p><img decoding="async" loading="lazy" alt="سه مدل رایج جداسازی داده در سیستم‌های چندمستاجری" src="https://example.com/assets/images/multi-tenancy-data-isolation-models-73e721d458db28309ea116df9acabc00.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>هر مدل، معامله‌ای میان هزینه، جداسازی، امنیت و پیچیدگی عملیاتی است.</em></p>
<table><thead><tr><th>مدل</th><th>توضیح</th><th>مزیت</th><th>هزینه یا خطر</th></tr></thead><tbody><tr><td>دیتابیس جدا برای هر tenant</td><td>هر مشتری دیتابیس خودش را دارد</td><td>جداسازی قوی‌تر، بکاپ و بازیابی مستقل‌تر</td><td>هزینه و عملیات بیشتر</td></tr><tr><td>schema جدا برای هر tenant</td><td>همه روی یک دیتابیس‌اند، اما schema جدا دارند</td><td>جداسازی منطقی مناسب، مدیریت متمرکزتر</td><td>migration و عملیات پیچیده‌تر</td></tr><tr><td>جدول مشترک با <code>tenant_id</code></td><td>داده‌ها در جدول‌های مشترک‌اند و با tenant_id جدا می‌شوند</td><td>هزینه کمتر و مناسب‌تر برای تعداد زیاد tenant</td><td>خطر نشت داده در صورت خطای query یا کنترل دسترسی</td></tr></tbody></table>
<p>هیچ‌کدام از این مدل‌ها «بهترین مطلق» نیستند. اگر داده‌ها بسیار حساس‌اند، تعداد tenantها کم است و نیاز به بکاپ و بازیابی مستقل داریم، دیتابیس جدا می‌تواند منطقی باشد. اگر تعداد tenantها زیاد است و هزینه‌ی عملیاتی باید کنترل شود، جدول مشترک با <code>tenant_id</code> شاید عملی‌تر باشد. گاهی هم مدل ترکیبی داریم: tenantهای معمولی روی منابع مشترک‌اند، اما tenantهای بزرگ یا حساس منابع جدا می‌گیرند.</p>
<p>اما مهم‌ترین سوءبرداشت این است که فکر کنیم چندمستاجری فقط مسئله‌ی دیتابیس است. اضافه کردن <code>tenant_id</code> به جدول‌ها شروع کار است، نه پایان آن. اگر cache با tenant جدا نشود، اگر فایل‌ها در storage مسیر یا مالکیت روشن نداشته باشند، اگر log و metricها tenant-aware نباشند، یا اگر مجوزها درست اعمال نشوند، سیستم هنوز خطرناک است.</p>
<p><img decoding="async" loading="lazy" alt="چندمستاجری فقط مسئله‌ی دیتابیس نیست و چند لایه‌ی مختلف دارد" src="https://example.com/assets/images/multi-tenancy-layers-3adb6c7f7a8cad29fd46581cc40efcd8.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در سیستم چندمستاجری، جداسازی داده فقط یکی از لایه‌هاست؛ دسترسی، تنظیمات، منابع، مشاهده‌پذیری و عملیات هم باید tenant-aware باشند.</em></p>
<p>چندمستاجری را بهتر است در چند لایه ببینیم:</p>
<table><thead><tr><th>لایه</th><th>پرسش اصلی</th></tr></thead><tbody><tr><td>داده</td><td>داده‌ی tenantها چطور جدا می‌شود؟ دیتابیس جدا، schema جدا یا <code>tenant_id</code>؟</td></tr><tr><td>دسترسی</td><td>کاربر از کجا معلوم است متعلق به کدام tenant است و چه مجوزی دارد؟</td></tr><tr><td>تنظیمات</td><td>هر tenant چه تنظیمات ویژه‌ای دارد و این تنظیمات کجا نگه‌داری می‌شود؟</td></tr><tr><td>منابع</td><td>آیا همه از منابع مشترک استفاده می‌کنند یا بعضی tenantها منابع جدا دارند؟</td></tr><tr><td>مشاهده‌پذیری</td><td>وقتی خطا یا افت عملکرد رخ می‌دهد، می‌فهمیم مربوط به کدام tenant است؟</td></tr><tr><td>عملیات</td><td>migration، بکاپ، حذف داده و بازیابی برای هر tenant چطور انجام می‌شود؟</td></tr></tbody></table>
<p>یک مثال ساده بزنیم. فرض کنیم فروشگاه الف و فروشگاه ب هر دو از سامانه‌ی ما استفاده می‌کنند. کاربر فروشگاه الف وارد پنل می‌شود و گزارش سفارش‌ها را می‌بیند. اگر query گزارش فقط بر اساس تاریخ فیلتر کند و tenant را فراموش کند، ممکن است سفارش‌های فروشگاه ب هم در نتیجه بیاید. این فقط یک bug معمولی نیست؛ در سیستم چندمستاجری، چنین خطایی می‌تواند فاجعه‌ی اعتماد و امنیت باشد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>یک سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>اضافه کردن <code>tenant_id</code> به جدول‌ها یعنی Multi-tenancy حل شد؟ نه. باید مطمئن شویم همه‌ی مسیرهای خواندن، نوشتن، cache، فایل، گزارش، log، metric، دسترسی و عملیات، مرز tenant را می‌شناسند و رعایت می‌کنند.</p></div></div>
<p>یکی از خطرهای مهم دیگر، مسئله‌ی همسایه‌ی پرمصرف یا noisy neighbor است. وقتی چند tenant روی منابع مشترک اجرا می‌شوند، یک tenant پرترافیک می‌تواند منابع را مصرف کند و کیفیت سرویس tenantهای دیگر را پایین بیاورد. مثلاً یک مشتری گزارش سنگین می‌گیرد، صف پردازش را پر می‌کند یا تعداد زیادی درخواست هم‌زمان می‌فرستد، و مشتری‌های دیگر هم کندی را تجربه می‌کنند.</p>
<p>پس در سیستم چندمستاجری، عدالت منابع هم بخشی از طراحی است. ممکن است به rate limit، سهمیه، صف جدا، pool جدا، منابع اختصاصی برای مشتری‌های بزرگ، یا اولویت‌بندی پردازش نیاز داشته باشیم. این تصمیم‌ها فقط فنی نیستند؛ به قرارداد، مدل درآمدی و سطح سرویس وعده‌داده‌شده به مشتری هم ربط دارند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>چندمستاجری و زیرساخت</div><div class="admonitionContent_hiHs"><p>فصل قبل درباره‌ی IaC و GitOps بود. در سیستم چندمستاجری، آن بحث دوباره مهم می‌شود: شاید برای tenantهای بزرگ دیتابیس جدا بسازیم، namespace جدا در Kubernetes بدهیم، یا تنظیمات و منابعشان را با کد و Git نگه‌داری کنیم. پس Multi-tenancy فقط در کد برنامه نیست؛ در زیرساخت و عملیات هم خودش را نشان می‌دهد.</p></div></div>
<p>انتخاب مدل چندمستاجری باید با چند معیار انجام شود: حساسیت داده، تعداد tenantها، نیاز به سفارشی‌سازی، هزینه‌ی عملیات، الزامات حقوقی، نیاز به بکاپ و بازیابی مستقل، و توان تیم برای نگه‌داری سیستم. تیمی که تازه محصول را ساخته، شاید با مدل ساده‌تری شروع کند و بعد برای tenantهای خاص، جداسازی قوی‌تری اضافه کند. اما اگر از ابتدا داده‌ها بسیار حساس‌اند، ساده‌ترین مدل مشترک شاید انتخاب خطرناکی باشد.</p>
<table><thead><tr><th>پرسش تصمیم‌گیری</th><th>چرا مهم است؟</th></tr></thead><tbody><tr><td>داده‌ی tenantها چقدر حساس است؟</td><td>حساسیت بالا معمولاً جداسازی قوی‌تر می‌خواهد.</td></tr><tr><td>چند tenant داریم یا خواهیم داشت؟</td><td>تعداد زیاد، عملیات مدل‌های جداگانه را سخت‌تر می‌کند.</td></tr><tr><td>هر tenant چقدر سفارشی‌سازی می‌خواهد؟</td><td>تنظیمات زیاد، طراحی پیکربندی و استقرار را پیچیده‌تر می‌کند.</td></tr><tr><td>آیا tenantهای بزرگ داریم؟</td><td>شاید برای آن‌ها منابع یا دیتابیس جدا لازم شود.</td></tr><tr><td>migration و بکاپ چطور انجام می‌شود؟</td><td>با زیاد شدن tenantها، عملیات داده سخت‌تر می‌شود.</td></tr><tr><td>آیا مشاهده‌پذیری tenant-aware داریم؟</td><td>بدون آن، عیب‌یابی و پاسخ‌گویی به مشتری سخت می‌شود.</td></tr></tbody></table>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی مدل مشترک با tenant_id می‌تواند مناسب باشد؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی تعداد tenantها زیاد است، داده‌ها حساسیت بسیار بالا ندارند، تیم می‌تواند کنترل دسترسی و queryها را خوب مدیریت کند، و هزینه‌ی عملیاتی باید پایین بماند، مدل مشترک با <code>tenant_id</code> می‌تواند انتخاب عملی‌تری باشد. البته این مدل نیاز به دقت جدی در queryها، cache، گزارش‌ها و تست‌ها دارد.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی دیتابیس جدا برای هر tenant منطقی‌تر است؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی داده‌ها بسیار حساس‌اند، مشتری‌ها بزرگ‌اند، بکاپ و بازیابی مستقل مهم است، یا الزام حقوقی و قراردادی برای جداسازی قوی‌تر داریم، دیتابیس جدا می‌تواند گزینه‌ی مناسب‌تری باشد. هزینه‌ی عملیاتی این مدل بیشتر است، اما مرز داده روشن‌تر و مدیریت بعضی عملیات‌ها مستقل‌تر می‌شود.</p></div></div></details>
<p>برای من، Multi-tenancy یعنی پذیرفتن یک معامله‌ی مهم: می‌خواهیم تجربه‌ای مشترک و قابل نگه‌داری بسازیم، اما نباید مرز مشتری‌ها را ساده بگیریم. هرجا منابع را مشترک می‌کنیم، باید آگاهانه‌تر درباره‌ی امنیت، دسترسی، مصرف منابع، مشاهده‌پذیری و عملیات فکر کنیم.</p>
<p>تا اینجا گفتیم یک سیستم چندمستاجری چطور داده و منابع چند مشتری را کنار هم نگه می‌دارد. اما همین انتخاب، فصل بعدی را سخت‌تر می‌کند: وقتی ساختار داده تغییر می‌کند، migration دیگر فقط تغییر چند جدول در یک دیتابیس نیست. شاید باید تغییر را روی چند دیتابیس، چند schema یا میلیون‌ها ردیف دارای <code>tenant_id</code> اجرا کنیم. اینجاست که وارد Data Migration می‌شویم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-دادهها-هم-اسبابکشی-دارند">وقتی داده‌ها هم اسباب‌کشی دارند<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%AF%D8%A7%D8%AF%D9%87%D9%87%D8%A7-%D9%87%D9%85-%D8%A7%D8%B3%D8%A8%D8%A7%D8%A8%DA%A9%D8%B4%DB%8C-%D8%AF%D8%A7%D8%B1%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی داده‌ها هم اسباب‌کشی دارند" title="لینک مستقیم به وقتی داده‌ها هم اسباب‌کشی دارند" translate="no">​</a></h2>
<p>در فصل قبل گفتیم وقتی یک سیستم چندمستاجری می‌شود، داده‌ی چند مشتری، سازمان یا گروه کاربری در کنار هم مدیریت می‌شود. حالا تصور کنیم محصول رشد کرده و یک تغییر ظاهراً ساده از راه می‌رسد: تیم محصول می‌خواهد فیلد آدرس دقیق‌تر شود؛ شهر، استان و کدپستی جدا شوند. یا تیم فنی می‌گوید جدول کاربران باید بازطراحی شود. یا تیم مالی می‌خواهد وضعیت تراکنش‌ها از یک مقدار کلی به چند وضعیت دقیق‌تر تبدیل شود.</p>
<p>در نگاه اول، این‌ها فقط تغییر دیتابیس‌اند: یک ستون اضافه کن، یک جدول بساز، چند داده را جابه‌جا کن، ستون قدیمی را حذف کن. اما در سیستم واقعی، داده زنده است. کاربرها هم‌زمان در حال استفاده‌اند، سرویس‌ها به جدول‌ها وابسته‌اند، گزارش‌ها از داده می‌خوانند، workerها در پس‌زمینه چیزی می‌نویسند، و در سیستم چندمستاجری شاید همین تغییر باید برای چند tenant، schema یا دیتابیس اجرا شود.</p>
<p>اینجاست که Data Migration یا مهاجرت داده وارد داستان می‌شود. مهاجرت داده فقط جابه‌جایی چند ردیف نیست؛ یعنی تغییر دادن ساختار، محل یا معنای داده، بدون اینکه اعتماد سیستم به داده‌ها از بین برود.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>Data Migration یعنی داده‌ی زنده را از یک وضعیت به وضعیت دیگر ببریم، بی‌آنکه صحت، معنا، دسترس‌پذیری و اعتماد به داده قربانی شود.</p></div></div>
<p>یک مثال ساده را در نظر بگیریم. در جدول <code>users</code> قبلاً فقط یک ستون <code>full_name</code> داشتیم. حالا می‌خواهیم آن را به <code>first_name</code> و <code>last_name</code> تبدیل کنیم. اگر در یک حرکت ستون قدیمی را حذف کنیم و کد جدید را منتشر کنیم، ممکن است نسخه‌های قدیمی‌تر کد هنوز <code>full_name</code> بخواهند. ممکن است بعضی داده‌ها درست تفکیک نشوند. ممکن است گزارش‌ها هنوز به ستون قدیمی وابسته باشند. اگر جدول بزرگ باشد، تغییر یک‌باره حتی می‌تواند باعث قفل شدن جدول یا اختلال در سرویس شود.</p>
<p><img decoding="async" loading="lazy" alt="مهاجرت داده‌ی خطرناک و یک‌مرحله‌ای" src="https://example.com/assets/images/risky-one-step-data-migration-896a7c1df3b0a7e2d209a2097ddbf1d7.png" width="1491" height="1055" class="img_m9Pm"></p>
<p><em>در مهاجرت یک‌مرحله‌ای، فرض می‌کنیم کد، داده و همه‌ی سرویس‌ها هم‌زمان و بی‌خطا تغییر می‌کنند؛ در سیستم واقعی این فرض معمولاً خطرناک است.</em></p>
<p>مهاجرت داده شکل‌های مختلفی دارد. گاهی ساختار جدول عوض می‌شود، گاهی خود داده‌ها تبدیل می‌شوند، و گاهی معنای یک مقدار تغییر می‌کند. سخت‌ترین حالت معمولاً همان تغییر معناست، چون خطایش همیشه سریع دیده نمی‌شود.</p>
<table><thead><tr><th>نوع تغییر</th><th>مثال</th><th>خطر اصلی</th></tr></thead><tbody><tr><td>تغییر ساختار</td><td>اضافه کردن ستون، ساخت جدول جدید، تغییر نوع ستون</td><td>ناسازگاری کد و دیتابیس</td></tr><tr><td>تغییر داده</td><td>پر کردن مقدار جدید، انتقال داده بین جدول‌ها، backfill</td><td>داده‌ی ناقص، فشار روی دیتابیس یا خطای تبدیل</td></tr><tr><td>تغییر معنا</td><td>تبدیل <code>canceled</code> به <code>canceled_by_user</code> و <code>canceled_by_system</code></td><td>خطای پنهان در گزارش‌ها و منطق کسب‌وکار</td></tr></tbody></table>
<p>نکته‌ی مهم این است که migration فقط <code>ALTER TABLE</code> نیست. ممکن است یک migration از نظر دیتابیس ساده باشد، اما از نظر محصولی پیچیده شود. اگر معنای یک وضعیت تغییر کند، باید گزارش‌ها، داشبوردها، APIها، تست‌ها، مستندات و حتی زبان تیم محصول هم با آن تغییر هماهنگ شوند.</p>
<p>راه امن‌تر معمولاً این است که migration را به یک فرایند مرحله‌ای تبدیل کنیم؛ نه یک تغییر ناگهانی. یکی از الگوهای رایج برای این کار، الگوی گسترش، پرکردن و جمع‌کردن است؛ همان چیزی که گاهی با نام Expand → Backfill → Contract شناخته می‌شود.</p>
<p><img decoding="async" loading="lazy" alt="مهاجرت داده‌ی مرحله‌ای و امن‌تر" src="https://example.com/assets/images/safe-expand-backfill-contract-migration-cec3a4f2b19af39a19be5190f06be706.png" width="1491" height="1055" class="img_m9Pm"></p>
<p><em>در مهاجرت مرحله‌ای، ابتدا مسیر جدید را بدون شکستن مسیر قدیمی اضافه می‌کنیم، بعد داده‌ها را آرام‌آرام منتقل می‌کنیم، و فقط وقتی مطمئن شدیم مسیر جدید پایدار است، مسیر قدیمی را حذف می‌کنیم.</em></p>
<p>در مثال <code>full_name</code>، مسیر امن‌تر می‌تواند این باشد:</p>
<ol>
<li class="">ستون‌های <code>first_name</code> و <code>last_name</code> را اضافه کنیم، بدون حذف <code>full_name</code>.</li>
<li class="">کد را طوری تغییر دهیم که بتواند با هر دو ساختار کنار بیاید.</li>
<li class="">داده‌های قدیمی را تدریجی و در batchهای کوچک به ساختار جدید منتقل کنیم.</li>
<li class="">مدتی خواندن و نوشتن را پایش کنیم و مطمئن شویم داده‌های جدید درست‌اند.</li>
<li class="">خواندن اصلی را از ساختار جدید انجام دهیم.</li>
<li class="">بعد از اطمینان، مسیر قدیمی را حذف کنیم.</li>
</ol>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Migration امن معمولاً چندمرحله‌ای است</div><div class="admonitionContent_hiHs"><p>در سیستم‌های واقعی، تغییر داده را بهتر است یک deploy ساده نبینیم. گاهی باید چند نسخه از کد و دیتابیس برای مدتی با هم سازگار بمانند تا بتوانیم بدون قطعی و با ریسک کمتر مهاجرت کنیم.</p></div></div>
<p>اینجا دوباره بحث سازگاری نسخه‌ها مهم می‌شود. کد جدید باید بتواند با داده‌ی قدیمی کنار بیاید. کد قدیمی هم ممکن است برای مدتی با schema جدید کار کند. اگر چند سرویس داریم که هم‌زمان deploy نمی‌شوند، migration نباید فرض کند همه‌ی آن‌ها هم‌زمان به نسخه‌ی جدید رفته‌اند. اضافه کردن یک ستون معمولاً امن‌تر از rename یا حذف ستون است، چون کد قدیمی معمولاً از اضافه شدن ستون جدید نمی‌شکند؛ اما حذف و تغییرهای مخرب باید با احتیاط و مرحله‌ای انجام شوند.</p>
<p>در سیستم چندمستاجری، migration پیچیده‌تر هم می‌شود. اگر مدل ما جدول مشترک با <code>tenant_id</code> است، migration روی جدول‌های بزرگ و مشترک اجرا می‌شود و باید مراقب فشار روی کل سیستم باشیم. اگر schema جدا برای هر tenant داریم، migration باید روی چند schema اجرا و پایش شود. اگر دیتابیس جدا برای هر tenant داریم، باید بدانیم کدام tenant مهاجرت شده، کدام نشده، و اگر یکی شکست خورد، چه می‌کنیم.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>در چندمستاجری، migration فقط تغییر داده نیست</div><div class="admonitionContent_hiHs"><p>در سیستم چندمستاجری، migration یک کار عملیاتی هم هست: باید بدانیم تغییر برای کدام tenant اجرا شده، کدام tenant خطا داده، آیا می‌توانیم tenantها را مرحله‌ای مهاجرت دهیم، و اگر یک tenant مشکل داشت، چطور جلوی اثرگذاری روی بقیه را بگیریم.</p></div></div>
<p>یکی از اشتباه‌های خطرناک این است که migration را در ساعت اوج و بدون مشاهده‌پذیری اجرا کنیم. اگر backfill روی جدول بزرگ بدون محدودیت اجرا شود، می‌تواند دیتابیس را تحت فشار بگذارد. اگر جدول قفل شود، سرویس کند یا از دسترس خارج می‌شود. اگر metric و alert نداشته باشیم، شاید دیر بفهمیم داده‌ها ناقص منتقل شده‌اند.</p>
<p>چند پرسش مهم پیش از اجرای migration:</p>
<table><thead><tr><th>پرسش</th><th>چرا مهم است؟</th></tr></thead><tbody><tr><td>آیا نسخه‌های قدیمی و جدید کد با schema جدید سازگارند؟</td><td>سرویس‌ها همیشه هم‌زمان deploy نمی‌شوند.</td></tr><tr><td>آیا migration روی جدول بزرگ lock ایجاد می‌کند؟</td><td>قفل شدن جدول می‌تواند سرویس را مختل کند.</td></tr><tr><td>آیا backfill قابل توقف و ادامه دادن است؟</td><td>migration طولانی ممکن است وسط کار شکست بخورد.</td></tr><tr><td>آیا progress و خطاها را metric و alert می‌کنیم؟</td><td>بدون مشاهده‌پذیری، migration کورکورانه است.</td></tr><tr><td>آیا backup، rollback یا برنامه‌ی ترمیم داریم؟</td><td>داده را مثل کد همیشه راحت نمی‌شود برگرداند.</td></tr><tr><td>در سیستم چندمستاجری، وضعیت هر tenant معلوم است؟</td><td>ممکن است بعضی tenantها مهاجرت شده باشند و بعضی نه.</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>داده را مثل کد همیشه نمی‌توان راحت برگرداند</div><div class="admonitionContent_hiHs"><p>اگر deploy بد باشد، معمولاً می‌توانیم نسخه‌ی قبلی کد را برگردانیم. اما اگر migration داده را خراب کند، rollback همیشه ساده نیست. گاهی باید داده را ترمیم کنیم، نه فقط کد را عقب ببریم.</p></div></div>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی migration یک‌مرحله‌ای قابل قبول‌تر است؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر سیستم هنوز کوچک است، جدول‌ها کم‌حجم‌اند، فقط یک سرویس به داده وابسته است، downtime کوتاه قابل قبول است و backup روشن داریم، migration یک‌مرحله‌ای ممکن است کافی باشد. اما با بزرگ شدن داده، زیاد شدن سرویس‌ها و حساس‌تر شدن محصول، این روش پرریسک‌تر می‌شود.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی migration مرحله‌ای ضروری‌تر می‌شود؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی داده زیاد است، سرویس‌ها متعددند، deployها هم‌زمان نیستند، downtime قابل قبول نیست، یا چند tenant داریم، migration مرحله‌ای معمولاً انتخاب امن‌تری است. در این حالت باید سازگاری نسخه‌ها، backfill تدریجی، پایش پیشرفت و برنامه‌ی ترمیم را جدی بگیریم.</p></div></div></details>
<p>برای من، Data Migration یعنی پذیرفتن این واقعیت که داده، برخلاف کد، فقط با برگشتن به commit قبلی درست نمی‌شود. داده تاریخچه دارد، معنا دارد، مشتری دارد و خطای آن می‌تواند در گزارش‌ها، تصمیم‌های کسب‌وکار و اعتماد کاربر باقی بماند. پس migration خوب بیشتر از آنکه یک دستور دیتابیس باشد، یک فرایند فنی و عملیاتی است.</p>
<p>وقتی migration را مرحله‌ای، قابل مشاهده و قابل ترمیم طراحی می‌کنیم، در واقع قبول کرده‌ایم که سیستم واقعی همیشه در معرض تغییر و ریسک است. اما آیا کافی است فقط امیدوار باشیم که همه‌چیز درست کار کند؟ یا می‌توانیم آگاهانه سیستم را در برابر خطاها، قطعی‌ها و رفتارهای غیرمنتظره آزمایش کنیم؟ این پرسش ما را به Chaos Engineering می‌رساند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-خرابی-اتفاق-بد-نیست-ناآمادگی-بد-است">وقتی خرابی اتفاق بد نیست؛ ناآمادگی بد است<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%AE%D8%B1%D8%A7%D8%A8%DB%8C-%D8%A7%D8%AA%D9%81%D8%A7%D9%82-%D8%A8%D8%AF-%D9%86%DB%8C%D8%B3%D8%AA-%D9%86%D8%A7%D8%A2%D9%85%D8%A7%D8%AF%DA%AF%DB%8C-%D8%A8%D8%AF-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به وقتی خرابی اتفاق بد نیست؛ ناآمادگی بد است" title="لینک مستقیم به وقتی خرابی اتفاق بد نیست؛ ناآمادگی بد است" translate="no">​</a></h2>
<p>تا اینجا سیستم ما بزرگ و واقعی‌تر شده است. سرویس‌ها از هم جدا شده‌اند، پیام و صف و رخداد داریم، داده‌ها مهاجرت می‌کنند، زیرساخت با کد مدیریت می‌شود و شاید چند tenant هم روی یک سامانه زندگی می‌کنند. اما یک پرسش هنوز باقی است: وقتی بخشی از این سیستم خراب شود، واقعاً می‌دانیم چه اتفاقی می‌افتد؟ یا فقط امیدواریم که همه‌چیز خوب کار کند؟</p>
<p>خیلی از سیستم‌ها در روزهای عادی سالم به نظر می‌رسند. نمودارها سبز هستند، سرویس‌ها پاسخ می‌دهند و کاربران شکایت خاصی ندارند. اما کافی است یک سرویس کمی کند شود، یک وابستگی بیرونی خطا بدهد، یک نمونه از سرویس سفارش از مدار خارج شود، یا ارتباط با cache برای چند دقیقه قطع شود. آن وقت ممکن است زنجیره‌ای از خطاها شروع شود؛ خطاهایی که تا قبل از حادثه، فقط در ذهن ما «نباید اتفاق می‌افتادند».</p>
<p>Chaos Engineering از همین نقطه شروع می‌شود: به‌جای اینکه فقط منتظر خرابی واقعی بمانیم، خرابی‌های محتمل را در اندازه‌ی کوچک، کنترل‌شده و قابل مشاهده تمرین کنیم تا ضعف‌ها را زودتر پیدا کنیم.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>Chaos Engineering یعنی طراحی و اجرای آزمایش‌های کنترل‌شده برای فهمیدن اینکه سیستم در برابر خرابی‌های واقعی چقدر تاب‌آور است. هدف خراب کردن سیستم نیست؛ هدف کم کردن غافلگیری در روز حادثه است.</p></div></div>
<p>این ایده بیش از همه با Netflix معروف شد. وقتی Netflix روی AWS و معماری ابری توزیع‌شده کار می‌کرد، با واقعیتی روبه‌رو بود که برای خیلی از سیستم‌های امروزی هم آشناست: خرابی ماشین، شبکه، وابستگی بیرونی یا بخشی از زیرساخت اتفاقی عجیب و نادر نیست؛ بخشی از زندگی روزمره‌ی سیستم‌های توزیع‌شده است. Netflix حدود سال ۲۰۱۱ ابزاری به نام Chaos Monkey ساخت؛ ابزاری که به‌صورت کنترل‌شده بعضی instanceها را از مدار خارج می‌کرد تا تیم‌ها مجبور شوند سیستم‌هایی بسازند که با خرابی یک نمونه از پا نیفتند. بعدتر این ایده در قالب ابزارها و روش‌های بیشتری رشد کرد و Chaos Engineering به یک رویکرد مهندسی برای آزمودن تاب‌آوری تبدیل شد.</p>
<p><img decoding="async" loading="lazy" alt="تاریخچه و ایده‌ی Chaos Engineering و Chaos Monkey" src="https://example.com/assets/images/chaos-engineering-chaos-monkey-history-55afa8ebf0c3bc4c1a7c19f207e10091.png" width="1536" height="1024" class="img_m9Pm"></p>
<p><em>ایده این نیست که سیستم را بی‌هدف خراب کنیم؛ ایده این است که خرابی‌های قابل انتظار را قبل از روز حادثه، کنترل‌شده تمرین کنیم.</em></p>
<p>البته خود ایده‌ی تمرین خرابی از هیچ جا ناگهان ظاهر نشد. پیش از این هم تیم‌ها تمرین‌های بازیابی از فاجعه، مانورهای عملیاتی و تست‌های تاب‌آوری داشتند. تفاوت مهم این بود که Netflix این نگاه را در فضای سرویس‌های ابری و توزیع‌شده بسیار ملموس کرد: اگر می‌دانیم خرابی بخشی از واقعیت است، پس باید آن را به شکل کنترل‌شده وارد فرایند یادگیری سیستم کنیم.</p>
<p>اما همین‌جا باید یک سوءبرداشت خطرناک را کنار بگذاریم. Chaos Engineering یعنی «خراب‌کاری تصادفی»؟ نه. یعنی تیمی بدون آمادگی برود production را بلرزاند تا ببیند چه می‌شود؟ قطعاً نه. Chaos Engineering درست، فرضیه‌محور است: اول می‌گوییم انتظار داریم سیستم در یک شرایط مشخص چه رفتاری داشته باشد، بعد یک اختلال محدود و قابل توقف وارد می‌کنیم، و در نهایت نتیجه را با معیارهای روشن می‌سنجیم.</p>
<p>مثلاً فرضیه می‌تواند این باشد: «اگر یکی از نمونه‌های سرویس سفارش از مدار خارج شود، کاربر نباید خطای محسوس ببیند و ترافیک باید به نمونه‌های سالم منتقل شود.» بعد آزمایش را در محدوده‌ای کوچک اجرا می‌کنیم، نرخ خطا، تأخیر، تعداد درخواست‌های ناموفق و رفتار alertها را می‌بینیم، و اگر وضعیت از حد امن خارج شد، آزمایش را متوقف می‌کنیم.</p>
<p><img decoding="async" loading="lazy" alt="جریان یک آزمایش کنترل‌شده در Chaos Engineering" src="https://example.com/assets/images/controlled-chaos-engineering-experiment-flow-0b31daf4f70c396a8832afad84d68dd3.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>یک آزمایش درست در Chaos Engineering با فرضیه شروع می‌شود، محدوده‌ی اثر دارد، قابل مشاهده است، شرط توقف دارد و در نهایت به یادگیری و اصلاح منجر می‌شود.</em></p>
<p>چند مفهوم پایه در این مسیر مهم‌اند:</p>
<table><thead><tr><th>مفهوم</th><th>توضیح ساده</th></tr></thead><tbody><tr><td>Steady State</td><td>وضعیت عادی و سالم سیستم که انتظار داریم حفظ شود</td></tr><tr><td>Hypothesis</td><td>فرضیه‌ای که می‌خواهیم آزمایش کنیم</td></tr><tr><td>Fault Injection</td><td>وارد کردن خطای کنترل‌شده؛ مثل قطع یک instance یا افزایش تأخیر</td></tr><tr><td>Blast Radius</td><td>محدوده‌ی اثر آزمایش؛ یعنی اگر بد شد، چقدر آسیب می‌زند</td></tr><tr><td>Abort Condition</td><td>شرط توقف آزمایش</td></tr><tr><td>Game Day</td><td>تمرین برنامه‌ریزی‌شده برای آزمودن واکنش سیستم و تیم به حادثه</td></tr></tbody></table>
<p>نمونه‌های ساده‌ی آزمایش می‌تواند این‌ها باشد: حذف یک pod در Kubernetes، خاموش کردن یک نمونه از سرویس سفارش، افزایش تأخیر بین سرویس پرداخت و سفارش، قطع موقت دسترسی به cache، کند کردن یک read replica، پر شدن دیسک یک worker، یا خطای موقت در سرویس ارسال پیامک. در سناریوهای پیشرفته‌تر شاید درباره‌ی از دسترس خارج شدن یک availability zone هم فکر کنیم؛ اما شروع عاقلانه معمولاً از آزمایش‌های کوچک‌تر است، نه از بزرگ‌ترین فاجعه‌ی ممکن.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>از کوچک شروع کنیم</div><div class="admonitionContent_hiHs"><p>Chaos Engineering خوب معمولاً با آزمایش‌های کوچک، قابل توقف و کم‌خطر شروع می‌شود. لازم نیست روز اول یک ناحیه‌ی ابری را از مدار خارج کنیم. اول باید مطمئن شویم مشاهده‌پذیری، alert، rollback، مالکیت سرویس و واکنش تیم قابل اتکا هستند.</p></div></div>
<p>حالا نقد مهم: چرا خیلی از تیم‌ها Chaos Engineering انجام نمی‌دهند؟ همیشه هم از تنبلی یا عقب‌ماندگی نیست. گاهی انجام ندادن آن تصمیم درستی است، چون پیش‌نیازهایش فراهم نیست. اگر metric، log، trace و alert درست نداریم، وارد کردن خطا فقط کور کردن خودمان است. اگر rollback و runbook نداریم، آزمایش خرابی ممکن است خودش حادثه شود. اگر تیم هنوز مالکیت سرویس‌ها را روشن نکرده، معلوم نیست هنگام آزمایش چه کسی باید تصمیم بگیرد.</p>
<table><thead><tr><th>دلیل انجام ندادن</th><th>چرا قابل فهم است؟</th></tr></thead><tbody><tr><td>مشاهده‌پذیری ضعیف</td><td>بدون metric و alert، نتیجه‌ی آزمایش قابل اعتماد نیست.</td></tr><tr><td>نبود برنامه‌ی برگشت</td><td>اگر آزمایش بد پیش برود، باید راه توقف و ترمیم داشته باشیم.</td></tr><tr><td>فشار محصولی</td><td>تیمی که زیر فشار تحویل ویژگی است، سخت برای تمرین تاب‌آوری زمان می‌گذارد.</td></tr><tr><td>فرهنگ سازمانی نامناسب</td><td>بعضی سازمان‌ها آزمایش کنترل‌شده‌ی خطا را با بی‌احتیاطی یکی می‌گیرند.</td></tr><tr><td>سیستم هنوز ساده است</td><td>برای بعضی سیستم‌های کوچک، تست‌های معمولی و پایش ساده کافی‌تر است.</td></tr><tr><td>نبود مالکیت روشن</td><td>اگر معلوم نیست مالک هر سرویس کیست، آزمایش حادثه‌محور خطرناک می‌شود.</td></tr></tbody></table>
<p>بنابراین نباید Chaos Engineering را مثل مدال افتخار معرفی کنیم. این کار زمانی ارزشمند است که روی پایه‌های درست سوار شود: مشاهده‌پذیری، مدیریت حادثه، تست‌های معمولی، برنامه‌ی برگشت، مالکیت سرویس و فرهنگ یادگیری بدون مقصرسازی.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>Chaos Engineering میان‌بر بلوغ نیست</div><div class="admonitionContent_hiHs"><p>اگر تیم هنوز مشاهده‌پذیری، rollback، runbook و واکنش روشن به حادثه ندارد، Chaos Engineering اولویت اول نیست. در چنین شرایطی، تزریق خطا می‌تواند بیشتر شلوغ‌کاری و خطر باشد تا مهندسی.</p></div></div>
<p>خود Chaos Engineering هم اگر بد اجرا شود، چند خطر دارد. یکی اعتماد کاذب است: چند آزمایش ساده موفق می‌شود و تیم فکر می‌کند سیستم واقعاً تاب‌آور است. دیگری آسیب واقعی است: آزمایش بدون محدوده و شرط توقف، خودش outage می‌سازد. خطر سوم هم نمایش مهندسی است؛ یعنی تیم به‌جای حل بدهی‌های معلوم، یک نمایش جذاب از «ما chaos داریم» اجرا می‌کند.</p>
<p>چند پرسش خوب پیش از هر آزمایش:</p>
<table><thead><tr><th>پرسش</th><th>چرا مهم است؟</th></tr></thead><tbody><tr><td>فرضیه‌ی ما دقیقاً چیست؟</td><td>بدون فرضیه، آزمایش بیشتر کنجکاوی خطرناک است.</td></tr><tr><td>وضعیت سالم سیستم را چطور می‌سنجیم؟</td><td>باید بدانیم چه چیزی نباید خراب شود.</td></tr><tr><td>محدوده‌ی اثر چقدر است؟</td><td>آزمایش باید blast radius کنترل‌شده داشته باشد.</td></tr><tr><td>چه زمانی آزمایش را متوقف می‌کنیم؟</td><td>شرط توقف باید قبل از اجرا روشن باشد.</td></tr><tr><td>چه کسی تصمیم‌گیر و پاسخ‌گوست؟</td><td>در زمان اختلال، مالکیت مبهم خطرناک است.</td></tr><tr><td>بعد از آزمایش چه چیزی تغییر می‌کند؟</td><td>اگر یادگیری به اصلاح نرسد، آزمایش ارزش کمی دارد.</td></tr></tbody></table>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی Chaos Engineering ارزشمندتر می‌شود؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی سیستم توزیع‌شده است، وابستگی‌های بیرونی دارد، چند سرویس و چند تیم درگیرند، downtime پرهزینه است، و تیم زیرساخت مشاهده‌پذیری و واکنش به حادثه‌ی قابل قبول دارد، Chaos Engineering می‌تواند ضعف‌های پنهان را پیش از حادثه‌ی واقعی آشکار کند.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی بهتر است فعلاً سراغش نرویم؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر هنوز تست‌های پایه، alertهای قابل اعتماد، runbook، rollback، backup، مالکیت سرویس‌ها و فرهنگ پاسخ‌گویی به حادثه نداریم، بهتر است اول همان پایه‌ها را بسازیم. Chaos Engineering نباید جایگزین کارهای پایه شود.</p></div></div></details>
<p>برای من، Chaos Engineering یعنی صادق بودن با واقعیت سیستم‌های واقعی: خرابی رخ می‌دهد. سؤال این نیست که آیا چیزی خراب می‌شود یا نه؛ سؤال این است که آیا پیش از کاربر و پیش از بحران، رفتار سیستم را در برابر خرابی فهمیده‌ایم یا نه. اگر این کار با فرضیه، محدوده، مشاهده‌پذیری و یادگیری انجام شود، می‌تواند سیستم و تیم را بالغ‌تر کند. اگر بدون این پایه‌ها انجام شود، فقط اسم شیک‌تری برای خطر ساختن است.</p>
<p>تا اینجا بیشتر درباره‌ی سیستم‌های فنی و تاب‌آوری آن‌ها حرف زدیم. اما بعضی پیچیدگی‌ها نه فقط از خرابی فنی، بلکه از فرایندهای انسانی و سازمانی می‌آیند: درخواست‌ها، تأییدها، گردش کارها، نقش‌ها و وضعیت‌هایی که باید بین چند نفر و چند سیستم جابه‌جا شوند. اینجا وارد Business Process Management Systems یا BPMS می‌شویم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-همهچیز-قرار-نیست-با-کدنویسی-کامل-ساخته-شود">وقتی همه‌چیز قرار نیست با کدنویسی کامل ساخته شود<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D9%87%D9%85%D9%87%DA%86%DB%8C%D8%B2-%D9%82%D8%B1%D8%A7%D8%B1-%D9%86%DB%8C%D8%B3%D8%AA-%D8%A8%D8%A7-%DA%A9%D8%AF%D9%86%D9%88%DB%8C%D8%B3%DB%8C-%DA%A9%D8%A7%D9%85%D9%84-%D8%B3%D8%A7%D8%AE%D8%AA%D9%87-%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی همه‌چیز قرار نیست با کدنویسی کامل ساخته شود" title="لینک مستقیم به وقتی همه‌چیز قرار نیست با کدنویسی کامل ساخته شود" translate="no">​</a></h2>
<p>بعد از این همه بحث درباره‌ی سرویس‌ها، زیرساخت، مهاجرت داده، چندمستاجری و تاب‌آوری، یک سؤال ساده اما مهم پیش می‌آید: آیا هر نیاز نرم‌افزاری باید با همین وزن مهندسی ساخته شود؟ آیا برای هر فرم داخلی، داشبورد سبک، اتوماسیون کوچک، پنل عملیاتی یا اتصال ساده بین دو ابزار، باید یک پروژه‌ی نرم‌افزاری کامل تعریف کنیم؟</p>
<p>در خیلی از سازمان‌ها، تیم‌های مختلف نیازهای کوچک اما واقعی دارند. تیم فروش یک فرم ثبت سرنخ می‌خواهد. تیم مالی یک گزارش هزینه می‌خواهد. تیم عملیات یک داشبورد وضعیت می‌خواهد. پشتیبانی می‌خواهد وقتی یک رخداد تکراری اتفاق افتاد، پیام خودکار بفرستد. منابع انسانی یک فرم درخواست مرخصی یا فرایند ساده‌ی تأیید می‌خواهد. اگر همه‌ی این درخواست‌ها وارد صف تیم مهندسی شود، کم‌کم تیم فنی به گلوگاه سازمان تبدیل می‌شود.</p>
<p><img decoding="async" loading="lazy" alt="درخواست‌های کوچک داخلی می‌توانند تیم مهندسی را به گلوگاه تبدیل کنند" src="https://example.com/assets/images/small-internal-tools-engineering-bottleneck-914ee8e94f87a0c99b8093d3180ed6cd.png" width="1536" height="1024" class="img_m9Pm"></p>
<p><em>گاهی مسئله این نیست که تیم مهندسی کند است؛ مسئله این است که هر نیاز کوچک و داخلی هم به همان مسیر سنگین توسعه‌ی نرم‌افزار فرستاده می‌شود.</em></p>
<p>Low-code و No-code از همین درد تغذیه می‌کنند. وعده‌ی آن‌ها جذاب است: سریع‌تر بساز، کمتر کد بزن، تیم‌های غیرمهندسی را توانمندتر کن، و همه‌چیز را برای یک فرم، داشبورد یا اتوماسیون کوچک از صفر نساز. اما همین‌جا باید مراقب باشیم؛ این بخش قرار نیست تبلیغ ابزار باشد. Low-code/No-code هم می‌تواند کمک کند، هم می‌تواند در بلندمدت سیستم را کندتر، مبهم‌تر و شکننده‌تر کند.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>Low-code/No-code یعنی ساخت بخشی از نرم‌افزارها، فرم‌ها، داشبوردها، اتوماسیون‌ها یا ابزارهای داخلی با کدنویسی کم یا بدون کدنویسی سنتی. اما «کد کمتر» به معنی «مسئولیت مهندسی کمتر» نیست.</p></div></div>
<p>ایده‌ی ساختن ابزار بدون کدنویسی کامل، چیز تازه‌ای نیست. سال‌ها قبل از اینکه اصطلاح Low-code/No-code مد شود، آدم‌ها با صفحه‌گسترده‌ها، Microsoft Access، ماکروها، فرم‌سازها و ابزارهای گزارش‌گیری تلاش می‌کردند کارهای روزمره‌ی خودشان را بدون ساختن یک نرم‌افزار کامل جلو ببرند. یعنی میل به اینکه «کاربر غیرمهندس هم بتواند چیزی بسازد» قدیمی‌تر از اسم‌های امروزی است.</p>
<p>بعدتر، ابزارهای سریع‌ساز برنامه، فرم‌سازها، ابزارهای اتوماسیون، پلتفرم‌های ساخت اپلیکیشن داخلی و سکوهای اتصال بین سرویس‌ها جدی‌تر شدند. امروز این فضا گسترده‌تر شده است: ابزارهایی مثل Retool، Airtable، AppSheet، Power Apps، Zapier، Make و n8n هرکدام از زاویه‌ای کمک می‌کنند فرم، داشبورد، پنل داخلی، جریان خودکار یا اتصال میان سرویس‌ها سریع‌تر ساخته شود.</p>
<p>n8n مثال خوبی است، چون نشان می‌دهد Low-code/No-code فقط فرم‌ساز نیست. n8n بیشتر در دسته‌ی اتوماسیون جریان کار قرار می‌گیرد: می‌توانیم چند سرویس را به هم وصل کنیم، وبهوک بگیریم، بر اساس زمان‌بندی کاری اجرا کنیم، داده را از یک ابزار به ابزار دیگر بفرستیم، پیام Telegram یا Slack ارسال کنیم، یا یک API داخلی را صدا بزنیم. مثلاً وقتی یک فرم پر شد، داده وارد CRM شود، یک پیام برای تیم فروش برود، یک وبهوک داخلی صدا زده شود، و در پایان ایمیل تأیید ارسال شود.</p>
<p>این‌ها واقعاً می‌توانند مفید باشند. برای ساخت نمونه‌ی اولیه، ابزار داخلی کم‌ریسک، اتوماسیون‌های سبک، داشبوردهای عملیاتی و کارهای تکراری، Low-code/No-code می‌تواند سرعت سازمان را بالا ببرد. تیم مهندسی هم به جای ساختن ده‌ها ابزار کوچک و کم‌ریسک، روی هسته‌ی محصول، معماری و مسائل سخت‌تر تمرکز می‌کند.</p>
<p>اما نیمه‌ی تاریک ماجرا همین‌جاست. چیزی که امروز به اسم میان‌بر ساخته می‌شود، فردا ممکن است تبدیل شود به زیرساخت نامرئی سازمان. یک جریان ساده در n8n، یک فرم در Airtable، یک پنل در Retool یا چند اتوماسیون در Zapier اول کار کمک می‌کند؛ اما کم‌کم همان‌ها می‌شوند مسیر اصلی ثبت درخواست، ارسال پیام به مشتری، تأیید مالی، تغییر وضعیت سفارش، گزارش مدیریتی یا اتصال بین دو سیستم مهم. آن وقت دیگر با یک ابزار ساده طرف نیستیم؛ با یک سیستم تولیدی طرفیم که فقط مثل سیستم تولیدی با آن رفتار نشده است.</p>
<p><img decoding="async" loading="lazy" alt="نیمه‌ی پنهان ابزارهای Low-code و No-code وقتی بدون مالکیت و کنترل رشد می‌کنند" src="https://example.com/assets/images/hidden-low-code-automation-debt-a5838b5e578947dac9168f90d4efe21e.png" width="1536" height="1024" class="img_m9Pm"></p>
<p><em>روی سطح، چند جریان سریع و تمیز می‌بینیم؛ زیر سطح، ممکن است منطق پخش‌شده، مالکیت مبهم، دسترسی‌های زیاد، نبود تست و بدهی نامرئی شکل گرفته باشد.</em></p>
<p>نقد ما اصل این ابزارها نیست؛ نقد ما استفاده‌ی بی‌مالکیت و بی‌چرخه‌ی عمر از آن‌هاست. نقد تند اینجاست: این ابزارها اغلب کار را از تیم مهندسی کم نمی‌کنند؛ کار را از جلوی چشم تیم مهندسی پنهان می‌کنند. در کوتاه‌مدت صف مهندسی را دور می‌زنند، اما در بلندمدت صف تازه‌ای می‌سازند: صف فهمیدن اینکه چه کسی کجا چه چیزی ساخته، چرا خراب شده، به چه داده‌ای دسترسی دارد، کدام جریان منطق اصلی کسب‌وکار را اجرا می‌کند، و اگر ابزار از کار افتاد چه کسی باید پاسخ‌گو باشد.</p>
<p>مشکل Low-code/No-code این نیست که «کد کم دارد»؛ مشکل وقتی شروع می‌شود که «مسئولیت مهندسی» هم کم‌کم حذف شود. هر چیزی که در مسیر واقعی کسب‌وکار قرار می‌گیرد، نرم‌افزار است؛ حتی اگر با کشیدن چند گره ساخته شده باشد. اگر آن چیز مشتری را تحت تأثیر قرار می‌دهد، داده‌ی حساس می‌خواند، پول جابه‌جا می‌کند، وضعیت سفارش را تغییر می‌دهد یا فرایند روزانه‌ی تیمی را پیش می‌برد، دیگر نمی‌توان با آن مثل یک فایل موقت برخورد کرد.</p>
<p>در n8n و ابزارهای مشابه، خطر اصلی این نیست که جریان ساخته‌ایم؛ خطر این است که جریان تبدیل به منطق کسب‌وکار شده، اما هنوز با آن مثل یک آزمایش کوچک برخورد می‌کنیم. این جریان‌ها معمولاً مجوز و اطلاعات اتصال دارند، به APIهای مختلف وصل می‌شوند، داده جابه‌جا می‌کنند و گاهی تصمیم‌های مهم می‌گیرند. اگر مالکیت، کنترل دسترسی، نسخه‌بندی، پشتیبان‌گیری، پایش و هشدار نداشته باشند، یک گره کوچک، یک مجوز منقضی‌شده یا یک تغییر در API بیرونی می‌تواند یک فرایند مهم را بخواباند.</p>
<p>پس اگر قرار است Low-code/No-code وارد سازمان شود، باید با حاکمیت و مرز روشن وارد شود. باید معلوم باشد چه کسی مالک هر جریان یا ابزار است، چه داده‌ای خوانده می‌شود، چه دسترسی‌هایی داده شده، تغییرات چطور بازبینی می‌شوند، خطاها کجا دیده می‌شوند، و چه زمانی یک ابزار داخلی باید از حالت موقت خارج شود و به سیستم جدی‌تر تبدیل شود.</p>
<p><img decoding="async" loading="lazy" alt="Low-code و No-code وقتی مفیدتر است که با مالکیت، کنترل دسترسی، بازبینی و پایش همراه باشد" src="https://example.com/assets/images/low-code-no-code-with-governance-6202dfca88ee036aadc4b8c54405092e.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>سرعت خوب است، اما بدون کنترل می‌تواند خطرناک شود. ابزارهای کم‌کدنویسی هم به مالکیت، دسترسی، بازبینی، نسخه‌بندی، پایش و پشتیبانی نیاز دارند.</em></p>
<p>Low-code/No-code برای همه‌چیز بد نیست؛ اتفاقاً اگر درست استفاده شود، بسیار مفید است. برای ساخت نمونه‌ی اولیه، فرم‌های داخلی کم‌ریسک، داشبوردهای سبک، اتوماسیون‌های ساده و ابزارهای عملیاتی کوچک می‌تواند انتخاب خوبی باشد. اما هرچه ابزار به داده‌ی حساس، تراکنش مالی، منطق اصلی محصول، مشتری واقعی یا عملیات حیاتی نزدیک‌تر شود، باید سخت‌گیرتر شویم. آنجا دیگر سرعت اولیه کافی نیست؛ چرخه‌ی عمر مهم است.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>میان‌برها گاهی مسیر را طولانی‌تر می‌کنند</div><div class="admonitionContent_hiHs"><p>ساختن یک جریان در چند ساعت شاید سریع باشد، اما اگر مالکیت، تست، نسخه‌بندی، امنیت، مشاهده‌پذیری و برنامه‌ی خطا نداشته باشد، هزینه‌اش در زمان تغییر، خطا و مهاجرت پس گرفته می‌شود.</p></div></div>
<p>این نگاه، ما را از یک سوءبرداشت دور می‌کند: Low-code/No-code جایگزین مهندسی نرم‌افزار نیست؛ فقط بعضی شکل‌های ساخت نرم‌افزار را برای بعضی مسئله‌ها سریع‌تر و در دسترس‌تر می‌کند. اگر مسئله کوچک، داخلی، کم‌ریسک و قابل جایگزینی است، این ابزارها می‌توانند عالی باشند. اگر مسئله حیاتی، حساس، پیچیده و بلندمدت است، باید همان سطحی از فکر مهندسی را وارد کنیم که برای هر سیستم مهم دیگری لازم داریم.</p>
<p>برای من، Low-code/No-code نه ناجی است و نه دشمن. یک ابزار است؛ ابزاری که وقتی برای مسئله‌ی درست، با مالکیت روشن و مرز مشخص استفاده شود، سرعت می‌سازد. اما وقتی جای معماری، مالکیت و چرخه‌ی عمر نرم‌افزار را بگیرد، در بلندمدت همان چیزی را تولید می‌کند که قرار بود حل کند: کندی، آشفتگی و وابستگی.</p>
<p>اینجا پل بخش بعد شکل می‌گیرد. Low-code/No-code خانواده‌ی وسیعی از ابزارهاست؛ از فرم و داشبورد تا اتوماسیون و اتصال بین سرویس‌ها. اما وقتی مسئله از چند اتصال ساده فراتر می‌رود و تبدیل می‌شود به فرایند رسمی سازمانی با نقش‌ها، وضعیت‌ها، تأییدها، زمان‌بندی‌ها، گزارش مدیریتی و چرخه‌ی عمر روشن، وارد دنیای BPMS می‌شویم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-کار-فقط-کد-نیست-فرایند-هم-هست">وقتی کار فقط کد نیست، فرایند هم هست<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%DA%A9%D8%A7%D8%B1-%D9%81%D9%82%D8%B7-%DA%A9%D8%AF-%D9%86%DB%8C%D8%B3%D8%AA-%D9%81%D8%B1%D8%A7%DB%8C%D9%86%D8%AF-%D9%87%D9%85-%D9%87%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به وقتی کار فقط کد نیست، فرایند هم هست" title="لینک مستقیم به وقتی کار فقط کد نیست، فرایند هم هست" translate="no">​</a></h2>
<p>در بخش قبل گفتیم Low-code و No-code می‌توانند برای فرم‌ها، داشبوردها، اتوماسیون‌های سبک و ابزارهای داخلی مفید باشند؛ اما همان‌جا هم دیدیم که اگر این ابزارها بدون مالکیت و مرز روشن رشد کنند، آرام‌آرام تبدیل می‌شوند به زیرساخت نامرئی سازمان. حالا یک قدم جلوتر می‌رویم. فرض کنیم مسئله دیگر فقط یک فرم یا یک جریان ساده نیست؛ مسئله خودِ فرایند کسب‌وکار است.</p>
<p>تفاوت BPMS با یک جریان ساده این است که BPMS بیشتر درباره‌ی زندگی یک پرونده در طول زمان است؛ نه فقط اجرای چند عمل پشت سر هم. یک پرونده ممکن است میان چند نقش انسانی، چند تصمیم، چند وضعیت، چند سیستم و چند زمان‌بندی حرکت کند.</p>
<p>یک مثال ساده را در نظر بگیریم: مرجوعی کالا. اول کار شاید فقط یک فرم باشد. مشتری درخواست می‌دهد، پشتیبانی بررسی می‌کند و نتیجه را اعلام می‌کند. اما بعد سازمان رشد می‌کند و فرایند شاخه پیدا می‌کند. اگر مبلغ کالا زیاد بود، مدیر باید تأیید کند. اگر دلیل مرجوعی خرابی بود، تیم کنترل کیفیت باید نظر بدهد. اگر تأیید شد، تیم مالی باید بازپرداخت انجام دهد. اگر سه روز گذشت و اقدامی نشد، باید هشدار بخورد. اگر رد شد، دلیل رد باید ثبت شود. اگر تأیید شد، باید به سیستم انبار یا مالی پیام برود.</p>
<p>اینجا دیگر با یک فرم ساده طرف نیستیم. با کاری طرفیم که بین چند نقش انسانی، چند تصمیم، چند وضعیت، چند سیستم و چند زمان‌بندی حرکت می‌کند. اگر این مسیر در ایمیل، اکسل، پیام‌رسان، تماس تلفنی، چند اسکریپت و حافظه‌ی آدم‌ها پخش شود، بعد از مدتی هیچ‌کس دقیق نمی‌داند هر درخواست کجاست، چرا گیر کرده، چه کسی باید اقدام کند و قانون تصمیم دقیقاً کجا تعریف شده است.</p>
<p><img decoding="async" loading="lazy" alt="فرایند کسب‌وکار وقتی بین ابزارهای مختلف پخش می‌شود" src="https://example.com/assets/images/business-process-scattered-across-tools-3bc059cae12bea2a804e4879e357599d.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>وقتی یک فرایند واحد در ابزارهای مختلف پخش شود، دیدن وضعیت، مسئول مرحله‌ی بعد و دلیل توقف کار سخت می‌شود.</em></p>
<p>BPMS یا سامانه‌ی مدیریت فرایندهای کسب‌وکار، از همین درد شروع می‌شود. ایده این است که فرایندهای مهم فقط در ذهن آدم‌ها، ایمیل‌ها، فایل‌های پراکنده و کدهای نامنظم زندگی نکنند؛ بلکه مدل‌سازی، اجرا، پایش و اصلاح شوند. BPMS تلاش می‌کند جریان کار را به چیزی تبدیل کند که بتوانیم آن را ببینیم، درباره‌اش حرف بزنیم، وضعیتش را بفهمیم و تغییرش را کنترل کنیم.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>BPMS ابزاری است برای مدل‌سازی، اجرای قابل پیگیری، پایش و تغییر فرایندهایی که معمولاً چند مرحله، چند نقش انسانی، چند تصمیم و چند اتصال سیستمی دارند.</p></div></div>
<p>میل به مدیریت فرایندها چیز تازه‌ای نیست. سازمان‌ها همیشه فرم، ارجاع، تأیید، پرونده و گردش کار داشته‌اند. در دنیای نرم‌افزار هم از سال‌ها قبل سیستم‌های جریان کار، موتورهای قوانین، ابزارهای BPM و زبان‌های مدل‌سازی فرایند شکل گرفتند. استانداردهایی مثل BPMN کمک کردند فرایندها به شکل نمودارهایی مدل شوند که هم برای تحلیل‌گر کسب‌وکار قابل فهم باشند، هم برای تیم فنی قابل تبدیل به اجرا یا پیاده‌سازی. امروز ابزارهایی مثل Camunda، Flowable، Bonita و ابزارهای مشابه تلاش می‌کنند همین ایده را در سیستم‌های واقعی اجراپذیرتر کنند.</p>
<p>اما BPMS فقط کشیدن فلوچارت نیست. اگر فقط نمودار بکشیم و بعد همه‌چیز همچنان در ایمیل و کدهای پراکنده اجرا شود، مسئله حل نشده است. BPMS وقتی معنا دارد که فرایند بتواند وضعیت داشته باشد، در مرحله‌ای منتظر بماند، به نقش مشخصی وظیفه بدهد، بر اساس تصمیم مسیر عوض کند، به سرویس بیرونی پیام بدهد، زمان‌سنج و هشدار داشته باشد و قابل پایش باشد.</p>
<p>در همان مثال مرجوعی کالا، می‌توانیم مسیر را روشن‌تر کنیم: درخواست ثبت می‌شود، پشتیبانی بررسی اولیه انجام می‌دهد، اگر نیاز به تأیید داشت مدیر وارد می‌شود، اگر تأیید شد تیم مالی اقدام می‌کند و در پایان وضعیت درخواست بسته می‌شود. در هر لحظه معلوم است درخواست در چه وضعیتی است، مسئول مرحله‌ی بعد کیست و چرا از یک مسیر خاص عبور کرده است.</p>
<p><img decoding="async" loading="lazy" alt="فرایند مدل‌شده با BPMS، همراه با نقش‌ها و وضعیت‌ها" src="https://example.com/assets/images/bpms-modeled-process-with-roles-and-states-535a1b0c76f102a83cc221eee19eee2b.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در BPMS، فرایند فقط چند دستور پشت سر هم نیست؛ یک مسیر دارای وضعیت، نقش، تصمیم و قابل پیگیری است.</em></p>
<p>مزیت اصلی BPMS این نیست که کد را حذف می‌کند. مزیت اصلی این است که فرایند را به یک موجود قابل مشاهده و قابل بحث تبدیل می‌کند. آدم محصول، عملیات، حقوقی، مالی و فنی می‌توانند درباره‌ی یک مدل مشترک حرف بزنند. می‌توانند ببینند گلوگاه کجاست، چه مرحله‌ای زیاد طول می‌کشد، کدام تصمیم‌ها زیاد رد می‌شوند، و کجا باید نقش، قانون یا مسیر فرایند تغییر کند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>وقتی BPMS ارزشمند می‌شود</div><div class="admonitionContent_hiHs"><p>BPMS وقتی ارزشمند است که مسئله‌ی اصلی، خود جریان کار باشد: فرایندی چندمرحله‌ای، چندنقشی، تغییرپذیر، زمان‌مند و نیازمند ردیابی. اگر فقط یک فرم ساده یا یک اتوماسیون کوچک داریم، شاید BPMS از همان ابتدا بیش از حد سنگین باشد.</p></div></div>
<p>اما حالا نوبت نقد جدی است. BPMS نباید تبدیل شود به جایی که منطق اصلی محصول در آن دفن می‌شود. این خطر واقعی است. چون ظاهر BPMS معمولاً شفاف و فریبنده است: چند نمودار، چند کار، چند فرم، چند تصمیم و چند اتصال. اما اگر مراقب نباشیم، همان منطق مهمی که باید در سرویس‌های قابل تست، قابل مشاهده و دامنه‌محور باشد، در میان نمودارها، کارهای اسکریپتی، قانون‌های پراکنده و اتصال‌های پنهان دفن می‌شود.</p>
<p>مشکل BPMS این نیست که فرایند را مدل می‌کند؛ مشکل وقتی شروع می‌شود که مدل فرایند جای طراحی دامنه و معماری سیستم را بگیرد. اگر قواعد اصلی محصول، محاسبات حساس، تصمیم‌های امنیتی، منطق مالی یا تغییرات مهم وضعیت را بی‌حساب داخل BPMS ببریم، بعد از مدتی سیستم نه ساده‌تر، که سخت‌تر از کد معمولی قابل فهم و تغییر می‌شود.</p>
<p><img decoding="async" loading="lazy" alt="ضدالگوی فرایند اسپاگتی در BPMS" src="https://example.com/assets/images/bpms-process-spaghetti-anti-pattern-d27287ba8caad48dd586c26546b6eb7c.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>گاهی BPMS پیچیدگی را از کد بیرون نمی‌کشد؛ فقط همان پیچیدگی را در نمودارها، کارها و اتصال‌های پنهان قایم می‌کند.</em></p>
<p>فرایندها به مرور شاخه و شرط پیدا می‌کنند. یک استثنا برای مشتری مهم اضافه می‌شود، بعد یک مسیر ویژه برای قراردادهای بزرگ، بعد یک قانون موقت برای تیم مالی، بعد یک اسکریپت برای اصلاح داده، بعد یک اتصال به CRM، بعد یک اتصال به سرویس پرداخت. بعد از مدتی نموداری که قرار بود شفاف‌کننده باشد، خودش تبدیل می‌شود به جنگلی از فلش‌ها و شرط‌ها. تغییر ساده نیازمند فهم چند مدل، چند نقش، چند فرم، چند اتصال بیرونی و چند قانون پنهان می‌شود.</p>
<p>پس مرز سالم این است: BPMS بهتر است ارکسترکننده‌ی فرایند باشد، نه مالک همه‌ی منطق کسب‌وکار. اگر قرار است اعتبار مالی مشتری محاسبه شود، موجودی کیف پول تغییر کند، سیاست امنیتی حساس اعمال شود یا تصمیمی با اثر مالی جدی گرفته شود، بهتر است این منطق در سرویس‌های دامنه‌محور، قابل تست و قابل مشاهده بماند. BPMS می‌تواند آن سرویس‌ها را صدا بزند، مسیر را هماهنگ کند و وضعیت فرایند را نگه دارد، اما نباید بی‌دلیل جای آن‌ها را بگیرد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>BPMS نباید اصل سیستم شود</div><div class="admonitionContent_hiHs"><p>BPMS برای هماهنگی فرایند خوب است، نه برای دفن کردن همه‌ی منطق محصول. اگر منطق اصلی، تست‌پذیری، مالکیت و مشاهده‌پذیری را قربانی نمودارهای ظاهراً ساده کنیم، فقط پیچیدگی را از یک جا به جای دیگر منتقل کرده‌ایم.</p></div></div>
<p>برای فرایندهای ساده، از روز اول BPMS لازم نیست. اگر یک فرم داخلی، یک تأیید ساده یا یک اتوماسیون کم‌ریسک داریم، شاید همان ابزارهای ساده‌تر بخش قبل کافی باشند. BPMS وقتی ارزش پیدا می‌کند که فرایند واقعاً طولانی، چندنقشی، تغییرپذیر، حساس به زمان و نیازمند حسابرسی باشد. در غیر این صورت، ممکن است ابزار سنگینی وارد کنیم که خودِ ابزار از مسئله بزرگ‌تر شود.</p>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی BPMS انتخاب خوبی است؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی فرایند چندمرحله‌ای است، چند نقش انسانی دارد، وضعیت آن باید در طول زمان نگه‌داری شود، تأخیرها و گلوگاه‌ها مهم‌اند، حسابرسی لازم است، و تغییر فرایند باید برای تیم‌های فنی و غیرفنی قابل بحث باشد، BPMS می‌تواند انتخاب مناسبی باشد.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی بهتر است سراغ BPMS نرویم؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر فرایند ساده است، تغییر کمی دارد، یک سرویس کوچک می‌تواند آن را روشن و قابل تست پیاده کند، یا هنوز خود مسئله به اندازه‌ی کافی تثبیت نشده، شروع با BPMS ممکن است زود باشد. در چنین حالتی ابزارهای سبک‌تر، کدنویسی مستقیم یا حتی یک جریان ساده ممکن است انتخاب بهتری باشد.</p></div></div></details>
<p>برای من، BPMS یعنی پذیرفتن اینکه در بعضی سیستم‌ها، خودِ فرایند به اندازه‌ی کد مهم است. باید بدانیم کار کجاست، دست کیست، چرا متوقف شده و با چه قانونی جلو می‌رود. اما BPMS هم مثل Low-code/No-code قرار نیست جای فکر مهندسی را بگیرد. وقتی درست استفاده شود، فرایند را شفاف و قابل پایش می‌کند. وقتی بد استفاده شود، پیچیدگی را در نمودارها پنهان می‌کند و به سیستم ظاهراً تصویری اما عملاً سخت‌فهم می‌رسیم.</p>
<p>تا اینجا درباره‌ی ابزارهایی حرف زدیم که ساخت نرم‌افزار، عملیات، فرایند و اتوماسیون را تغییر می‌دهند. اما موج تازه‌تری هم وارد مهندسی نرم‌افزار شده است: هوش مصنوعی. حالا سؤال این است که آیا هوش مصنوعی می‌تواند خود فرایند ساخت نرم‌افزار را تغییر دهد؟ اینجا وارد AI4SE می‌شویم؛ یعنی استفاده از هوش مصنوعی برای کمک به مهندسی نرم‌افزار.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-هوش-مصنوعی-وارد-کارگاه-مهندسی-نرمافزار-میشود">وقتی هوش مصنوعی وارد کارگاه مهندسی نرم‌افزار می‌شود<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D9%87%D9%88%D8%B4-%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C-%D9%88%D8%A7%D8%B1%D8%AF-%DA%A9%D8%A7%D8%B1%DA%AF%D8%A7%D9%87-%D9%85%D9%87%D9%86%D8%AF%D8%B3%DB%8C-%D9%86%D8%B1%D9%85%D8%A7%D9%81%D8%B2%D8%A7%D8%B1-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی هوش مصنوعی وارد کارگاه مهندسی نرم‌افزار می‌شود" title="لینک مستقیم به وقتی هوش مصنوعی وارد کارگاه مهندسی نرم‌افزار می‌شود" translate="no">​</a></h2>
<p>حتی اگر معماری، زیرساخت و فرایندها را بهتر کنیم، خود کار روزمره‌ی مهندسی نرم‌افزار هنوز سنگین است. کد بیشتر شده، درخواست‌های ادغام یا Pull Requestها بزرگ‌تر شده‌اند، سرویس‌ها به هم وابسته‌تر شده‌اند، مستندات زود کهنه می‌شوند، تست‌ها باید به‌روز بمانند، بازآرایی کد یا refactor خطر شکستن رفتار دارد، و بازبینی تغییرات وقت و دقت زیادی می‌خواهد. حتی اگر تیم خوبی داشته باشیم، بخشی از کارهای روزانه تکراری، فرساینده و مستعد خطای انسانی‌اند.</p>
<p>اینجا AI4SE وارد می‌شود. AI4SE یعنی استفاده از هوش مصنوعی برای کمک به فعالیت‌های مهندسی نرم‌افزار؛ نه حذف مهندسی نرم‌افزار. هوش مصنوعی می‌تواند کنار تیم بنشیند، بعضی کارها را سریع‌تر کند، بعضی مسیرهای فهم را کوتاه‌تر کند و بعضی خطاها را زودتر به چشم بیاورد. اما تصمیم، مالکیت و مسئولیت همچنان با تیم مهندسی است.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>AI4SE یعنی به‌کارگیری هوش مصنوعی برای بهتر، سریع‌تر یا قابل‌اعتمادتر کردن فعالیت‌های مهندسی نرم‌افزار؛ از فهم کد و تولید تست تا بازبینی، مستندسازی، تحلیل خطا و کمک به نگه‌داری سیستم.</p></div></div>
<p>این مسیر از هیچ‌جا ناگهان با ChatGPT شروع نشد. قبل از موج مدل‌های زبانی بزرگ، سال‌ها ابزارهای تحلیل ایستا، ابزارهای سبک بررسی کد، جست‌وجوی کد، تکمیل خودکار، پیشنهاد بازآرایی، تشخیص خطا و تولید تست وجود داشتند. بخشی از آن‌ها قاعده‌محور بودند، بخشی از تکنیک‌های برنامه‌کاوی و یادگیری ماشین استفاده می‌کردند، و بخشی هم در محیط‌های توسعه به شکل پیشنهادهای محدود دیده می‌شدند.</p>
<p>بعدتر با رشد یادگیری عمیق و مدل‌های آموزش‌دیده روی کد، کمک‌های هوشمندتر ممکن شد. برای خیلی از توسعه‌دهنده‌ها، GitHub Copilot یک نقطه‌ی عطف بود؛ ابزاری که در ۲۰۲۱ به‌عنوان یک «همکار برنامه‌نویس هوشمند» معرفی شد و تجربه‌ی تولید و تکمیل کد با مدل زبانی را وارد کار روزمره‌ی توسعه‌دهندگان کرد. اما موج بعدی فقط تکمیل خودکار نبود. با رشد مدل‌های زبانی بزرگ، مسئله از ادامه دادن چند خط کد به فهمیدن شرح مسئله، خواندن چند فایل، پیشنهاد وصله، نوشتن تست، توضیح تغییر و حتی اجرای چند گام روی مخزن کد نزدیک شد.</p>
<p><img decoding="async" loading="lazy" alt="هوش مصنوعی در کارگاه مهندسی نرم‌افزار" src="https://example.com/assets/images/ai4se-software-engineering-workbench-161e3460c55cbea5e0fe79eaad3eff72.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>هوش مصنوعی می‌تواند کنار مهندس باشد؛ برای پیشنهاد، توضیح، بررسی و یادآوری. اما جای تصمیم مهندسی، مالکیت و فهم سیستم را نمی‌گیرد.</em></p>
<p>کاربردهای AI4SE فقط به تولید کد محدود نیست. گاهی ارزش آن در فهمیدن کد موجود است: خلاصه کردن یک فایل پیچیده، توضیح مسیر اجرای یک قابلیت، یا پیدا کردن ارتباط چند کلاس و سرویس. گاهی در تست کمک می‌کند: پیشنهاد حالت‌های مرزی، ساخت داده‌ی آزمایشی یا یادآوری سناریوهایی که از قلم افتاده‌اند. گاهی در بازبینی کد مفید است: خلاصه کردن درخواست ادغام، برجسته کردن تغییرهای پرریسک، پیشنهاد تست، یا پیدا کردن ناسازگاری با قرارداد API. گاهی هم در مستندسازی و عیب‌یابی کمک می‌کند: نوشتن پیش‌نویس مستندات، خلاصه کردن لاگ‌ها، دسته‌بندی خطاها یا کوتاه کردن مسیر بررسی حادثه.</p>
<p>اما هرچه از «تولید چند خط کد» به «تصمیم مهندسی» نزدیک می‌شویم، کیفیت زمینه مهم‌تر می‌شود. یک مدل اگر فقط تغییرات کد را ببیند، شاید بتواند ظاهر تغییر را توضیح دهد؛ اما نمی‌تواند همیشه بفهمد این تغییر با نیت شرح مسئله، قرارداد API، معماری سرویس، تست‌های موجود، محدودیت‌های دامنه و تجربه‌ی کاربر سازگار است یا نه. در کارهای جدی، مدل فقط به دستور خوب نیاز ندارد؛ به زمینه‌ی خوب نیاز دارد.</p>
<p>این نکته مخصوصاً در بازبینی کد پررنگ است. بازبینی خوب فقط خواندن تغییرات کد نیست. بازبینی یعنی فهمیدن اینکه چرا این تغییر انجام شده، روی چه رفتاری اثر می‌گذارد، چه قراردادی را تغییر می‌دهد، کدام تست باید اضافه شود، چه چیزی ممکن است در سرویس دیگری بشکند و آیا این تغییر با مسیر بلندمدت سیستم سازگار است یا نه.</p>
<p><img decoding="async" loading="lazy" alt="بازبینی کد با آگاهی از زمینه" src="https://example.com/assets/images/ai4se-context-aware-code-review-5eb3171d305a2f8aa6802a883aad689b.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>هوش مصنوعی وقتی بهتر بازبینی می‌کند که فقط تغییرات کد را نبیند؛ بلکه شرح مسئله، تست‌ها، مستندات، قراردادها، معماری و لاگ‌ها هم بخشی از زمینه باشند.</em></p>
<p>برای همین، بازبین هوشمند خوب باید بیشتر از یک تولیدکننده‌ی نظر باشد. باید بتواند نیت تغییر را بفهمد، سؤال‌های درست بپرسد، جاهایی را که نیاز به تست دارند برجسته کند، و اگر زمینه کافی نیست، با اعتمادبه‌نفس بی‌جا حکم ندهد. حتی در بهترین حالت هم نقش آن کمک به بازبین انسانی است، نه جایگزینی کامل او.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>بازبین هوشمند خوب، زمینه می‌خواهد</div><div class="admonitionContent_hiHs"><p>بازبینی فقط خواندن تغییرات کد نیست. بازبینی یعنی فهمیدن نیت تغییر، اثر آن روی رفتار سیستم، قراردادها، تست‌ها و نگه‌داری آینده. بدون زمینه‌ی کافی، بازبین هوشمند هم به‌راحتی به نظرهای عمومی و کم‌ارزش می‌رسد.</p></div></div>
<p>در تولید تست هم همین مرز وجود دارد. هوش مصنوعی می‌تواند حالت آزمایشی پیشنهاد بدهد، اما تست خوب فقط زیاد بودن تعداد تست‌ها نیست. تست خوب باید رفتار مهم را تثبیت کند، حالت مرزی واقعی را پوشش دهد و نسبت به بازآرایی‌های بی‌اثر شکننده نباشد. اگر هوش مصنوعی تست‌هایی بسازد که فقط جزئیات پیاده‌سازی را بررسی می‌کنند، ممکن است تعداد تست‌ها زیاد شود، اما اعتماد ما به تغییر بیشتر نشود. حتی بدتر، ممکن است بازآرایی‌های خوب را سخت‌تر کند، چون تست‌ها به جای رفتار، به شکل داخلی کد چسبیده‌اند.</p>
<p>هدف تست، افزایش اعتماد به تغییر است؛ نه فقط افزایش تعداد فایل‌های تست. این همان جایی است که قضاوت مهندسی مهم می‌شود. مدل می‌تواند پیشنهاد بدهد، اما تیم باید تشخیص دهد آیا این تست واقعاً رفتار مهم را پوشش می‌دهد یا فقط خروجی ظاهراً قانع‌کننده تولید کرده است.</p>
<p>حالا نقد اصلی بخش: AI4SE وقتی خطرناک می‌شود که سازمان‌ها سرعت تولید خروجی را با کیفیت مهندسی اشتباه بگیرند. مدل می‌تواند سریع کد بنویسد، اما سریع‌تر نوشتن کد لزوماً یعنی سریع‌تر رسیدن به نرم‌افزار قابل نگه‌داری نیست. ممکن است خروجی زیاد شود، اما بازبینی سخت‌تر شود. ممکن است کد بیشتری تولید شود، اما کسی عمیقاً مالک آن نباشد. ممکن است تست بیشتری ساخته شود، اما رفتار مهم پوشش داده نشود. ممکن است مستندات روان‌تر شوند، اما دقیق نباشند.</p>
<p><img decoding="async" loading="lazy" alt="هزینه‌ی پنهان نگه‌داری کد تولیدشده با هوش مصنوعی" src="https://example.com/assets/images/ai-generated-code-hidden-maintenance-cost-f887b2c8ba717825c86fecad978c200b.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>روی سطح، تولید کد، تست و مستندات سریع‌تر می‌شود؛ زیر سطح، اگر مالکیت و بازبینی نباشد، بدهی نگه‌داری و ابهام سیستم بیشتر می‌شود.</em></p>
<p>خطر AI4SE فقط این نیست که مدل اشتباه می‌کند. خطر بزرگ‌تر این است که مدل با اعتمادبه‌نفس اشتباه می‌کند و ما هم از خستگی، فشار زمانی یا جذابیت سرعت، آن را می‌پذیریم. خروجی مدل معمولاً خوش‌ساخت و قانع‌کننده است؛ همین باعث می‌شود خطاهایش دیرتر دیده شود. کدی که با اطمینان نوشته شده، تستی که نام خوبی دارد، یا مستندی که روان است، الزاماً درست نیست.</p>
<p>هوش مصنوعی می‌تواند تولید کد را ارزان‌تر کند، اما اگر فهم سیستم گران‌تر شود، تیم در مجموع کندتر شده است. این جمله برای من مرکز نقد AI4SE است. اگر هر قابلیت با سرعت بیشتری کد تولید کند، اما بازبینی، نگه‌داری، عیب‌یابی و فهم تغییر سخت‌تر شود، ما فقط بدهی فنی را سریع‌تر تولید کرده‌ایم.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>سرعت تولید، همان کیفیت مهندسی نیست</div><div class="admonitionContent_hiHs"><p>هوش مصنوعی می‌تواند خروجی بیشتری تولید کند؛ اما خروجی بیشتر اگر بی‌مالک، کم‌فهم، کم‌تست یا ناسازگار با معماری باشد، به کیفیت تبدیل نمی‌شود. گاهی فقط هزینه‌ی نگه‌داری را به آینده منتقل می‌کند.</p></div></div>
<p>پس مرز سالم چیست؟ هوش مصنوعی برای کارهای کمکی، تکراری، توضیحی و اکتشافی بسیار مفید است؛ جایی که انسان هنوز تصمیم نهایی را می‌گیرد. خلاصه کردن درخواست ادغام، پیشنهاد حالت‌های آزمایشی، تولید پیش‌نویس مستندات، توضیح کد قدیمی، کمک به جست‌وجو در مخزن کد، یا ساخت نمونه‌ی اولیه می‌تواند عالی باشد. اما تصمیم‌های حساس معماری، امنیت، داده، رفتار دامنه و تغییرات پرریسک نباید فقط به خروجی مدل سپرده شوند.</p>
<p>هوش مصنوعی نمی‌تواند جای خالی معماری نامفهوم، تست‌های بد، مستندات فرسوده و مالکیت مبهم را معجزه‌وار پر کند؛ گاهی فقط آن‌ها را سریع‌تر تکثیر می‌کند. اگر مخزن کد بی‌نظم است، اگر قراردادها روشن نیستند، اگر تست‌ها شکننده‌اند، اگر شرح مسئله‌ها مبهم‌اند، مدل هم بر همان ابهام سوار می‌شود و خروجی‌هایی می‌دهد که شاید زیبا باشند، اما الزاماً قابل اتکا نیستند.</p>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی AI4SE واقعاً کمک می‌کند؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی مسئله محدود و روشن است، زمینه‌ی کافی داریم، خروجی مدل بازبینی می‌شود، و تیم از هوش مصنوعی برای کاهش کار تکراری یا افزایش فهم استفاده می‌کند، نه برای حذف قضاوت مهندسی. در این حالت هوش مصنوعی می‌تواند سرعت فهم، مستندسازی، تست‌نویسی اولیه و بررسی تغییرات را بهتر کند.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی AI4SE خطرناک می‌شود؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی خروجی مدل بدون بازبینی وارد کد می‌شود، وقتی زمینه ناقص است، وقتی مدل به جای بازبین یا معمار تصمیم می‌گیرد، وقتی تست‌های تولیدی فقط ظاهر اعتماد می‌سازند، یا وقتی سازمان به جای بهتر کردن معماری و کیفیت، فقط انتظار دارد هوش مصنوعی مشکلات ساختاری را جبران کند.</p></div></div></details>
<p>برای من، AI4SE یعنی آوردن یک دستیار قدرتمند به کارگاه مهندسی نرم‌افزار. این دستیار می‌تواند سریع بخواند، سریع پیشنهاد بدهد، سریع خلاصه کند و سریع پیش‌نویس بسازد. اما نرم‌افزار خوب فقط از سرعت ساخته نمی‌شود. نرم‌افزار خوب به فهم، مالکیت، بازبینی، تست، طراحی و تصمیم‌گیری مسئولانه نیاز دارد. هوش مصنوعی اگر این‌ها را تقویت کند، ارزشمند است؛ اگر جای آن‌ها بنشیند، خطرناک است.</p>
<p>تا اینجا گفتیم هوش مصنوعی چطور می‌تواند به مهندسی نرم‌افزار کمک کند. اما سؤال برعکس هم وجود دارد: وقتی خود محصول ما مبتنی بر هوش مصنوعی است، چطور باید آن را مهندسی کنیم؟ مدل‌ها رفتار کاملاً قطعی ندارند، داده و دستور و ارزیابی بخشی از سیستم می‌شوند، و رفتار خروجی همیشه مثل کد معمولی قابل پیش‌بینی نیست. اینجا وارد SE4AI می‌شویم: مهندسی نرم‌افزار برای سیستم‌های هوش مصنوعی.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-خود-هوش-مصنوعی-هم-به-مهندسی-نیاز-دارد">وقتی خود هوش مصنوعی هم به مهندسی نیاز دارد<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D8%AE%D9%88%D8%AF-%D9%87%D9%88%D8%B4-%D9%85%D8%B5%D9%86%D9%88%D8%B9%DB%8C-%D9%87%D9%85-%D8%A8%D9%87-%D9%85%D9%87%D9%86%D8%AF%D8%B3%DB%8C-%D9%86%DB%8C%D8%A7%D8%B2-%D8%AF%D8%A7%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی خود هوش مصنوعی هم به مهندسی نیاز دارد" title="لینک مستقیم به وقتی خود هوش مصنوعی هم به مهندسی نیاز دارد" translate="no">​</a></h2>
<p>در بخش قبل گفتیم هوش مصنوعی چطور می‌تواند به مهندسی نرم‌افزار کمک کند: در فهم کد، نوشتن تست، بازبینی، مستندسازی و عیب‌یابی. حالا سؤال را برعکس می‌کنیم. اگر خودِ محصول ما مبتنی بر هوش مصنوعی باشد چه؟ اگر قابلیت اصلی سیستم نه فقط چند خط کد، بلکه یک مدل، دستور مدل، داده، بازیابی دانش، ارزیابی و سیاست محصولی باشد، مهندسی نرم‌افزار چه نقشی دارد؟</p>
<p>تصور کنیم تیمی می‌خواهد یک قابلیت هوشمند به محصول اضافه کند: چت‌بات پشتیبانی، سیستم پیشنهاددهنده، تشخیص تقلب، خلاصه‌سازی تیکت‌ها، تحلیل احساسات پیام‌های کاربران، جست‌وجوی هوشمند، یا یک قابلیت مبتنی بر مدل زبانی بزرگ برای تولید پاسخ. در دمو همه‌چیز جذاب است. چند مثال خوب جواب می‌گیرند، تیم محصول هیجان‌زده می‌شود و همه حس می‌کنند «فقط کافی است مدل را صدا بزنیم».</p>
<p>اما محصول واقعی با دمو فرق دارد. ورودی‌ها تمیز و قابل پیش‌بینی نیستند. کاربران سؤال‌های عجیب می‌پرسند. داده‌ی زمینه ناقص است. خروجی مدل گاهی غلط اما قانع‌کننده است. دستورهای مدل تغییر می‌کنند. نسخه‌ی مدل عوض می‌شود. معیار کیفیت همیشه یک عدد ساده نیست. گاهی پاسخ از نظر زبانی خوب است، اما از نظر محصولی، حقوقی، امنیتی یا اخلاقی قابل قبول نیست. اینجا روشن می‌شود که سیستم مبتنی بر هوش مصنوعی فقط مدل نیست؛ یک سیستم نرم‌افزاری کامل است.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>SE4AI یعنی استفاده از اصول مهندسی نرم‌افزار برای ساخت سیستم‌های مبتنی بر هوش مصنوعی؛ سیستم‌هایی که فقط با داشتن مدل قوی قابل اعتماد نمی‌شوند.</p></div></div>
<p>مرز این بخش با بخش قبل و بعدی مهم است. AI4SE می‌پرسید: هوش مصنوعی چطور به کارهای مهندسی نرم‌افزار کمک می‌کند؟ SE4AI می‌پرسد: وقتی نرم‌افزارمان هوش مصنوعی دارد، چطور آن را مثل یک سیستم مهندسی‌شده، قابل تست، قابل کنترل و قابل اعتماد بسازیم؟ MLOps که بخش بعدی است، بیشتر روی چرخه‌ی عملیاتی مدل‌ها تمرکز می‌کند: داده، آموزش، نسخه‌بندی مدل، استقرار، پایش، رانش و بازآموزی.</p>
<p>در نرم‌افزار سنتی، بخش زیادی از رفتار سیستم از کدی می‌آمد که خودمان نوشته بودیم. اگر ورودی مشخصی می‌دادیم، انتظار خروجی مشخصی داشتیم. البته باگ همیشه وجود داشت، اما رفتار اصلی سیستم از منطق صریح کد می‌آمد. با ورود سیستم‌های یادگیری ماشین، بخشی از رفتار دیگر مستقیماً در کد نوشته نمی‌شد؛ از داده یاد گرفته می‌شد. با مدل‌های زبانی بزرگ این پیچیدگی شکل تازه‌ای گرفت: علاوه بر کد، داده و مدل، حالا دستور مدل، زمینه، بازیابی دانش، حافظه، ابزارهای متصل، لایه‌های حفاظتی و ارزیابی هم بخشی از رفتار سیستم‌اند.</p>
<p><img decoding="async" loading="lazy" alt="سیستم هوش مصنوعی فقط مدل نیست" src="https://example.com/assets/images/se4ai-ai-system-is-more-than-model-fc08295a6ff4df7d53b73be5dc1090e7.png" width="1672" height="941" class="img_m9Pm"></p>
<p><em>مدل فقط یکی از اجزای سیستم هوش مصنوعی است. داده، دستور مدل، بازیابی دانش، ارزیابی، حفاظت‌ها، مشاهده‌پذیری، مسیر جایگزین و بازبینی انسانی هم بخشی از سیستم‌اند.</em></p>
<p>پس پرسش‌های مهندسی عوض می‌شوند. چه چیزی نسخه‌بندی می‌شود؟ فقط کد یا دستور مدل و مجموعه‌داده‌ی ارزیابی هم؟ چه چیزی تست می‌شود؟ فقط API یا کیفیت خروجی مدل هم؟ چه چیزی قابل برگشت است؟ نسخه‌ی مدل، دستور مدل، شاخص بازیابی دانش یا سیاست محصول؟ چه کسی مالک رفتار خروجی است؟ اگر مدل مطمئن نیست، سیستم باید چه کند؟ اگر پاسخ خطرناک، نادرست یا خارج از سیاست محصول بود، چه لایه‌ای جلوی آن را می‌گیرد؟</p>
<p>یکی از سخت‌ترین بخش‌های SE4AI، تست و ارزیابی است. در سیستم‌های کلاسیک، اغلب می‌توانیم بگوییم برای این ورودی، خروجی دقیقاً باید این مقدار باشد. اما در سیستم‌های هوش مصنوعی، مخصوصاً مدل‌های زبانی، همیشه یک خروجی واحد و قطعی نداریم. چند پاسخ ممکن است قابل قبول باشند. یک پاسخ ممکن است از نظر جمله‌بندی عالی باشد، اما از نظر دامنه غلط باشد. ممکن است مدل در بیشتر نمونه‌ها خوب عمل کند، اما در چند سناریوی حساس خطای پرهزینه بدهد.</p>
<p>به همین دلیل، تست در سیستم‌های هوش مصنوعی فقط test case کلاسیک نیست. به ارزیابی نیاز داریم: نمونه‌های واقعی و نماینده، معیارهای کیفیت، ارزیابی انسانی در بخش‌های حساس، تست بازگشتی برای دستور مدل و نسخه‌ی مدل، سناریوهای خطرناک، و بررسی توهم مدل، سوگیری، حریم خصوصی و امنیت. در سیستم هوش مصنوعی، گاهی سؤال اصلی این نیست که «آیا خروجی دقیقاً برابر مقدار مورد انتظار است؟»؛ سؤال این است که «آیا خروجی برای این زمینه قابل قبول، امن، مفید و مطابق سیاست محصول است؟»</p>
<p><img decoding="async" loading="lazy" alt="جریان ارزیابی و حفاظت در سیستم‌های هوش مصنوعی" src="https://example.com/assets/images/se4ai-evaluation-and-guardrails-flow-f1d4fc8c965fe82677aa929cb5e13404.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>خروجی هوش مصنوعی نباید خام وارد محصول شود؛ باید اعتبارسنجی، ارزیابی، محدودسازی، پایش و در موقعیت‌های حساس، بازبینی انسانی داشته باشد.</em></p>
<p>این موضوع ما را به مسئولیت محصول می‌رساند. در نرم‌افزار کلاسیک، اگر تابعی خطا بدهد، معمولاً دنبال باگ در کد می‌گردیم. در سیستم هوش مصنوعی ممکن است کد درست اجرا شده باشد، API مدل پاسخ داده باشد، هیچ خطای اجرایی رخ نداده باشد، اما خروجی محصول غلط باشد. مثلاً چت‌بات پشتیبانی سیاست بازگشت وجه را اشتباه توضیح دهد. ابزار خلاصه‌سازی نکته‌ی مهم تیکت را حذف کند. سیستم رتبه‌بندی گروهی از کاربران را ناعادلانه پایین‌تر بیاورد. یا یک ابزار تولید محتوا ناخواسته داده‌ی حساس را وارد پاسخ کند.</p>
<p>از نگاه کاربر، این‌ها خطای «مدل» نیستند؛ خطای محصول‌اند. وقتی هوش مصنوعی وارد محصول می‌شود، خروجی مدل هم بخشی از تجربه‌ی کاربر و مسئولیت تیم است. نمی‌توانیم بگوییم «مدل این‌طور گفت» و کنار بکشیم. باید از قبل طراحی کنیم: کجا مدل اجازه‌ی پاسخ آزاد دارد؟ کجا باید فقط از منابع معتبر جواب بدهد؟ کجا باید بگوید نمی‌دانم؟ کجا خروجی فقط پیشنهاد است و اقدام خودکار نیست؟ کجا انسان باید وارد حلقه شود؟ و کجا ردپای حسابرسی لازم داریم؟</p>
<p>در سیستم‌های مبتنی بر مدل زبانی، چند جزء تازه وارد معماری می‌شوند. الگوی دستور مدل فقط یک متن ساده نیست؛ یک دارایی مهندسی است که باید نسخه داشته باشد، قابل بازبینی باشد و با تغییرش ارزیابی اجرا شود. بازیابی دانش فقط جست‌وجوی چند سند نیست؛ روی کیفیت پاسخ و ریسک توهم مدل اثر می‌گذارد. اتصال مدل به ابزارها فقط قابلیت جذاب محصولی نیست؛ باید محدودیت دسترسی، حسابرسی، خطایابی و مسیر جایگزین داشته باشد. حافظه‌ی مکالمه هم فقط امکانات محصولی نیست؛ می‌تواند مسئله‌ی حریم خصوصی، امنیت و کیفیت ایجاد کند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>دستور مدل هم بخشی از سیستم است</div><div class="admonitionContent_hiHs"><p>در سیستم‌های مبتنی بر مدل زبانی، دستور مدل، سازنده‌ی زمینه، بازیاب دانش، ابزارهای متصل، حافظه، لایه‌های حفاظتی و مجموعه‌داده‌ی ارزیابی باید مثل اجزای واقعی سیستم دیده شوند؛ نه مثل متن‌های موقتی که هرکس دستی تغییرشان بدهد.</p></div></div>
<p>حالا نقد اصلی بخش: بزرگ‌ترین خطای تیم‌ها این است که سیستم هوش مصنوعی را به مدل تقلیل می‌دهند. فکر می‌کنند اگر مدل قوی‌تر شود، مسئله حل است. اما در عمل، بسیاری از شکست‌ها از خود مدل شروع نمی‌شوند؛ از سیستم اطراف مدل می‌آیند: داده‌ی بد، تعریف مبهم مسئله، نداشتن ارزیابی، نبود مسیر جایگزین، سیاست محصولی نامشخص، داده‌ی حساس در دستور مدل، اعتماد بیش از حد به خروجی، نبود مشاهده‌پذیری، یا نبود مالکیت برای رفتار مدل.</p>
<p><img decoding="async" loading="lazy" alt="ضدالگوی تمرکز افراطی بر مدل" src="https://example.com/assets/images/se4ai-model-centric-failure-anti-pattern-6417f1187eb408f9ebe289cbf8689f32.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>مدل بهتر، سیستم بد را نجات نمی‌دهد؛ اگر داده، ارزیابی، حفاظت، پایش، سیاست محصول و مسیر جایگزین ضعیف باشند، خروجی فقط قانع‌کننده‌تر و خطرناک‌تر می‌شود.</em></p>
<p>مدل بهتر، معماری بد را نجات نمی‌دهد؛ فقط گاهی خرابی را قانع‌کننده‌تر پنهان می‌کند. اگر نمی‌دانیم خروجی هوش مصنوعی را چطور ارزیابی کنیم، هنوز آماده‌ی سپردن تصمیم مهم به آن نیستیم. اگر نمی‌دانیم در چه شرایطی باید بگوید «نمی‌دانم»، هنوز طراحی محصول کامل نشده است. اگر نمی‌دانیم وقتی مدل اشتباه کرد چه کسی پاسخ‌گوست، هنوز مالکیت روشن نداریم.</p>
<p>این نگاه یعنی SE4AI فقط بحث مدل و دقت نیست؛ بحث سیستم و مسئولیت است. باید بدانیم کدام داده وارد مدل می‌شود، کدام خروجی اجازه‌ی نمایش به کاربر دارد، کدام تصمیم نیاز به تأیید انسانی دارد، کدام رفتار باید ثبت و حسابرسی شود، و کدام تغییر باید با ارزیابی مقایسه شود. باید بتوانیم نسخه‌ی مدل، دستور مدل، بازیابی دانش و سیاست محصول را کنار هم ردیابی کنیم تا اگر رفتار محصول تغییر کرد، بفهمیم چرا.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>مدل قوی کافی نیست</div><div class="admonitionContent_hiHs"><p>اگر داده بد است، ارزیابی نداریم، مسیر جایگزین نداریم، لایه‌های حفاظتی ضعیف‌اند، دستور مدل بی‌نسخه است و مالکیت رفتار مدل روشن نیست، مدل قوی‌تر فقط بخشی از مسئله را پنهان می‌کند. اعتماد به سیستم هوش مصنوعی از مهندسی کل سیستم می‌آید، نه فقط از نام مدل.</p></div></div>
<p>برای ساخت سیستم هوش مصنوعی قابل اعتماد، باید چند چیز را از ابتدا جدی بگیریم. مسئله و معیار خوب بودن خروجی باید روشن باشد. داده‌ی ورودی و داده‌ی ارزیابی باید نماینده و قابل پیگیری باشند. دستور مدل و تنظیمات باید نسخه‌بندی شوند. خروجی باید پایش شود. برای خطا، عدم اطمینان، محتوای خطرناک یا درخواست خارج از محدوده باید مسیر جایگزین داشته باشیم. در کارهای حساس، انسان باید بخشی از طراحی باشد، نه وصله‌ای که بعد از حادثه اضافه می‌شود.</p>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی SE4AI جدی‌تر می‌شود؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی خروجی هوش مصنوعی روی کاربر واقعی، تصمیم محصولی، داده‌ی حساس، پول، اعتبار، رتبه‌بندی، توصیه، پشتیبانی یا اقدام خودکار اثر می‌گذارد، دیگر با یک دمو طرف نیستیم. در این شرایط باید ارزیابی، لایه‌های حفاظتی، مشاهده‌پذیری، مسیر جایگزین، امنیت، حریم خصوصی و مالکیت رفتار مدل جدی گرفته شود.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی هنوز در مرحله‌ی آزمایش هستیم؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر فقط یک نمونه‌ی اولیه داریم، خروجی هوش مصنوعی تصمیم مهمی نمی‌گیرد، داده‌ی حساس درگیر نیست، و انسان همه‌ی خروجی‌ها را پیش از استفاده بررسی می‌کند، می‌توانیم سبک‌تر حرکت کنیم. اما همین مرحله هم باید از ابتدا به ما یاد بدهد که معیار کیفیت، ریسک‌ها و مرزهای استفاده چه هستند.</p></div></div></details>
<p>برای من، SE4AI یعنی پذیرفتن اینکه سیستم مبتنی بر هوش مصنوعی فقط یک مدل نیست. مدل مهم است، اما بدون داده‌ی خوب، دستور مدل قابل کنترل، ارزیابی، لایه‌ی حفاظتی، مشاهده‌پذیری، مسیر جایگزین، امنیت، تجربه‌ی کاربر و مسئولیت انسانی، محصول قابل اعتمادی ساخته نمی‌شود. هوش مصنوعی وقتی وارد محصول می‌شود، باید مثل بخشی از سیستم مهندسی شود؛ نه مثل جادویی که بیرون از قواعد مهندسی قرار دارد.</p>
<p>تا اینجا گفتیم سیستم هوش مصنوعی فقط مدل نیست و باید مهندسی شود. اما وقتی پای مدل، داده و تغییر مداوم رفتار وسط باشد، عملیات هم شکل تازه‌ای پیدا می‌کند. نمی‌توان مدل را یک‌بار مستقر کرد و فراموش کرد. داده تغییر می‌کند، توزیع ورودی عوض می‌شود، کیفیت افت می‌کند، و مدل باید پایش و گاهی بازآموزی شود. این ما را به بخش بعدی می‌رساند: MLOps.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-مدل-هم-مثل-کد-چرخهی-تولید-و-نگهداری-میخواهد">وقتی مدل هم مثل کد، چرخه‌ی تولید و نگه‌داری می‌خواهد<a href="https://example.com/blog/software-engineering-growth-story#%D9%88%D9%82%D8%AA%DB%8C-%D9%85%D8%AF%D9%84-%D9%87%D9%85-%D9%85%D8%AB%D9%84-%DA%A9%D8%AF-%DA%86%D8%B1%D8%AE%D9%87%DB%8C-%D8%AA%D9%88%D9%84%DB%8C%D8%AF-%D9%88-%D9%86%DA%AF%D9%87%D8%AF%D8%A7%D8%B1%DB%8C-%D9%85%DB%8C%D8%AE%D9%88%D8%A7%D9%87%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی مدل هم مثل کد، چرخه‌ی تولید و نگه‌داری می‌خواهد" title="لینک مستقیم به وقتی مدل هم مثل کد، چرخه‌ی تولید و نگه‌داری می‌خواهد" translate="no">​</a></h2>
<p>در بخش قبل گفتیم سیستم مبتنی بر هوش مصنوعی فقط یک مدل نیست. داده، دستور مدل، ارزیابی، حفاظت‌ها، مشاهده‌پذیری، مسیر جایگزین و مسئولیت انسانی هم بخشی از سیستم‌اند. حالا یک قدم عملیاتی‌تر برمی‌داریم: اگر مدل وارد محصول شد، چطور آن را در تولید زنده، قابل پایش، قابل بازگشت و قابل بهبود نگه داریم؟</p>
<p>یک صحنه‌ی آشنا را تصور کنیم. تیم داده یا تیم محصول یک مدل خوب ساخته است. در نوت‌بوک آزمایشی همه‌چیز امیدوارکننده است. داده‌ی آموزشی آماده شده، چند نمودار خوب داریم، معیارها مناسب‌اند و مدل روی داده‌ی تست عملکرد قابل قبولی نشان می‌دهد. همه خوشحال‌اند و جمله‌ی معروف شنیده می‌شود: «خب، حالا فقط مستقر کنیم.»</p>
<p>اما درست از همین‌جا درد واقعی شروع می‌شود. مدل در محیط تولید با داده‌هایی روبه‌رو می‌شود که همیشه شبیه داده‌ی آزمایشگاهی نیستند. رفتار کاربران تغییر می‌کند. فصل، کمپین، قیمت، سیاست محصول، بازار یا حتی شکل استفاده‌ی کاربران عوض می‌شود. ویژگی‌هایی که موقع آموزش تمیز و کامل بودند، در تولید دیر، ناقص یا با تعریف متفاوت می‌رسند. مدلی که امروز خوب کار می‌کند، ممکن است دو ماه بعد آرام‌آرام افت کند؛ بدون اینکه سرویس از کار بیفتد یا خطای واضحی بدهد.</p>
<p>اینجاست که MLOps وارد داستان می‌شود.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>MLOps یعنی ساختن چرخه‌ای قابل اعتماد برای بردن مدل‌های یادگیری ماشین از آزمایش به تولید، و نگه‌داری آن‌ها در دنیایی که داده و رفتار سیستم مدام تغییر می‌کند.</p></div></div>
<p>مرز MLOps با بخش قبل مهم است. SE4AI می‌پرسید سیستم مبتنی بر هوش مصنوعی را چطور درست طراحی کنیم: کیفیت خروجی، ارزیابی، حفاظت، تجربه‌ی کاربر، مسئولیت و معماری محصول. MLOps بیشتر روی چرخه‌ی عملیاتی مدل و داده تمرکز دارد: داده چطور آماده می‌شود، مدل چطور آموزش می‌بیند، نسخه‌ی مدل و داده چطور ثبت می‌شود، مدل چطور مستقر می‌شود، کیفیتش در تولید چطور پایش می‌شود، و اگر افت کرد یا داده تغییر کرد، چه می‌کنیم.</p>
<p>MLOps از نظر ذهنی به DevOps نزدیک است. در DevOps یاد گرفتیم کد فقط نوشته نمی‌شود؛ باید ساخته، تست، مستقر، پایش و در صورت نیاز برگردانده شود. اما در سیستم‌های یادگیری ماشین، فقط کد نداریم. داده و مدل هم دارایی‌های اصلی‌اند. در نرم‌افزار کلاسیک، اگر کد تغییر نکند، انتظار داریم رفتار سیستم نسبتاً پایدار بماند. اما در یادگیری ماشین، حتی اگر کد و مدل تغییر نکنند، دنیای بیرون تغییر می‌کند و کیفیت مدل می‌تواند افت کند.</p>
<p>در MLOps، دارایی مهندسی فقط کد نیست؛ داده، ویژگی، مدل، معیار ارزیابی و خط لوله‌ی آموزش هم دارایی مهندسی‌اند.</p>
<p><img decoding="async" loading="lazy" alt="چرخه‌ی MLOps از داده تا پایش و بازآموزی" src="https://example.com/assets/images/mlops-lifecycle-from-data-to-monitoring-366a2984beec9d781ad5c150130e529a.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>مدل فقط آموزش داده نمی‌شود؛ وارد چرخه‌ای از داده، آماده‌سازی، آموزش، ارزیابی، ثبت، استقرار، پایش و بازخورد می‌شود.</em></p>
<p>در مرحله‌ی آزمایش، نوت‌بوک ابزار بسیار خوبی است. می‌توانیم سریع ایده‌ها را امتحان کنیم، نمودار بکشیم، مدل‌ها را مقایسه کنیم و بفهمیم اصلاً مسئله ارزش ادامه دادن دارد یا نه. اما نوت‌بوک به‌تنهایی محصول نیست. اگر نمی‌دانیم داده دقیقاً از کجا آمده، چطور پاک‌سازی شده، چه ویژگی‌هایی ساخته شده، کدام نسخه از کد استفاده شده، چه پارامترهایی تنظیم شده و مدل با چه معیارهایی ارزیابی شده، بازتولید همان نتیجه بعداً سخت می‌شود.</p>
<p>مدلی که فقط در نوت‌بوک خوب کار می‌کند، هنوز محصول نیست؛ فقط یک شواهد آزمایشگاهی است. برای اینکه مدل وارد سیستم واقعی شود، باید آموزش آن تکرارپذیر باشد، داده‌اش قابل اعتبارسنجی باشد، مدلش نسخه داشته باشد، استقرارش کنترل‌شده باشد و کیفیتش بعد از استقرار دیده شود.</p>
<p>در یادگیری ماشین، داده فقط ورودی نیست؛ بخشی از رفتار سیستم است. اگر داده‌ی آموزشی نماینده‌ی دنیای واقعی نباشد، مدل در تولید بد عمل می‌کند. اگر schema داده تغییر کند، مدل ممکن است بی‌سروصدا خراب شود. اگر تعریف یک ویژگی عوض شود، مدل همان ورودی ظاهری را می‌گیرد، اما معنای آن عوض شده است. اگر داده دیر برسد یا ناقص باشد، خروجی مدل قابل اعتماد نیست.</p>
<p>مثلاً مدلی برای تشخیص تقلب داریم. اگر روش‌های تقلب تغییر کنند، رفتار کاربران در یک فصل خاص عوض شود، یا یک ویژگی مهم با تأخیر برسد، داده‌ی تولید با داده‌ی زمان آموزش فاصله می‌گیرد. مدل همچنان خروجی می‌دهد، اما کیفیتش ممکن است پایین آمده باشد. این نوع خرابی با خطای اجرایی معلوم نمی‌شود؛ باید با پایش داده و کیفیت مدل دیده شود.</p>
<p><img decoding="async" loading="lazy" alt="پایش رانش داده و مدل در MLOps" src="https://example.com/assets/images/mlops-data-and-model-drift-monitoring-da802bef9293ec794fd4d39e8f40f0c8.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>در یادگیری ماشین، خرابی همیشه از کار افتادن سرویس نیست؛ گاهی افت آرام کیفیت مدل است، چون داده‌ی واقعی از داده‌ی زمان آموزش فاصله گرفته است.</em></p>
<p>اینجا مفهوم رانش مهم می‌شود. رانش داده یعنی توزیع داده‌ی ورودی در تولید با داده‌ی زمان آموزش فرق کند. رانش مدل یا افت کیفیت یعنی خروجی و عملکرد مدل در دنیای واقعی دیگر مثل گذشته قابل اعتماد نباشد. گاهی این افت روی کل کاربران دیده نمی‌شود، بلکه روی یک گروه یا بخش خاص پنهان می‌ماند. برای همین فقط دیدن یک عدد کلی مثل دقت کافی نیست. باید بدانیم مدل روی گروه‌ها، سناریوها و بخش‌های مهم محصول چطور رفتار می‌کند.</p>
<p>MLOps باید به اعتبارسنجی داده، کیفیت داده، سازگاری ویژگی‌ها و پایش توجه کند. باید بفهمیم آیا داده‌ای که مدل می‌بیند همان معنایی را دارد که زمان آموزش داشته است یا نه. باید تشخیص دهیم آیا توزیع پیش‌بینی‌ها تغییر کرده، تأخیر بالا رفته، خطای ویژگی‌ها زیاد شده، یا کیفیت مدل روی گروه خاصی افت کرده است.</p>
<p>بخش مهم دیگر، نسخه‌بندی و ثبت مدل است. وقتی مدل جدیدی مستقر می‌شود، باید بتوانیم بگوییم با چه داده‌ای آموزش دیده، چه کدی آن را ساخته، چه پارامترهایی داشته، چه معیارهایی گرفته، چه کسی آن را تأیید کرده و روی چه بخش‌هایی خوب یا بد عمل کرده است. اگر مدل جدید بدتر شد، نسخه‌ی قبلی باید قابل پیدا کردن و برگشت باشد.</p>
<p>اینجاست که مفاهیمی مثل رهگیری آزمایش‌ها، مخزن ثبت مدل و مدیریت خروجی‌های مدل مهم می‌شوند. ابزارهایی مثل MLflow، Weights &amp; Biases، Kubeflow یا مخزن ویژگی‌هایی مثل Feast هرکدام بخشی از این مسئله را حل می‌کنند. اما ابزار به‌تنهایی کافی نیست. اگر تیم نداند معیار تصمیم‌گیری چیست، چه چیزی باید ثبت شود و چه زمانی یک مدل اجازه‌ی ورود به تولید دارد، مخزن ثبت مدل فقط تبدیل می‌شود به انبار فایل‌های مدل.</p>
<p>در نرم‌افزار معمولی، CI/CD معمولاً یعنی کد را بساز، تست کن و بعد مستقر کن. در یادگیری ماشین این چرخه پیچیده‌تر است. باید کد آموزش تست شود، داده اعتبارسنجی شود، مدل آموزش ببیند، مدل ارزیابی شود، اگر از حد قابل قبول بهتر بود ثبت شود، بعد به شکل کنترل‌شده مستقر شود و پس از آن رفتار مدل در تولید پایش شود. گاهی حتی از بازآموزی پیوسته حرف می‌زنیم؛ یعنی بازآموزی مداوم یا دوره‌ای مدل. اما اینجا هم باید مراقب باشیم: خودکار کردن بازآموزی بدون کنترل کیفیت داده، یعنی سرعت دادن به تولید مدل‌های بد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>بازآموزی خودکار همیشه بلوغ نیست</div><div class="admonitionContent_hiHs"><p>اگر داده‌ی جدید آلوده، ناقص یا نماینده‌ی رفتار درست نباشد، بازآموزی خودکار فقط خطا را سریع‌تر وارد مدل بعدی می‌کند. MLOps خوب یعنی کنترل کیفیت چرخه، نه فقط اتوماسیون بیشتر.</p></div></div>
<p>نقد اصلی بخش این است: خیلی از تیم‌ها MLOps را با ابزار اشتباه می‌گیرند. فکر می‌کنند اگر MLflow، Kubeflow، خط لوله‌ی جذاب یا داشبورد داشته باشند، MLOps حل شده است. اما MLOps قبل از ابزار، یک نظم مهندسی است: تعریف مالکیت، معیار کیفیت، داده‌ی قابل اعتماد، بازتولیدپذیری، ارزیابی، پایش، برگشت و فرایند تصمیم‌گیری.</p>
<p>MLOps بدون تعریف روشن از کیفیت مدل، فقط اتوماسیون تولید خروجی است. اگر نمی‌دانیم مدل در تولید خوب کار می‌کند یا نه، داشتن خط لوله فقط ما را سریع‌تر به ابهام می‌رساند. مدل خوب در آزمایش الزاماً مدل خوب در محصول نیست. دقت کلی ممکن است افت کیفیت روی بخش‌های مهم را پنهان کند. تشخیص رانش بدون برنامه‌ی اقدام، فقط یک هشدار تزئینی است. پایش هم فقط تأخیر و در دسترس بودن سرویس نیست؛ کیفیت پیش‌بینی هم بخشی از سلامت سیستم است.</p>
<p><img decoding="async" loading="lazy" alt="ضدالگوی استقرار و فراموشی در MLOps" src="https://example.com/assets/images/mlops-anti-pattern-deploy-and-forget-0589f2a1cc79b1f50c6254dd06548faf.png" width="1448" height="1086" class="img_m9Pm"></p>
<p><em>استقرار مدل پایان کار نیست؛ شروع نگه‌داری است. اگر مدل را مستقر کنیم و فراموش کنیم، تغییر داده و افت کیفیت دیر یا زود خودش را نشان می‌دهد.</em></p>
<p>ضدالگوی رایج این است که مدل را مستقر کنیم و بعد فراموشش کنیم. در روز استقرار همه‌چیز خوب به نظر می‌رسد. اما هفته‌ها و ماه‌ها بعد، داده تغییر می‌کند، کیفیت مدل افت می‌کند، کاربران رفتار متفاوتی نشان می‌دهند، هشدار مشخصی وجود ندارد و کسی دقیق نمی‌داند مدل فعلی با کدام داده و کدام کد ساخته شده است. اینجا مشکل فقط فنی نیست؛ اعتماد محصولی و تصمیم‌گیری سازمانی هم آسیب می‌بیند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>مدل در تولید زنده است</div><div class="admonitionContent_hiHs"><p>مدل در تولید مثل یک فایل ثابت نیست. کیفیت آن به داده‌ی ورودی، رفتار کاربران، تغییرات محصول و شرایط بیرونی وابسته است. پس نگه‌داری مدل باید بخشی از طراحی سیستم باشد، نه کاری که بعد از اولین حادثه به آن فکر کنیم.</p></div></div>
<p>MLOps برای آزمایش ساده‌ی دانشگاهی یا یک نمونه‌ی اولیه‌ی کوچک شاید لازم نباشد. اگر هدف فقط یادگیری، ارائه‌ی اولیه یا اثبات امکان‌پذیری است، یک نوت‌بوک و چند معیار ساده می‌تواند کافی باشد. اما وقتی خروجی مدل روی کاربران واقعی، پول، ریسک، رتبه‌بندی، پیشنهاد، تشخیص تقلب، پشتیبانی یا تصمیم محصولی اثر می‌گذارد، دیگر با یک آزمایش طرف نیستیم. مدل بخشی از سیستم تولیدی است و باید مثل بخشی از سیستم تولیدی با آن رفتار کنیم.</p>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی MLOps جدی می‌شود؟</summary><div><div class="collapsibleContent_T_pc"><p>وقتی مدل وارد تولید می‌شود، چند تیم درگیر می‌شوند، داده مرتب تغییر می‌کند، تصمیم مدل روی کاربر یا کسب‌وکار اثر دارد، و لازم است آموزش، ارزیابی، استقرار و پایش قابل تکرار و قابل ردیابی باشند، MLOps از یک انتخاب خوب به یک نیاز جدی تبدیل می‌شود.</p></div></div></details>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>چه زمانی هنوز ساده‌تر حرکت کنیم؟</summary><div><div class="collapsibleContent_T_pc"><p>اگر هنوز در مرحله‌ی اکتشاف هستیم، مدل فقط برای یادگیری یا نمونه‌ی اولیه استفاده می‌شود، خروجی روی کاربر واقعی اثر ندارد، و بازتولید دقیق هنوز مسئله‌ی اصلی نیست، می‌توان سبک‌تر شروع کرد. اما حتی در همین مرحله هم بهتر است از ابتدا عادت کنیم داده، کد، معیار و نتیجه‌ی آزمایش‌ها را شفاف نگه داریم.</p></div></div></details>
<p>برای من، MLOps یعنی مدل را از یک خروجی آزمایشگاهی به بخشی قابل اعتماد از سیستم تولید تبدیل کنیم. این کار فقط با مستقر کردن مدل تمام نمی‌شود؛ به داده‌ی قابل اعتماد، آموزش تکرارپذیر، نسخه‌بندی، ارزیابی، استقرار کنترل‌شده، پایش کیفیت، تشخیص رانش، برگشت و تصمیم‌گیری انسانی نیاز دارد. مدل در تولید زنده است، چون دنیای اطرافش تغییر می‌کند.</p>
<p>از یک برنامه‌ی ساده شروع کردیم و رسیدیم به سیستمی که داده، مدل، زیرساخت، عملیات، انسان، فرایند و هوش مصنوعی دارد. حالا دیگر مهندسی نرم‌افزار فقط نوشتن کد نیست؛ ساختن سیستمی است که در زمان، تغییر، خطا و رشد دوام بیاورد. بخش پایانی همین مسیر را جمع‌بندی می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی-وقتی-نرمافزار-بزرگ-میشود-مسئله-فقط-کد-نیست">جمع‌بندی: وقتی نرم‌افزار بزرگ می‌شود، مسئله فقط کد نیست<a href="https://example.com/blog/software-engineering-growth-story#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C-%D9%88%D9%82%D8%AA%DB%8C-%D9%86%D8%B1%D9%85%D8%A7%D9%81%D8%B2%D8%A7%D8%B1-%D8%A8%D8%B2%D8%B1%DA%AF-%D9%85%DB%8C%D8%B4%D9%88%D8%AF-%D9%85%D8%B3%D8%A6%D9%84%D9%87-%D9%81%D9%82%D8%B7-%DA%A9%D8%AF-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی: وقتی نرم‌افزار بزرگ می‌شود، مسئله فقط کد نیست" title="لینک مستقیم به جمع‌بندی: وقتی نرم‌افزار بزرگ می‌شود، مسئله فقط کد نیست" translate="no">​</a></h2>
<p>ما این مسیر را از یک جای ساده شروع کردیم: یک برنامه، چند endpoint، چند کاربر و چند نیاز روشن. در آن نقطه، خیلی از مفاهیمی که در این نوشته دیدیم شاید زیادی بزرگ، رسمی یا حتی نمایشی به نظر برسند. کسی که هنوز یک محصول کوچک دارد، شاید واقعاً نیازی به Kubernetes، GitOps، CQRS، Event Sourcing، BPMS، Chaos Engineering یا MLOps نداشته باشد. این نکته را باید جدی گرفت؛ معماری خوب یعنی انتخاب به‌اندازه، نه جمع کردن اسم‌های جذاب.</p>
<p>اما نرم‌افزار اگر زنده بماند، تغییر می‌کند. کاربرها بیشتر می‌شوند، تیم بزرگ‌تر می‌شود، نیازها از هم فاصله می‌گیرند، چند مصرف‌کننده‌ی مختلف پیدا می‌کنیم، سرویس‌ها به هم وابسته می‌شوند، داده زیاد می‌شود، خطاها پرهزینه‌تر می‌شوند، زیرساخت پیچیده‌تر می‌شود و کم‌کم پرسش‌های تازه‌ای پیش می‌آید: قرارداد API کجاست؟ هر مصرف‌کننده چه نیازی دارد؟ مرز دامنه کجاست؟ پیام‌ها چطور جابه‌جا می‌شوند؟ داده‌ی قدیمی را چطور مهاجرت می‌دهیم؟ اگر بخشی از سیستم خراب شد چه می‌شود؟ فرایندهای سازمانی کجا قابل پیگیری‌اند؟ و وقتی هوش مصنوعی وارد محصول شد، کیفیت و مسئولیت خروجی را چطور می‌سنجیم؟</p>
<p>این نوشته قرار نبود از همه‌ی این مفاهیم قهرمان بسازد. اتفاقاً در بیشتر بخش‌ها یک هشدار تکرار شد: هر ابزار و الگو اگر بی‌جا وارد شود، می‌تواند مسئله‌ی تازه بسازد. BFF اگر فقط کپی‌کاری منطق شود، بدهی می‌سازد. Event Sourcing اگر برای مسئله‌ی نامناسب انتخاب شود، خواندن و نگه‌داری را سخت می‌کند. Serverless اگر بدون فهم مدل اجرا و هزینه وارد شود، غافلگیر می‌کند. Low-code و BPMS اگر بدون مالکیت و مرز روشن رشد کنند، منطق را از جلوی چشم تیم پنهان می‌کنند. هوش مصنوعی اگر بدون زمینه، ارزیابی و مسئولیت انسانی استفاده شود، فقط خروجی قانع‌کننده‌تر تولید می‌کند؛ نه لزوماً نرم‌افزار بهتر.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>پیام اصلی</div><div class="admonitionContent_hiHs"><p>این مفاهیم مدال افتخار نیستند؛ ابزارهایی‌اند برای پاسخ دادن به دردهای واقعی. وقتی درد واقعی وجود ندارد، استفاده از آن‌ها بیشتر نمایش معماری است تا مهندسی.</p></div></div>
<p>از طرف دیگر، سادگی هم همیشه فضیلت نیست. سادگی وقتی ارزشمند است که مسئولانه باشد. اینکه همه‌چیز را در یک سرویس، یک جدول، یک جریان کاری، یک اسکریپت یا یک نوت‌بوک نگه داریم، شاید در شروع سریع باشد؛ اما اگر سیستم رشد کرده و هنوز هیچ مرزی، هیچ پایشی، هیچ تستی، هیچ قرارداد روشنی و هیچ مالکیتی نداریم، دیگر اسمش سادگی نیست. آنجا با سادگی بی‌مسئولیت طرفیم؛ چیزی که امروز راحت است و فردا هزینه‌اش را با کندی، خطا و ترس از تغییر پس می‌دهد.</p>
<p>پس خط میانی این است: <strong>نه معماری نمایشی، نه سادگی بی‌مسئولیت.</strong></p>
<p>معماری نمایشی یعنی ابزارها را زودتر از درد واقعی وارد کنیم. سادگی بی‌مسئولیت یعنی درد واقعی را ببینیم و همچنان وانمود کنیم با چند فایل و چند دستور دستی همه‌چیز قابل کنترل است. مهندسی نرم‌افزار بالغ، بین این دو افراط حرکت می‌کند: مسئله را می‌فهمد، هزینه‌ی راه‌حل را می‌سنجد، و ابزار را وقتی وارد می‌کند که واقعاً به روشن‌تر شدن، قابل نگه‌داری‌تر شدن یا قابل اعتمادتر شدن سیستم کمک کند.</p>
<p>در این مسیر، تقریباً همه‌ی مفاهیم یک پیام مشترک داشتند: سیستم‌های نرم‌افزاری فقط از کد تشکیل نشده‌اند. قراردادها، داده‌ها، رخدادها، صف‌ها، زیرساخت، استقرار، مشاهده‌پذیری، مهاجرت داده، فرایندهای انسانی، ابزارهای داخلی، مدل‌های هوش مصنوعی و حتی تصمیم‌های سازمانی، همه بخشی از واقعیت نرم‌افزارند. اگر فقط کد را ببینیم، دیر یا زود آن بخش‌های پنهان از جایی دیگر خودشان را نشان می‌دهند.</p>
<p>همین‌جا ارزش نگاه مرحله‌ای روشن می‌شود. لازم نیست روز اول همه‌چیز را کامل و سنگین طراحی کنیم. بهتر است از ساده‌ترین راه مسئولانه شروع کنیم، اما حواسمان به نشانه‌های رشد باشد. وقتی چند مصرف‌کننده نیازهای متفاوت دارند، شاید BFF معنا پیدا کند. وقتی ارتباط سرویس‌ها زیاد می‌شود، شاید صف پیام یا معماری رخدادمحور لازم شود. وقتی زیرساخت با کلیک و حافظه‌ی افراد جلو می‌رود، IaC و GitOps جدی‌تر می‌شوند. وقتی داده‌ی زنده تغییر می‌کند، مهاجرت مرحله‌ای لازم می‌شود. وقتی مدل وارد تولید می‌شود، MLOps دیگر تزئین نیست.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یک معیار ساده</div><div class="admonitionContent_hiHs"><p>قبل از وارد کردن هر الگو یا ابزار، بپرسیم: این انتخاب کدام درد واقعی را کم می‌کند؟ چه هزینه‌ای به سیستم اضافه می‌کند؟ چه کسی مالک آن است؟ اگر شکست خورد، چطور می‌فهمیم و چطور برمی‌گردیم؟</p></div></div>
<p>اگر بخواهم کل این نوشته را در یک تصویر ذهنی خلاصه کنم، نرم‌افزار شبیه موجودی است که در زمان رشد می‌کند. اوایل فقط باید راه برود. بعد باید با دیگران حرف بزند. بعد باید بار بیشتری حمل کند. بعد باید بیمار نشود، یا اگر شد، زود بفهمیم. بعد باید در محیط‌های مختلف زندگی کند. بعد باید با داده، فرایند، انسان و هوش مصنوعی کنار بیاید. ما هم به‌عنوان مهندس، فقط سازنده‌ی چند تابع نیستیم؛ نگه‌دارنده‌ی این موجود در مسیر رشد هستیم.</p>
<p>این یعنی مهندسی نرم‌افزار، در معنای جدی‌اش، هنر انتخاب‌های مسئولانه در زمان است. گاهی انتخاب مسئولانه این است که ابزار تازه‌ای وارد نکنیم. گاهی انتخاب مسئولانه این است که بالاخره بپذیریم راه دستی و ساده‌ی قبلی دیگر جواب نمی‌دهد. گاهی باید پیچیدگی را بپذیریم، چون مسئله واقعاً پیچیده شده است. گاهی باید جلوی پیچیدگی را بگیریم، چون فقط از هیجان ابزار آمده، نه از نیاز سیستم.</p>
<p>در پایان، اگر خواننده بعد از این نوشته فقط نام این بیست مفهوم را حفظ کند، اتفاق مهمی نیفتاده است. هدف بهتر این است که وقتی در یک جلسه، طراحی، بازبینی، مهاجرت داده، انتخاب ابزار یا بحث معماری با یکی از این واژه‌ها روبه‌رو می‌شود، بتواند یک قدم عقب‌تر بایستد و بپرسد:</p>
<p>این مفهوم دقیقاً قرار است کدام مشکل ما را حل کند؟<br>
<!-- -->آیا مشکل ما واقعاً به این اندازه رسیده است؟<br>
<!-- -->چه هزینه‌ای پنهان می‌کند؟<br>
<!-- -->چه چیزی را قابل مشاهده‌تر، قابل تغییرتر یا قابل اعتمادتر می‌کند؟<br>
<!-- -->و اگر اشتباه انتخابش کنیم، چه چیزی سخت‌تر می‌شود؟</p>
<p>اگر این پرسش‌ها همراه ما بمانند، این مسیر ارزشش را داشته است. چون مهندسی نرم‌افزار خوب با حفظ کردن اسم الگوها ساخته نمی‌شود؛ با دیدن مسئله، فهمیدن زمینه، سنجیدن هزینه‌ها و انتخاب‌های مرحله‌ای ساخته می‌شود.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="Software Engineering" term="Software Engineering"/>
        <category label="Architecture" term="Architecture"/>
        <category label="Software Design" term="Software Design"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[اصل تک‌مسئولیتی؛ وقتی خودِ قاب‌بندی مسئله‌ساز است]]></title>
        <id>https://example.com/blog/solid-single-responsibility-critique</id>
        <link href="https://example.com/blog/solid-single-responsibility-critique"/>
        <updated>2026-05-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[نقدی روایی و فنی به اصل تک‌مسئولیتی؛ از زاویه‌ی هزینه‌ی تغییر، فهم، تست و نگهداری در طول زمان.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="کد تمیزی که داستانش گم شده" src="https://example.com/assets/images/srp-clean-code-lost-story-72e623ae66bd4d7e8c629b2a3d0af03a.png" width="1672" height="941" class="img_m9Pm"></p>
<p>این نوشته درباره‌ی این نیست که کسی اصل تک‌مسئولیتی را «بد اجرا کرده» و اگر کمی دقیق‌تر اجرا می‌کرد، همه‌چیز درست می‌شد. نقد من کمی ریشه‌ای‌تر است: خودِ قاب‌بندی این اصل گاهی ما را با سؤال اشتباهی وارد طراحی می‌کند.</p>
<p>به‌جای اینکه از خودمان بپرسیم «این مرزبندی در طول زمان چه هزینه‌ای دارد؟»، خیلی زود می‌پرسیم: «این کلاس چند مسئولیت دارد؟» همین تغییر کوچک در سؤال، ما را آرام‌آرام به سمت شکستن کد بر اساس مسئولیت‌های ظاهری می‌برد: <code>Validator</code>، <code>Policy</code>، <code>Factory</code>، <code>Writer</code>، <code>Service</code>.</p>
<p>گاهی نتیجه، کدی است که از دور مرتب‌تر و حرفه‌ای‌تر به نظر می‌رسد؛ اما تصمیم اصلی دامنه‌ای در آن مالک روشن ندارد. حرف اصلی این متن همین است: <strong>جداسازی باید هزینه‌ی آینده را کم کند؛ اگر فقط ظاهر امروز را تمیزتر کند، هنوز باید از خودش دفاع کند.</strong></p>
<p>بعضی اصل‌ها آن‌قدر خوش‌بیان و دلنشین‌اند که مخالفت با آن‌ها شبیه مخالفت با عقل سلیم به نظر می‌رسد. اصل تک‌مسئولیتی هم از همین جنس است. جمله‌ی «هر چیز فقط یک دلیل برای تغییر داشته باشد» کوتاه است، مرتب است، و در نگاه اول انگار عصاره‌ی طراحی خوب را در خودش دارد.</p>
<p>اما گاهی همین جمله‌های خیلی تمیز، دقیقاً همان‌جایی خطرناک می‌شوند که دیگر کسی درباره‌ی هزینه‌ی واقعی‌شان سؤال نمی‌پرسد.</p>
<p>مسئله این نیست که جداسازی همیشه بد است. مسئله این است که ما قبل از دیدن الگوی واقعی تغییر در کدبیس، وارد بازی دیگری می‌شویم: بازیِ شمردن مسئولیت‌ها، شکستن کلاس‌ها، نام‌گذاری نقش‌های فنی، و ساختن انتزاع‌هایی که شاید هیچ‌وقت هزینه‌شان را پس ندهند.</p>
<div class="theme-admonition theme-admonition-danger admonition_xGDO alert alert--danger"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"></path></svg></span>تز متن</div><div class="admonitionContent_hiHs"><p>مشکل فقط اجرای بد SRP نیست؛ مشکل این است که SRP طراحی را با سؤال اشتباهی شروع می‌کند.</p></div></div>
<p>سؤال رایج در این قاب‌بندی معمولاً این است:</p>
<blockquote>
<p>این کد چند مسئولیت دارد؟</p>
</blockquote>
<p>اما سؤال مهندسی‌تر این است:</p>
<blockquote>
<p>این مرزبندی در طول زمان چه چیزی را ارزان‌تر و چه چیزی را گران‌تر می‌کند؟</p>
</blockquote>
<p>این دو سؤال شبیه هم نیستند. اولی ما را وارد بحث‌های تمام‌نشدنی درباره‌ی تعریف «مسئولیت» می‌کند. دومی مجبورمان می‌کند هزینه‌ی تصمیم را در طول عمر کد بسنجیم: فهم، تغییر، تست، ریویو، دیباگ، ورود آدم‌های تازه به تیم، و مقیاس‌پذیری این قاعده در کل کدبیس.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="سؤال-غلط-چند-مسئولیت-دارد">سؤال غلط: «چند مسئولیت دارد؟»<a href="https://example.com/blog/solid-single-responsibility-critique#%D8%B3%D8%A4%D8%A7%D9%84-%D8%BA%D9%84%D8%B7-%DA%86%D9%86%D8%AF-%D9%85%D8%B3%D8%A6%D9%88%D9%84%DB%8C%D8%AA-%D8%AF%D8%A7%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به سؤال غلط: «چند مسئولیت دارد؟»" title="لینک مستقیم به سؤال غلط: «چند مسئولیت دارد؟»" translate="no">​</a></h2>
<p>اصل تک‌مسئولیتی می‌گوید هر ماژول باید فقط یک دلیل برای تغییر داشته باشد. روی کاغذ، این حرف بدی نیست. اتفاقاً اگر آن را دقیق و بالغ بفهمیم، دارد درباره‌ی تغییر حرف می‌زند، نه درباره‌ی کوتاه‌بودن فایل یا زیادبودن فعل‌ها.</p>
<p>اما پروژه‌ی واقعی به این تمیزی رفتار نمی‌کند. دلیل تغییر معمولاً یک چیز ساده، ثابت و تک‌محوری نیست. یک تغییر از محصول می‌آید، یکی از مقررات، یکی از مدل داده، یکی از کارایی، یکی از تست، یکی از مشاهده‌پذیری، و یکی از مالکیت تیمی. گاهی هم چندتا از این‌ها با هم وارد می‌شوند و همه‌ی مرزهای قبلی را به چالش می‌کشند.</p>
<p>حالا در چنین محیطی، وقتی از خودمان می‌پرسیم «این کلاس چند مسئولیت دارد؟»، خیلی زود وارد یک بازی تفسیری می‌شویم:</p>
<ul>
<li class="">یک نفر می‌گوید validation یک مسئولیت است.</li>
<li class="">یکی می‌گوید policy یک مسئولیت است.</li>
<li class="">یکی می‌گوید ساخت object یک مسئولیت است.</li>
<li class="">یکی می‌گوید persistence یک مسئولیت است.</li>
</ul>
<p>و نکته‌ی دردناک این است که همه می‌توانند با ادبیات SRP از خودشان دفاع کنند.</p>
<p>یکی می‌گوید این شرط‌ها را ببریم در <code>Validator</code>، چون اعتبارسنجی مسئولیت جداست. یکی می‌گوید تصمیم دامنه باید در <code>Policy</code> باشد. یکی می‌گوید ساخت آبجکت باید برود در <code>Factory</code>. یکی هم می‌گوید سرویس باید لاغر بماند و فقط orchestration کند.</p>
<p>هیچ‌کدام از این جمله‌ها ذاتاً غلط نیستند. خیلی وقت‌ها حتی نجات‌دهنده‌اند. مشکل این است که SRP به‌تنهایی به ما نمی‌گوید کدام‌یک از این جداسازی‌ها در طول زمان هزینه‌ی تغییر را کم می‌کند. فقط یک قاب جذاب می‌دهد: «مسئولیت‌ها را جدا کن.»</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>قاعده‌ی جایگزین</div><div class="admonitionContent_hiHs"><p>به‌جای اینکه اول بپرسیم «این چند مسئولیت دارد؟»، بهتر است این پرسش‌ها را جلویمان بگذاریم:</p><ul>
<li class="">این تصمیم کجا زندگی می‌کند؟</li>
<li class="">چه چیزهایی با هم تغییر می‌کنند؟</li>
<li class="">کدام مرز فهم را ساده‌تر می‌کند؟</li>
<li class="">این جداسازی در مقیاس تیمی چه هزینه‌ای دارد؟</li>
</ul></div></div>
<p>وقتی اصل، معیار عملیاتی روشن نمی‌دهد، هر کس برداشت خودش از «مسئولیت» را تبدیل به معماری می‌کند. همین‌جاست که کد از نظر ظاهری تمیز می‌شود، اما رفتار واقعی سیستم آرام‌آرام بین چند نقش فنی پخش می‌شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-قانون-کوچک-و-یک-تصمیم-بیمالک">یک قانون کوچک و یک تصمیم بی‌مالک<a href="https://example.com/blog/solid-single-responsibility-critique#%DB%8C%DA%A9-%D9%82%D8%A7%D9%86%D9%88%D9%86-%DA%A9%D9%88%DA%86%DA%A9-%D9%88-%DB%8C%DA%A9-%D8%AA%D8%B5%D9%85%DB%8C%D9%85-%D8%A8%DB%8C%D9%85%D8%A7%D9%84%DA%A9" class="hash-link" aria-label="لینک مستقیم به یک قانون کوچک و یک تصمیم بی‌مالک" title="لینک مستقیم به یک قانون کوچک و یک تصمیم بی‌مالک" translate="no">​</a></h2>
<p>فرض کن قانون محصولی کوچکی عوض شده است:</p>
<blockquote>
<p>انتقال‌های زیر ۵۰ هزار تومان دیگر نیاز به بررسی اضافه ندارند.</p>
</blockquote>
<p>از بیرون، تغییر ساده است. یک قانون کوچک عوض شده. نه قرارداد API عوض شده، نه مدل داده، نه جریان اصلی محصول.</p>
<p>اما وارد کد که می‌شوی، سؤال ساده‌ای که باید جواب روشن داشته باشد، پخش می‌شود:</p>
<blockquote>
<p>این تصمیم دقیقاً کجا زندگی می‌کند؟</p>
</blockquote>
<p>آیا این حد در <code>validator</code> است؟ در <code>policy</code>؟ در ساختن تراکنش؟ در تست سرویس؟ در چند جای هم‌زمان؟</p>
<p>اینجا نباید حواسمان پرت تعداد فایل‌ها شود. تعداد فایل به‌تنهایی استدلال قوی‌ای نیست. تغییر زیاد می‌تواند کاملاً طبیعی و حتی نشانه‌ی کار درست باشد. اگر schema عوض شده، migration لازم بوده، مستندات اصلاح شده، تست‌ها به‌روز شده‌اند، قرارداد API تغییر کرده، یا observability باید با رفتار جدید هماهنگ می‌شده، این‌ها fan-out ضروری‌اند.</p>
<p>مشکل جای دیگری است: <strong>تصمیم واحد دامنه‌ای مالک روشن ندارد.</strong></p>
<p>یعنی مسئله واقعاً چند مرز مستقل ندارد؛ یک تصمیم واحد قبلاً به اسم مسئولیت‌های جداگانه تکه‌تکه شده است. حالا برای فهم و اصلاح آن باید چند کلاس، چند تست و چند mock را کنار هم بگذاری تا تازه بفهمی تصمیم اصلی کجا پنهان شده است.</p>
<p>این همان جایی است که تمیزی ظاهری شروع می‌کند به گرفتن مالیات از تیم.</p>
<p>در یک کدبیس کوچک، شاید فقط کمی آزاردهنده باشد. در یک تیم بزرگ‌تر، این هزینه چند برابر می‌شود. یک نفر قانون محصولی را تغییر می‌دهد، نفر دوم تست را می‌خواند، نفر سوم ریویو می‌کند، نفر چهارم شش ماه بعد باگ را دیباگ می‌کند. اگر هر کدام برای فهم یک تصمیم ساده باید همان مسیر پراکنده را دوباره کشف کنند، هزینه‌ی طراحی فقط یک بار پرداخت نشده؛ بارها و بارها از جیب تیم کم شده است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="درد-اصلی-جداسازی-مسئولیتمحور-نه-تغییرمحور">درد اصلی: جداسازی مسئولیت‌محور، نه تغییرمحور<a href="https://example.com/blog/solid-single-responsibility-critique#%D8%AF%D8%B1%D8%AF-%D8%A7%D8%B5%D9%84%DB%8C-%D8%AC%D8%AF%D8%A7%D8%B3%D8%A7%D8%B2%DB%8C-%D9%85%D8%B3%D8%A6%D9%88%D9%84%DB%8C%D8%AA%D9%85%D8%AD%D9%88%D8%B1-%D9%86%D9%87-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1%D9%85%D8%AD%D9%88%D8%B1" class="hash-link" aria-label="لینک مستقیم به درد اصلی: جداسازی مسئولیت‌محور، نه تغییرمحور" title="لینک مستقیم به درد اصلی: جداسازی مسئولیت‌محور، نه تغییرمحور" translate="no">​</a></h2>
<p>مشکل من «جداسازی» نیست. مشکل من جداسازی‌ای است که از سؤال غلط شروع می‌شود.</p>
<p>وقتی به‌جای محور تغییر، از «مسئولیت ظاهری» شروع کنیم، خیلی زود کد را بر اساس نقش‌های فنی می‌بریم:</p>
<table><thead><tr><th>سؤال SRP-زده</th><th>سؤال مهندسی‌تر</th></tr></thead><tbody><tr><td>این کلاس چند مسئولیت دارد؟</td><td>این مرز در طول زمان چه هزینه‌ای می‌سازد؟</td></tr><tr><td>آیا validation را جدا کرده‌ایم؟</td><td>آیا تصمیم اصلی مالک روشن دارد؟</td></tr><tr><td>آیا policy جداست؟</td><td>آیا تغییر محصولی در یک نقطه‌ی قابل فهم جمع می‌شود؟</td></tr><tr><td>آیا سرویس لاغر است؟</td><td>آیا مسیر تصمیم برای خواننده روشن است؟</td></tr><tr><td>آیا abstraction داریم؟</td><td>آیا abstraction هزینه‌اش را پس داده است؟</td></tr></tbody></table>
<p>نظم فنی همیشه بد نیست. اتفاقاً در خیلی از تیم‌ها مزیت جدی است. وقتی تازه‌وارد می‌داند validation کجاست، policy کجاست و persistence کجاست، ورودش به کدبیس ساده‌تر می‌شود. ساختار قابل پیش‌بینی کمک می‌کند آدم‌ها سریع‌تر در کد راه بیفتند. مالکیت لایه‌ها هم می‌تواند روشن‌تر شود.</p>
<p>اما همین نظم از جایی به بعد می‌تواند ضد خودش شود.</p>
<p>اگر یک تصمیم واحد دامنه‌ای بین <code>Validator</code> و <code>Policy</code> و <code>Factory</code> پخش شود، ساختار قابل پیش‌بینی دیگر کمک نمی‌کند؛ فقط باعث می‌شود تصمیم در کشوهای مرتب گم شود. مشکل کشو داشتن نیست. کشو، فهرست و نمایه گاهی دقیقاً همان چیزی است که یک کتاب را قابل استفاده می‌کند. مشکل وقتی شروع می‌شود که برای خواندن یک پاراگراف مجبور شوی پنج کشو را هم‌زمان باز کنی.</p>
<p><img decoding="async" loading="lazy" alt="میز خلوت‌تر شد؛ داستان گم شد" src="https://example.com/assets/images/decision-lost-between-layers-07109ad5636f058e508053586554a187.png" width="1672" height="941" class="img_m9Pm"></p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چند-حالت-خوب-و-بد">چند حالت خوب و بد<a href="https://example.com/blog/solid-single-responsibility-critique#%DA%86%D9%86%D8%AF-%D8%AD%D8%A7%D9%84%D8%AA-%D8%AE%D9%88%D8%A8-%D9%88-%D8%A8%D8%AF" class="hash-link" aria-label="لینک مستقیم به چند حالت خوب و بد" title="لینک مستقیم به چند حالت خوب و بد" translate="no">​</a></h2>
<p>برای اینکه بحث منصفانه بماند، باید بپذیریم خیلی از چیزهایی که نقد می‌کنم، در جای درست خودشان ابزارهای مفیدی‌اند. مشکل ابزار نیست؛ معیار تصمیم است. مشکل وقتی شروع می‌شود که ابزار مفید را بدون سنجیدن هزینه‌ی بلندمدت، به قاعده‌ی عمومی تبدیل کنیم.</p>
<div class="theme-tabs-container tabs-container tabList_D8NW"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_OkUL tabs__item--active">Validator</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_OkUL">Policy</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_OkUL">Factory</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_OkUL">Interface</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_fvDV"><p><code>Validator</code> خوب است وقتی واقعاً مرز مستقلی دارد. مثلاً اعتبارسنجی زیر مالکیت تیم compliance یا security است، با تغییر مقررات عوض می‌شود، و چند مسیر از یک قرارداد ورودی مشترک استفاده می‌کنند. در این حالت، جداکردن validation هزینه‌ی تغییر را کم می‌کند؛ چون تغییر مقررات در همان مرز جمع می‌شود.</p><p>اما <code>Validator</code> بد می‌شود وقتی فقط چند شرط از یک تصمیم واحد دامنه‌ای را جدا کرده‌ایم. اگر هر بار قانون محصولی عوض می‌شود هم <code>validator</code> تغییر می‌کند، هم <code>policy</code>، یعنی شاید این دو از نظر اسم جدا باشند، اما از نظر تغییر واقعی هنوز به هم چسبیده‌اند.</p><p>بده‌بستان اینجاست:</p><table><thead><tr><th>جنبه</th><th>توضیح</th></tr></thead><tbody><tr><td>مزیت</td><td>مرز روشن برای اعتبارسنجی</td></tr><tr><td>هزینه</td><td>احتمال پخش‌شدن تصمیم محصولی بین validation و policy</td></tr><tr><td>سؤال مهندسی</td><td>در تغییرهای واقعی، این دو مستقل حرکت می‌کنند یا همیشه با هم؟</td></tr></tbody></table><p>SRP می‌تواند جداشدن validator را تشویق کند، اما به‌تنهایی نمی‌پرسد آیا این جداسازی تغییر آینده را محلی‌تر کرده یا نه.</p></div><div role="tabpanel" class="tabItem_fvDV" hidden=""><p><code>Policy</code> خوب است وقتی واقعاً مرکز تصمیم‌های دامنه است. یعنی تغییرهای محصولی آنجا جمع می‌شوند، قرارداد رفتاری روشنی دارد، و تست‌های رفتاری نشان می‌دهند این policy چه تصمیمی می‌گیرد.</p><p>اما <code>Policy</code> بد می‌شود وقتی فقط اسم شیک‌تری برای چند شرط پراکنده است. بخشی از تصمیم در <code>validator</code> مانده، بخشی در <code>factory</code> دفن شده، بخشی هم در mockهای تست فرض شده. در این حالت policy مالک تصمیم نیست؛ فقط یک ایستگاه در زنجیره‌ی پاس‌دادن است.</p><p>بده‌بستان:</p><table><thead><tr><th>جنبه</th><th>توضیح</th></tr></thead><tbody><tr><td>مزیت</td><td>تمرکز تصمیم دامنه</td></tr><tr><td>هزینه</td><td>اگر تصمیم از policy نشت کند، policy فقط یک نام زیبا روی پراکندگی است</td></tr><tr><td>سؤال مهندسی</td><td>آیا تغییر محصولی واقعاً در policy جمع می‌شود؟</td></tr></tbody></table><p>SRP به ما می‌گوید تصمیم را جدا کن، اما نمی‌گوید تصمیم واقعاً کجا باید زندگی کند و چه چیزی نباید از آن بیرون نشت کند.</p></div><div role="tabpanel" class="tabItem_fvDV" hidden=""><p><code>Factory</code> خوب است وقتی ساخت آبجکت واقعاً پیچیده است؛ مثلاً invariantهای ساخت باید یک‌جا حفظ شوند، چند مسیر ساخت داریم، یا خود ساختن آبجکت قرارداد مهمی دارد.</p><p>اما <code>Factory</code> بد می‌شود وقتی فقط constructor ساده را پنهان کرده‌ایم. بدتر از آن، وقتی بخشی از تصمیم دامنه‌ای در status mapping یا type mapping داخل factory دفن شده باشد. آن‌وقت برای فهم قانون محصولی باید factory را هم بخوانیم، نه چون ساخت آبجکت مهم است، بلکه چون تصمیم از جای خودش نشت کرده.</p><p>بده‌بستان:</p><table><thead><tr><th>جنبه</th><th>توضیح</th></tr></thead><tbody><tr><td>مزیت</td><td>تمرکز ساخت پیچیده و invariantها</td></tr><tr><td>هزینه</td><td>پنهان‌شدن تصمیم دامنه‌ای در مرحله‌ی ساخت</td></tr><tr><td>سؤال مهندسی</td><td>factory قرارداد ساخت را ساده کرده یا مسیر فهم تصمیم را طولانی‌تر؟</td></tr></tbody></table><p>SRP می‌تواند «ساختن» را مسئولیتی جدا ببیند، اما گاهی همین جداکردن باعث می‌شود بخشی از تصمیم دامنه‌ای در مرحله‌ی ساخت دفن شود.</p></div><div role="tabpanel" class="tabItem_fvDV" hidden=""><p><code>Interface</code> خوب است وقتی مرز خارجی داریم، قرارداد پایدار می‌خواهیم، چند پیاده‌سازی واقعی یا نزدیک داریم، یا می‌خواهیم وابستگی به جزئیات فنی را کم کنیم. در این حالت interface می‌تواند مرز سیستم را روشن‌تر و تغییر آینده را ارزان‌تر کند.</p><p>اما interface بد می‌شود وقتی فقط چون «شاید بعداً پیاده‌سازی دوم آمد» ساخته شده. هیچ مرز واقعی، قرارداد مفهومی یا تغییرپذیری واقعی ایجاد نکرده، اما از همین امروز به همه‌ی خواننده‌ها و تست‌ها مالیات abstraction تحمیل کرده است.</p><p>بده‌بستان:</p><table><thead><tr><th>جنبه</th><th>توضیح</th></tr></thead><tbody><tr><td>مزیت</td><td>مرز پایدار و امکان جایگزینی واقعی</td></tr><tr><td>هزینه</td><td>لایه‌ی اضافه برای خواننده، تست و ریویو</td></tr><tr><td>سؤال مهندسی</td><td>آیا این interface امروز ارزشی تولید کرده یا فقط آینده‌ی خیالی را بیمه کرده؟</td></tr></tbody></table><p>مشکل این است که SRP، وقتی کنار ادبیات رایج clean code و dependency inversion می‌نشیند، خیلی راحت آینده‌ی خیالی را تبدیل به abstraction امروز می‌کند.</p></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>آینده‌ی خیالی، هزینه‌ی واقعی</div><div class="admonitionContent_hiHs"><p>«شاید بعداً لازم شود» به‌تنهایی دلیل کافی برای ساختن مرز جدید نیست. مرز جدید از همان روز اول هزینه دارد: خواندن، تست، ریویو، navigation و تصمیم‌گیری را گران‌تر می‌کند. آینده باید آن‌قدر محتمل و نزدیک باشد که این هزینه را پس بدهد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="شاهد-کدی-انتقال-پول">شاهد کدی: انتقال پول<a href="https://example.com/blog/solid-single-responsibility-critique#%D8%B4%D8%A7%D9%87%D8%AF-%DA%A9%D8%AF%DB%8C-%D8%A7%D9%86%D8%AA%D9%82%D8%A7%D9%84-%D9%BE%D9%88%D9%84" class="hash-link" aria-label="لینک مستقیم به شاهد کدی: انتقال پول" title="لینک مستقیم به شاهد کدی: انتقال پول" translate="no">​</a></h2>
<p>حالا برگردیم به همان قانون انتقال زیر ۵۰ هزار تومان.</p>
<p>نسخه‌ی مستقیم‌تر شاید چیزی شبیه این باشد:</p>
<div class="language-go codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">transfer_service.go</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-go codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">func</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">s </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">TransferService</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Transfer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cmd TransferCommand</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token builtin">error</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Amount </span><span class="token operator" style="color:#393A34">&lt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> ErrInvalidAmount</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    account</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">accounts</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SourceAccountID</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Amount </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">50_000</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">NeedsExtraCheck</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> ErrExtraCheckRequired</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">CanWithdraw</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Amount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> ErrInsufficientBalance</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transfer </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">NewTransfer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cmd</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ledger</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Record</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transfer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">accounts</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ApplyTransfer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> transfer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد ایده‌آل نیست. ممکن است <code>ledger</code> واقعاً مرز مستقل حسابداری باشد. ممکن است <code>accounts</code> ماژول جدا داشته باشد. ممکن است تصمیم‌های انتقال واقعاً باید در policy باشند. من از تابع‌های چاق و کلاس‌های خداگونه دفاع نمی‌کنم.</p>
<p>اما این نسخه یک مزیت دارد: خواننده می‌تواند مسیر تصمیم را ببیند. قانون ۵۰ هزار تومان همان‌جا وسط جریان دیده می‌شود.</p>
<p>نسخه‌ی SRPزده ممکن است از دور تمیزتر باشد:</p>
<div class="language-go codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">transfer_service.go</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-go codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">func</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">s </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">TransferService</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Transfer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cmd TransferCommand</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token builtin">error</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">validator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Validate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    decision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">policy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Decide</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    tx </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">factory</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Build</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cmd</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> decision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">writer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Write</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> tx</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> err</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">updater</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Apply</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> tx</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این نسخه هم لزوماً بد نیست. <code>TransferService</code> می‌تواند orchestrator سالمی باشد؛ یعنی dependencyهای معنادار را کنار هم بیاورد و مسیر تصمیم را روشن کند. اما اگر فقط pass-through chain باشد، یعنی کار را از یک collaborator به بعدی پاس بدهد و تصمیم اصلی را پنهان کند، مشکل داریم.</p>
<p>نسخه‌ی بد ماجرا وقتی است که قانون اصلی در چند تکه پخش شده باشد:</p>
<div class="language-go codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">transfer_validator.go</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-go codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">func</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">v </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">TransferValidator</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Validate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cmd TransferCommand</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token builtin">error</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Amount </span><span class="token operator" style="color:#393A34">&lt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> ErrInvalidAmount</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Amount </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> SmallTransferLimit </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">RequiresExtraCheckFlag </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> ErrInvalidSmallTransfer</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<div class="language-go codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">transfer_policy.go</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-go codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">func</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">p </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">TransferPolicy</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Decide</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">account Account</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cmd TransferCommand</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> Decision </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Amount </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> SmallTransferLimit </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">NeedsExtraCheck</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> Decision</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">RequiresExtraCheck</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> Decision</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">RequiresExtraCheck</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<div class="language-go codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">transaction_factory.go</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-go codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">func</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">f </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">TransactionFactory</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Build</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cmd TransferCommand</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> decision Decision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> Transfer </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> Transfer</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Amount</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> cmd</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Status</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">statusFromDecision</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">decision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا اگر قانون ۵۰ هزار تومان تغییر کند، باید بفهمیم این حد در <code>validator</code> چه اثری دارد، در <code>policy</code> چه تصمیمی می‌سازد، در <code>factory</code> چه وضعیتی تولید می‌کند، و کدام تست‌ها با mock کردن policy فقط بخشی از رفتار را فرض گرفته‌اند.</p>
<p>مشکل این نیست که policy داریم. مشکل این است که تصمیم اصلی در یک جای روشن زندگی نمی‌کند.</p>
<p>تست هم همین‌جا می‌تواند هزینه را زیاد کند. interaction test همیشه بد نیست. mock کردن <code>policy</code> می‌تواند تست سرویس را سریع‌تر و متمرکزتر کند؛ قرار نیست هر تست سرویس کل مسیر را end-to-end اجرا کند. مشکل وقتی شروع می‌شود که mock تنها شاهد ما از رفتار باشد، قرارداد واقعی <code>policy</code> جداگانه تست نشده باشد، یا تست سرویس به جای سنجیدن اثر قابل مشاهده‌ی انتقال، فقط ثابت کند که <code>policy</code> با چه ورودی‌ای صدا زده شده است.</p>
<p><img decoding="async" loading="lazy" alt="شرط ساده‌ای که بین کلاس‌های تمیز گم شد" src="https://example.com/assets/images/decision-lost-between-layers-07109ad5636f058e508053586554a187.png" width="1672" height="941" class="img_m9Pm"></p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مثال-کوچکتر-تابعی-که-فقط-برای-آینده-abstract-شده">مثال کوچک‌تر: تابعی که فقط برای آینده abstract شده<a href="https://example.com/blog/solid-single-responsibility-critique#%D9%85%D8%AB%D8%A7%D9%84-%DA%A9%D9%88%DA%86%DA%A9%D8%AA%D8%B1-%D8%AA%D8%A7%D8%A8%D8%B9%DB%8C-%DA%A9%D9%87-%D9%81%D9%82%D8%B7-%D8%A8%D8%B1%D8%A7%DB%8C-%D8%A2%DB%8C%D9%86%D8%AF%D9%87-abstract-%D8%B4%D8%AF%D9%87" class="hash-link" aria-label="لینک مستقیم به مثال کوچک‌تر: تابعی که فقط برای آینده abstract شده" title="لینک مستقیم به مثال کوچک‌تر: تابعی که فقط برای آینده abstract شده" translate="no">​</a></h2>
<p>این بیماری فقط در کلاس‌های بزرگ و الگوهای معماری نیست. گاهی در یک تابع کوچک هم خودش را نشان می‌دهد.</p>
<p>مثلاً:</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">payment_methods.py</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">get_enabled_payment_methods</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token builtin">list</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">PaymentMethod</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token builtin">list</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        PaymentMethod</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">objects</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">is_enabled</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">order_by</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"display_order"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>در نگاه اول، بی‌آزار است. حتی ممکن است کسی بگوید: «خب خوب است دیگر؛ اگر فردا خواستیم cache اضافه کنیم، همین‌جا اضافه می‌کنیم. اگر فردا filter جدید خواستیم، همین تابع را تغییر می‌دهیم.»</p>
<p>اما سؤال مهندسی این است:</p>
<blockquote>
<p>آیا امروز واقعاً چنین محوری برای تغییر داریم، یا فقط یک query ساده را پشت یک اسم پنهان کرده‌ایم؟</p>
</blockquote>
<p>این تابع می‌تواند خوب باشد اگر واقعاً مفهوم دامنه‌ای مشترکی داریم: «روش‌های پرداخت فعال». اگر چند caller همین قرارداد را می‌خواهند. اگر ترتیب <code>display_order</code> بخشی از قرارداد محصول است. اگر cache واقعاً نیاز شده یا نزدیک است.</p>
<p>اما اگر فقط یک‌بار استفاده شده، اگر cache هنوز فقط یک احتمال ذهنی است، اگر callerهای آینده معلوم نیستند، و اگر این تابع فقط query ساده‌ای را پنهان می‌کند، شاید inline بودن صادقانه‌تر باشد:</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">payment_methods </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">list</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    PaymentMethod</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">objects</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">is_enabled</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">order_by</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"display_order"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>کد تکراری واقعی، از abstraction خیالی قابل تحمل‌تر است. چون تکرار را می‌بینی، اما abstraction غلط خودش را شبیه طراحی خوب جا می‌زند.</p>
<p><img decoding="async" loading="lazy" alt="آینده‌ی خیالی، هزینه‌ی واقعی" src="https://example.com/assets/images/speculative-abstraction-tax-d6ae0318501b59f1b130861786a51059.png" width="1672" height="941" class="img_m9Pm"></p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="از-کجا-بفهمیم-وارد-مسیر-غلط-شدهایم">از کجا بفهمیم وارد مسیر غلط شده‌ایم؟<a href="https://example.com/blog/solid-single-responsibility-critique#%D8%A7%D8%B2-%DA%A9%D8%AC%D8%A7-%D8%A8%D9%81%D9%87%D9%85%DB%8C%D9%85-%D9%88%D8%A7%D8%B1%D8%AF-%D9%85%D8%B3%DB%8C%D8%B1-%D8%BA%D9%84%D8%B7-%D8%B4%D8%AF%D9%87%D8%A7%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به از کجا بفهمیم وارد مسیر غلط شده‌ایم؟" title="لینک مستقیم به از کجا بفهمیم وارد مسیر غلط شده‌ایم؟" translate="no">​</a></h2>
<p>من دنبال عدد قطعی نیستم. تعداد فایل، تعداد کلاس، تعداد mock یا تعداد interface به‌تنهایی حکم نمی‌دهد. این‌ها فقط proxy هستند. اگر تبدیلشان کنیم به هدف، خودشان فساد می‌سازند.</p>
<p>هدف اصلی این است:</p>
<blockquote>
<p>کاهش هزینه‌ی فهم و تغییر در طول زمان</p>
</blockquote>
<p>برای نزدیک‌شدن به این هدف، چند سیگنال بهتر از «چند مسئولیت دارد؟» داریم:</p>
<table><thead><tr><th>Signal</th><th>سؤال عملی</th></tr></thead><tbody><tr><td>مالک تصمیم</td><td>آیا می‌توانیم یک نقطه‌ی روشن در کد نشان بدهیم و بگوییم تصمیم اینجاست؟</td></tr><tr><td>هم‌تغییری در گیت</td><td>آیا چند فایل ظاهراً جدا، در تغییرهای مشابه مدام با هم تغییر می‌کنند؟</td></tr><tr><td>تست رفتاری</td><td>آیا تست‌ها اثر قابل مشاهده را می‌سنجند یا فقط interactionها را قفل کرده‌اند؟</td></tr><tr><td>ریویوی متمرکز</td><td>آیا ریویور تصمیم اصلی را می‌بیند یا در سیم‌کشی DI و mock و constructor گم می‌شود؟</td></tr><tr><td>abstraction واقعی</td><td>آیا abstraction با نیاز واقعی توجیه شده یا با «شاید بعداً»؟</td></tr><tr><td>مقیاس تیمی</td><td>اگر این قاعده در کل تیم تکرار شود، هزینه‌ی نگهداری کم می‌شود یا زیاد؟</td></tr></tbody></table>
<p>این‌ها حقیقت مطلق نیستند، اما از سؤال «چند مسئولیت دارد؟» عملیاتی‌ترند. چون به جای بحث‌های تفسیری، ما را می‌برند سمت رفتار واقعی کد در طول زمان.</p>
<details class="details_KCVU alert alert--info details_Sxo0" data-collapsed="true"><summary>این سیگنال‌ها را چطور بخوانیم؟</summary><div><div class="collapsibleContent_T_pc"><p>این جدول قرار نیست به ماشین تصمیم‌گیری تبدیل شود. مثلاً هم‌تغییری در گیت همیشه بد نیست؛ ممکن است چند فایل واقعاً بخشی از یک قرارداد بزرگ‌تر باشند. mock هم همیشه بد نیست؛ گاهی برای جدا کردن مرزهای کند و ناپایدار لازم است. interface تک‌پیاده‌سازی‌شده هم همیشه اشتباه نیست؛ اگر قرارداد پایدار و مرز خارجی می‌سازد، می‌تواند کاملاً قابل دفاع باشد.</p><p>اما اگر چند سیگنال هم‌زمان ظاهر شوند، باید مکث کنیم. مثلاً تصمیم مالک روشن ندارد، تست‌ها فقط interactionها را قفل کرده‌اند، و هر تغییر محصولی همان چند فایل را با هم تکان می‌دهد. آنجا دیگر با یک «کد تمیزتر» طرف نیستیم؛ احتمالاً با هزینه‌ای طرفیم که فقط خوب بسته‌بندی شده است.</p></div></div></details>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>قبل از جداکردن، این‌ها را بپرس</div><div class="admonitionContent_hiHs"><ul>
<li class="">آیا این جداسازی تغییر را محلی‌تر می‌کند؟</li>
<li class="">آیا تصمیم مالک روشن‌تری پیدا می‌کند؟</li>
<li class="">آیا فهم مسیر اصلی ساده‌تر می‌شود؟</li>
<li class="">آیا تست‌ها رفتاری‌تر می‌شوند؟</li>
<li class="">آیا ریویو روی تصمیم متمرکزتر می‌شود؟</li>
<li class="">آیا این قاعده در مقیاس تیمی جواب می‌دهد؟</li>
</ul></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نه-god-class-نه-srpزدگی">نه God Class، نه SRPزدگی<a href="https://example.com/blog/solid-single-responsibility-critique#%D9%86%D9%87-god-class-%D9%86%D9%87-srp%D8%B2%D8%AF%DA%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به نه God Class، نه SRPزدگی" title="لینک مستقیم به نه God Class، نه SRPزدگی" translate="no">​</a></h2>
<p>ممکن است کسی بگوید: «پس همه‌چیز را بریزیم در یک کلاس؟»</p>
<p>نه. این هم همان‌قدر بد است.</p>
<p>کلاس خداگونه هم هزینه‌ی خودش را دارد: تغییر خطرناک می‌شود، تست سخت می‌شود، مسئولیت‌های واقعاً نامرتبط قاطی می‌شوند، و هر اصلاح کوچک ممکن است جای دیگری را خراب کند. من از تابع‌های هزارخطی، duplication بی‌حد، و کلاس‌هایی که همه‌چیز را قورت می‌دهند دفاع نمی‌کنم.</p>
<p>اما نقد من این است که SRP ما را زیادی زود به سمت شکستن می‌برد. قبل از اینکه هزینه‌ی تغییر را بسنجیم، شروع می‌کنیم به تعریف مسئولیت و جداکردن اجزا.</p>
<p>مرز سالم نه با شعار «همه‌چیز جدا»، نه با شعار «همه‌چیز کنار هم» پیدا می‌شود. مرز سالم از مشاهده‌ی تغییر واقعی، مالکیت تصمیم، فهم انسانی و مقیاس تیمی می‌آید.</p>
<p>جمله‌ی من این است:</p>
<blockquote>
<p>همه‌چیز را کنار هم نگه ندار؛ اما قبل از جداکردن، ثابت کن این جداسازی هزینه‌ی آینده را کم می‌کند.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="اصلهایی-که-هزینهشان-را-نشان-نمیدهند">اصل‌هایی که هزینه‌شان را نشان نمی‌دهند<a href="https://example.com/blog/solid-single-responsibility-critique#%D8%A7%D8%B5%D9%84%D9%87%D8%A7%DB%8C%DB%8C-%DA%A9%D9%87-%D9%87%D8%B2%DB%8C%D9%86%D9%87%D8%B4%D8%A7%D9%86-%D8%B1%D8%A7-%D9%86%D8%B4%D8%A7%D9%86-%D9%86%D9%85%DB%8C%D8%AF%D9%87%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به اصل‌هایی که هزینه‌شان را نشان نمی‌دهند" title="لینک مستقیم به اصل‌هایی که هزینه‌شان را نشان نمی‌دهند" translate="no">​</a></h2>
<p>مشکل من با SRP این است که هزینه‌ی خودش را نشان نمی‌دهد. جمله‌ی «هر چیز فقط یک دلیل برای تغییر داشته باشد» آن‌قدر تمیز است که آدم حس می‌کند اگر کد را شکست، حتماً کار درستی کرده است.</p>
<p>اما طراحی خوب با حس خوب فرق دارد.</p>
<p>طراحی خوب باید در طول زمان دوام بیاورد. باید وقتی تیم بزرگ‌تر شد، هنوز قابل فهم باشد. باید وقتی قانون محصولی عوض شد، محل تغییر قابل پیش‌بینی باشد. باید وقتی refactor می‌کنیم، تست‌ها از رفتار محافظت کنند، نه از سیم‌کشی داخلی. باید وقتی ریویو می‌کنیم، تصمیم اصلی دیده شود، نه فقط choreography کلاس‌ها.</p>
<p>پس دفعه‌ی بعد که خواستی چیزی را به اسم اصل تک‌مسئولیتی جدا کنی، فقط نپرس:</p>
<blockquote>
<p>آیا این مسئولیت جداست؟</p>
</blockquote>
<p>بپرس:</p>
<blockquote>
<p>این جداسازی چه چیزی را در آینده ارزان‌تر می‌کند؟</p>
</blockquote>
<p>اگر جواب روشنی نداری، شاید هنوز وقت شکستن نرسیده است.</p>
<div class="theme-admonition theme-admonition-danger admonition_xGDO alert alert--danger"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"></path></svg></span>ضربه‌ی آخر</div><div class="admonitionContent_hiHs"><p>اگر این جداسازی نه تغییر را محلی‌تر کرده، نه فهم را ساده‌تر، نه تست را رفتاری‌تر، و نه مرز واقعی‌تری ساخته، کد را تمیز نکرده‌ای؛ فقط کد کثیفی ساخته‌ای که خوب لباس پوشیده است.</p></div></div>]]></content>
        <category label="Software Engineering" term="Software Engineering"/>
        <category label="Clean Code" term="Clean Code"/>
        <category label="solid" term="solid"/>
        <category label="testing" term="testing"/>
        <category label="refactoring" term="refactoring"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[هرچه رابط کاربری کم‌ادعاتر باشد، آزمون‌پذیری بیشتر می‌شود]]></title>
        <id>https://example.com/blog/presenter-and-humble-object</id>
        <link href="https://example.com/blog/presenter-and-humble-object"/>
        <updated>2026-05-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[بررسی الگوی Humble Object و اینکه چگونه می‌توان منطق آزمون‌پذیر را از بخش‌های سخت‌آزمون جدا کرد.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="یک بخش ساده‌ی نمایش در کنار بخشی که داده‌های خام را مرتب، تصمیم‌ها را اعمال، و خروجی آماده‌ی نمایش تولید می‌کند." src="https://example.com/assets/images/presenter-and-humble-object-b30b89afce15fe7a1611b93727c0ce48.png" width="1672" height="941" class="img_m9Pm"></p>
<p>فرض کنید صفحه‌ای داریم که وضعیت حساب کاربر را نمایش می‌دهد. این صفحه باید تصمیم بگیرد آیا حساب فعال است یا نه، متن مناسب را نشان دهد، رنگ وضعیت را انتخاب کند، مبلغ را قالب‌بندی کند، پیام هشدار بسازد، و اگر کاربر محدودیت برداشت دارد، دکمه‌ی برداشت را غیرفعال کند.</p>
<p>در آغاز، شاید طبیعی به نظر برسد که همه‌ی این تصمیم‌ها را همان‌جا در خود رابط کاربری بنویسیم. بالاخره خروجی قرار است در همین صفحه دیده شود:</p>
<div class="language-tsx codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-tsx codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">AccountSummary</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">}</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">account</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Account</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isBlocked </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">status</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'blocked'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isLowBalance </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">balance</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> canWithdraw </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">status</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'active'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">balance</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> statusText </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> isBlocked </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'مسدود'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'فعال'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> statusColor </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> isBlocked </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'red'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'green'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> balanceText </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">account</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation">balance</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation function" style="color:#d73a49">toLocaleString</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation string" style="color:#e3116c">'fa-IR'</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> تومان</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> warningText </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> isLowBalance</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'موجودی حساب کم است'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">section</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">h2</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ownerName</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">h2</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">span</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">className</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">statusColor</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">statusText</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">span</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">balanceText</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">warningText </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">warningText</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">button</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">disabled</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript operator" style="color:#393A34">!</span><span class="token tag script language-javascript" style="color:#00009f">canWithdraw</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text">برداشت</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">button</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">section</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد در نگاه نخست کوچک است، اما چند تصمیم مهم را در جایی گذاشته که آزمون‌کردنش معمولاً سخت‌تر از آزمون یک تابع ساده است. برای اطمینان از درستی این منطق، باید کامپوننت را رندر کنیم، وضعیت‌های مختلف را بسازیم، متن‌ها و دکمه‌ها را از خروجی پیدا کنیم، و گاهی حتی با رفتار مرورگر یا کتابخانه‌ی رابط کاربری درگیر شویم.</p>
<p>مسئله این نیست که رابط کاربری بد است. مسئله این است که رابط کاربری جای خوبی برای پنهان‌کردن منطق تصمیم‌گیری نیست.</p>
<p>در فصل «Presenters and Humble Objects»، رابرت مارتین به همین مشکل اشاره می‌کند: بعضی بخش‌های سیستم ذاتاً سخت‌تر آزمون می‌شوند؛ پس بهتر است این بخش‌ها تا حد ممکن ساده و کم‌منطق بمانند، و تصمیم‌های اصلی به بخش‌هایی منتقل شوند که آزمون‌کردنشان آسان‌تر است.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>الگوی شیء فروتن (Humble Object) می‌گوید بخشی که سخت آزمون می‌شود باید تا حد ممکن کم‌منطق و ساده باشد.</p><p>در این نگاه، منطق تصمیم‌گیری، قالب‌بندی، آماده‌سازی داده و انتخاب وضعیت‌ها به بخشی منتقل می‌شود که بتوان آن را با آزمون‌های ساده و سریع بررسی کرد. بخش سخت‌آزمون، مثل رابط کاربری، چارچوب وب، پایگاه داده یا دستگاه بیرونی، فقط کارهای ضروری و کم‌ادعا را انجام می‌دهد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مشکل-ui-پرمنطق-چیست">مشکل UI پرمنطق چیست؟<a href="https://example.com/blog/presenter-and-humble-object#%D9%85%D8%B4%DA%A9%D9%84-ui-%D9%BE%D8%B1%D9%85%D9%86%D8%B7%D9%82-%DA%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به مشکل UI پرمنطق چیست؟" title="لینک مستقیم به مشکل UI پرمنطق چیست؟" translate="no">​</a></h2>
<p>رابط کاربری، مخصوصاً در برنامه‌های واقعی، معمولاً به چیزهای زیادی وابسته است: چارچوب، مرورگر، رخدادها، وضعیت محلی، کتابخانه‌ی طراحی، اندازه‌ی صفحه، زبان، دسترسی‌پذیری، و گاهی داده‌هایی که از چند مسیر می‌آیند. همین وابستگی‌ها باعث می‌شوند آزمون رابط کاربری معمولاً سنگین‌تر و شکننده‌تر از آزمون یک تابع معمولی باشد.</p>
<p>حالا اگر منطق تصمیم‌گیری را هم در همین لایه پنهان کنیم، دو مشکل را با هم ترکیب کرده‌ایم: هم منطق مهم داریم، هم محیط آزمون سخت.</p>
<p>در مثال حساب کاربر، این تصمیم‌ها درون کامپوننت پنهان شده‌اند:</p>
<ul>
<li class="">وضعیت حساب چه متنی داشته باشد؟</li>
<li class="">چه رنگی برای وضعیت مناسب است؟</li>
<li class="">دکمه‌ی برداشت چه زمانی فعال باشد؟</li>
<li class="">هشدار موجودی چه زمانی نمایش داده شود؟</li>
<li class="">مبلغ چطور قالب‌بندی شود؟</li>
</ul>
<p>این‌ها فقط جزئیات نمایشی نیستند. بعضی از آن‌ها تصمیم‌های محصولی یا کسب‌وکاری‌اند. اگر اشتباه شوند، کاربر پیام نادرست می‌بیند یا عملیاتی را می‌بیند که نباید انجام دهد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>رابط کاربری پرمنطق معمولاً به‌آرامی ساخته می‌شود. اول فقط یک شرط کوچک اضافه می‌کنیم. بعد یک قالب‌بندی ساده. بعد یک پیام خطا. بعد چند حالت ویژه.</p><p>پس از مدتی، صفحه‌ای داریم که هم نمایش می‌دهد، هم تصمیم می‌گیرد، هم داده را تفسیر می‌کند، هم وضعیت کسب‌وکار را تعیین می‌کند. چنین صفحه‌ای سخت‌تر آزمون می‌شود، سخت‌تر تغییر می‌کند، و خطاهایش دیرتر دیده می‌شوند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="presenter-چه-کمکی-میکند">Presenter چه کمکی می‌کند؟<a href="https://example.com/blog/presenter-and-humble-object#presenter-%DA%86%D9%87-%DA%A9%D9%85%DA%A9%DB%8C-%D9%85%DB%8C%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به Presenter چه کمکی می‌کند؟" title="لینک مستقیم به Presenter چه کمکی می‌کند؟" translate="no">​</a></h2>
<p>ایده‌ی ارائه‌دهنده (Presenter) این است که داده‌ی خام را بگیرد و آن را به شکلی آماده کند که رابط کاربری فقط آن را نمایش دهد. یعنی به‌جای اینکه کامپوننت خودش تصمیم بگیرد چه چیزی نشان دهد، یک بخش جدا این تصمیم‌ها را می‌گیرد و خروجی آماده‌ی نمایش می‌سازد.</p>
<p>تصویر زیر همین مرز را نشان می‌دهد: رابط کاربری در سمت نمایش می‌ماند و کارهای پرتصمیم، مثل قالب‌بندی داده، انتخاب وضعیت، آماده‌سازی متن‌ها و ساخت مدل نمایش، در Presenter انجام می‌شود. نتیجه این است که UI کم‌منطق‌تر و پایدارتر می‌ماند، و Presenter را می‌توان با آزمون‌های ساده‌تر بررسی کرد.</p>
<p><img decoding="async" loading="lazy" alt="جریان آماده‌سازی داده در Presenter و ساده ماندن رابط کاربری در الگوی Humble Object." src="https://example.com/assets/images/presenter-humble-object-flow-f22ae94a27f7315e6f84b7a4255c2970.png" width="1672" height="941" class="img_m9Pm"></p>
<p>برای نمونه، می‌توانیم منطق مثال قبلی را به یک تابع یا کلاس ساده منتقل کنیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">AccountSummaryViewModel</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ownerName</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  statusText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  statusTone</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'success'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'danger'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  balanceText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  warningText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  canWithdraw</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">presentAccountSummary</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">account</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Account</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> AccountSummaryViewModel </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isBlocked </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'blocked'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isLowBalance </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">balance </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ownerName</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ownerName</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    statusText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> isBlocked </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'مسدود'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'فعال'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    statusTone</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> isBlocked </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'danger'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'success'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    balanceText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">account</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation">balance</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation function" style="color:#d73a49">toLocaleString</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation string" style="color:#e3116c">'fa-IR'</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> تومان</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    warningText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> isLowBalance </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'موجودی حساب کم است'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    canWithdraw</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'active'</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">balance </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا کامپوننت ساده‌تر می‌شود:</p>
<div class="language-tsx codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-tsx codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">AccountSummary</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">}</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">account</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Account</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> viewModel </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">presentAccountSummary</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">section</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">h2</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">viewModel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ownerName</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">h2</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">span</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">data-tone</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">viewModel</span><span class="token tag script language-javascript punctuation" style="color:#393A34">.</span><span class="token tag script language-javascript property-access" style="color:#00009f">statusTone</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">viewModel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">statusText</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">span</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">viewModel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">balanceText</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">viewModel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">warningText</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">viewModel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">warningText</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">p</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">button</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">disabled</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript operator" style="color:#393A34">!</span><span class="token tag script language-javascript" style="color:#00009f">viewModel</span><span class="token tag script language-javascript punctuation" style="color:#393A34">.</span><span class="token tag script language-javascript property-access" style="color:#00009f">canWithdraw</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text">برداشت</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">button</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">section</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این نسخه، کامپوننت هنوز لازم است. اما فروتن‌تر شده است. یعنی کمتر تصمیم می‌گیرد و بیشتر نمایش می‌دهد. منطق اصلی به جایی رفته که می‌توانیم آن را بدون رندر کردن صفحه و بدون درگیرشدن با مرورگر آزمون کنیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آزمون-سادهتر-طراحی-بهتر-را-نشان-میدهد">آزمون ساده‌تر، طراحی بهتر را نشان می‌دهد<a href="https://example.com/blog/presenter-and-humble-object#%D8%A2%D8%B2%D9%85%D9%88%D9%86-%D8%B3%D8%A7%D8%AF%D9%87%D8%AA%D8%B1-%D8%B7%D8%B1%D8%A7%D8%AD%DB%8C-%D8%A8%D9%87%D8%AA%D8%B1-%D8%B1%D8%A7-%D9%86%D8%B4%D8%A7%D9%86-%D9%85%DB%8C%D8%AF%D9%87%D8%AF" class="hash-link" aria-label="لینک مستقیم به آزمون ساده‌تر، طراحی بهتر را نشان می‌دهد" title="لینک مستقیم به آزمون ساده‌تر، طراحی بهتر را نشان می‌دهد" translate="no">​</a></h2>
<p>وقتی منطق در Presenter قرار می‌گیرد، آزمون آن بسیار ساده‌تر می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'presents blocked account summary'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> viewModel </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">presentAccountSummary</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ownerName</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'علی محمدی'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    status</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'blocked'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    balance</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">50_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">viewModel</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toEqual</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ownerName</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'علی محمدی'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    statusText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'مسدود'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    statusTone</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'danger'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    balanceText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'۵۰٬۰۰۰ تومان'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    warningText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'موجودی حساب کم است'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    canWithdraw</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>این آزمون نه به رندر نیاز دارد، نه به مرورگر، نه به کتابخانه‌ی رابط کاربری. یک ورودی می‌دهیم و خروجی را بررسی می‌کنیم. اگر قانون نمایش وضعیت یا پیام هشدار تغییر کند، آزمون دقیقاً همان بخش را نشان می‌دهد.</p>
<p>این همان سود اصلی الگوی Humble Object است: بخش سخت‌آزمون را کم‌منطق می‌کنیم و منطق را به جایی می‌بریم که آزمونش ساده، سریع و دقیق باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="presenter-فقط-برای-رابط-کاربری-نیست">Presenter فقط برای رابط کاربری نیست<a href="https://example.com/blog/presenter-and-humble-object#presenter-%D9%81%D9%82%D8%B7-%D8%A8%D8%B1%D8%A7%DB%8C-%D8%B1%D8%A7%D8%A8%D8%B7-%DA%A9%D8%A7%D8%B1%D8%A8%D8%B1%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به Presenter فقط برای رابط کاربری نیست" title="لینک مستقیم به Presenter فقط برای رابط کاربری نیست" translate="no">​</a></h2>
<p>معمولاً وقتی از Presenter حرف می‌زنیم، ذهنمان به صفحه و کامپوننت می‌رود. اما همین ایده در پاسخ‌های رابط برنامه‌نویسی کاربردی (API)، خروجی گزارش، پیام‌های صف، فایل‌های خروجی و حتی قالب ایمیل هم کاربرد دارد.</p>
<p>فرض کنید یک کنترلر وب چنین کاری می‌کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getAccountResponse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> account </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> accountService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'blocked'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">403</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'حساب مسدود است'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      canWithdraw</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ownerName</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    balance</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">account</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation">balance</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation function" style="color:#d73a49">toLocaleString</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation string" style="color:#e3116c">'fa-IR'</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> تومان</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    canWithdraw</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">balance </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در اینجا هم منطق تصمیم‌گیری داخل بخشی رفته که بهتر است فروتن بماند. کنترلر وب باید درخواست را بگیرد، پاسخ را برگرداند و خطاهای چارچوب را مدیریت کند؛ اما لازم نیست همه‌ی منطق ارائه‌ی پاسخ را خودش نگه دارد.</p>
<p>می‌توانیم خروجی پاسخ را جدا کنیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">presentAccountResponse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">account</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Account</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> AccountResponse </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'blocked'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      statusCode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">403</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'حساب مسدود است'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        canWithdraw</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    statusCode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">200</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ownerName</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      balance</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">account</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation">balance</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation function" style="color:#d73a49">toLocaleString</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation string" style="color:#e3116c">'fa-IR'</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> تومان</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      canWithdraw</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> account</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">balance </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و کنترلر را ساده‌تر نگه داریم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getAccountResponse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Request</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Response</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> account </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> accountService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">presentAccountResponse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">account</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">statusCode</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این طراحی، کنترلر همان Humble Object است: کم‌منطق، وابسته به چارچوب، و مسئول کارهای اجرایی. Presenter بخش قابل‌آزمون‌تر است: داده را می‌گیرد، تصمیم می‌گیرد و خروجی آماده‌ی پاسخ می‌سازد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>برای طراحی Presenter از این پرسش شروع کنید: «اگر رابط کاربری فقط قرار باشد نمایش دهد، چه داده‌ای باید آماده و بی‌ابهام به آن برسد؟»</p><p>Presenter خوب معمولاً خروجی‌ای می‌سازد که رابط کاربری برای نمایش آن به تصمیم‌های پیچیده نیاز نداشته باشد. یعنی متن‌ها، وضعیت‌ها، قالب‌بندی‌ها، امکان انجام عملیات، و پیام‌های مهم تا حد ممکن پیش از رسیدن به UI آماده شده باشند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مرز-دقیق-کجاست">مرز دقیق کجاست؟<a href="https://example.com/blog/presenter-and-humble-object#%D9%85%D8%B1%D8%B2-%D8%AF%D9%82%DB%8C%D9%82-%DA%A9%D8%AC%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به مرز دقیق کجاست؟" title="لینک مستقیم به مرز دقیق کجاست؟" translate="no">​</a></h2>
<p>نباید از این الگو برداشت افراطی داشته باشیم. قرار نیست هیچ شرطی در رابط کاربری باقی نماند. مثلاً شرطی مثل «اگر <code>warningText</code> وجود داشت، آن را نمایش بده» معمولاً مسئله‌ای ندارد. این یک شرط نمایشی ساده است.</p>
<p>اما شرط‌هایی که معنای کسب‌وکاری یا محصولی دارند بهتر است از رابط کاربری بیرون بیایند. برای نمونه:</p>
<ul>
<li class="">آیا کاربر اجازه‌ی برداشت دارد؟</li>
<li class="">آیا این وضعیت باید هشدار قرمز بسازد؟</li>
<li class="">آیا پاسخ باید با کد ۴۰۳ برگردد یا ۲۰۰؟</li>
<li class="">آیا این مبلغ باید پنهان شود؟</li>
<li class="">آیا کاربر باید پیام محدودیت ببیند؟</li>
</ul>
<p>این‌ها تصمیم‌های مهم‌تری هستند و آزمون‌کردنشان باید آسان باشد.</p>
<p>یک قاعده‌ی ساده این است: اگر برای اطمینان از یک تصمیم مجبورید کامپوننت را رندر کنید، ولی خود تصمیم ربطی به رندر ندارد، احتمالاً آن تصمیم جای بهتری بیرون از UI دارد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="humble-object-یعنی-بیارزشکردن-ui-نیست">Humble Object یعنی بی‌ارزش‌کردن UI نیست<a href="https://example.com/blog/presenter-and-humble-object#humble-object-%DB%8C%D8%B9%D9%86%DB%8C-%D8%A8%DB%8C%D8%A7%D8%B1%D8%B2%D8%B4%DA%A9%D8%B1%D8%AF%D9%86-ui-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به Humble Object یعنی بی‌ارزش‌کردن UI نیست" title="لینک مستقیم به Humble Object یعنی بی‌ارزش‌کردن UI نیست" translate="no">​</a></h2>
<p>گاهی وقتی می‌گوییم رابط کاربری باید کم‌منطق باشد، ممکن است این سوءبرداشت پیش بیاید که UI بخش کم‌اهمیت سیستم است. این برداشت درست نیست. رابط کاربری بسیار مهم است، چون جایی است که کاربر با سیستم روبه‌رو می‌شود.</p>
<p>اما مهم‌بودن UI به این معنا نیست که باید همه‌ی تصمیم‌ها در آن باشد. اتفاقاً چون رابط کاربری مهم است و خطاهایش مستقیم دیده می‌شوند، بهتر است تا حد ممکن ساده، قابل‌فهم و کم‌ریسک بماند.</p>
<p>در الگوی Humble Object، بخش فروتن تحقیر نمی‌شود؛ بلکه از بار اضافه محافظت می‌شود. رابط کاربری کار خودش را خوب انجام می‌دهد: نمایش، دریافت رخداد، فراخوانی عملیات، و اتصال به محیط اجرا. تصمیم‌های پیچیده‌تر در جایی قرار می‌گیرند که با هزینه‌ی کمتر آزمون می‌شوند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چه-زمانی-presenter-لازم-نیست">چه زمانی Presenter لازم نیست؟<a href="https://example.com/blog/presenter-and-humble-object#%DA%86%D9%87-%D8%B2%D9%85%D8%A7%D9%86%DB%8C-presenter-%D9%84%D8%A7%D8%B2%D9%85-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به چه زمانی Presenter لازم نیست؟" title="لینک مستقیم به چه زمانی Presenter لازم نیست؟" translate="no">​</a></h2>
<p>هر صفحه یا پاسخ ساده‌ای نیاز به Presenter جدا ندارد. اگر یک صفحه فقط چند فیلد را مستقیم نمایش می‌دهد و تصمیم خاصی در آن نیست، افزودن Presenter ممکن است فقط کد را شلوغ کند.</p>
<p>برای نمونه، چنین چیزی شاید نیازی به Presenter جدا نداشته باشد:</p>
<div class="language-tsx codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-tsx codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">UserName</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">}</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">user</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">User</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">span</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">name</span><span class="token punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">span</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اما اگر همین بخش کم‌کم شروع کند به تصمیم‌گیری درباره‌ی وضعیت، مجوز، قالب‌بندی، پیام، رنگ، هشدار و رفتار دکمه‌ها، آن‌وقت بهتر است مرزها را دوباره بررسی کنیم.</p>
<p>پس معیار اصلی، وجود منطق تصمیم‌گیری و هزینه‌ی آزمون است، نه صرفاً تعداد خط‌های کد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-ساختار-ساده-برای-جداکردن-presenter">یک ساختار ساده برای جداکردن Presenter<a href="https://example.com/blog/presenter-and-humble-object#%DB%8C%DA%A9-%D8%B3%D8%A7%D8%AE%D8%AA%D8%A7%D8%B1-%D8%B3%D8%A7%D8%AF%D9%87-%D8%A8%D8%B1%D8%A7%DB%8C-%D8%AC%D8%AF%D8%A7%DA%A9%D8%B1%D8%AF%D9%86-presenter" class="hash-link" aria-label="لینک مستقیم به یک ساختار ساده برای جداکردن Presenter" title="لینک مستقیم به یک ساختار ساده برای جداکردن Presenter" translate="no">​</a></h2>
<p>در یک پروژه‌ی واقعی، ممکن است ساختار پوشه‌ها چیزی شبیه این باشد:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">account-summary/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  account-summary.tsx</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  account-summary-presenter.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  account-summary-presenter.test.ts</span><br></span></code></pre></div></div>
<p>یا اگر با پاسخ رابط برنامه‌نویسی کاربردی کار می‌کنید:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">account/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  account-controller.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  account-response-presenter.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  account-response-presenter.test.ts</span><br></span></code></pre></div></div>
<p>در این ساختار، فایل UI یا کنترلر کوچک و اجرایی می‌ماند، و منطق آماده‌سازی خروجی در فایل Presenter قرار می‌گیرد. آزمون هم کنار همان منطق نوشته می‌شود.</p>
<p>این ساختار قرار نیست قانون همیشگی باشد، اما کمک می‌کند مرز بین «نمایش» و «آماده‌سازی برای نمایش» روشن بماند.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>اطلاع</div><div class="admonitionContent_hiHs"><p>در این بحث، مدل نمایش یا View Model خروجی‌ای است که Presenter برای مصرف رابط کاربری یا کنترلر آماده می‌کند.</p><p>این مدل معمولاً داده‌ی خام دامنه نیست. داده‌ای است که برای نمایش آماده شده: متن‌ها، برچسب‌ها، وضعیت‌ها، امکان انجام عملیات، پیام‌ها و قالب‌بندی‌های لازم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/presenter-and-humble-object#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای استفاده از این ایده، لازم نیست همه‌ی صفحه‌ها و کنترلرها را بازنویسی کنید. از بخش‌هایی شروع کنید که منطق تصمیم‌گیری در آن‌ها پنهان شده و آزمون‌کردنشان سخت است.</p>
<p>چیزهایی که باید بررسی شوند:</p>
<ul>
<li class="">آیا صفحه یا کنترلر چند شرط محصولی یا کسب‌وکاری دارد؟</li>
<li class="">آیا برای آزمون یک تصمیم ساده مجبورید UI را رندر کنید؟</li>
<li class="">آیا قالب‌بندی متن، عدد، وضعیت و پیام‌ها در چند جای مختلف تکرار شده است؟</li>
<li class="">آیا خطاهای نمایشی به‌خاطر تصمیم‌های پنهان در UI رخ می‌دهند؟</li>
<li class="">آیا می‌توان خروجی موردنیاز صفحه را به‌صورت یک View Model روشن تعریف کرد؟</li>
</ul>
<p>چیزهایی که نباید بی‌دلیل تغییر کنند:</p>
<ul>
<li class="">کامپوننت‌های ساده‌ای که فقط داده را نمایش می‌دهند.</li>
<li class="">شرط‌های نمایشی کوچک، مثل وجود یا نبود یک متن آماده.</li>
<li class="">ساختارهایی که هنوز درد واقعی در آزمون یا تغییر ایجاد نکرده‌اند.</li>
<li class="">منطق‌هایی که جای درستشان واقعاً در خود تعامل کاربر است، نه در Presenter.</li>
</ul>
<p>برای اعتبارسنجی تغییر، دست‌کم این گام‌ها را اجرا کنید:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">npm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run lint</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run typecheck</span><br></span></code></pre></div></div>
<p>اگر پروژه‌ی شما این فرمان‌ها را ندارد، معادل آن‌ها را اجرا کنید. بعد از جداسازی، بهتر است آزمون Presenter را جداگانه بنویسید و یک آزمون سبک هم برای خود UI نگه دارید تا مطمئن شوید داده‌ی آماده‌شده درست نمایش داده می‌شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/presenter-and-humble-object#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>الگوی Presenter و Humble Object می‌خواهد ما منطق مهم را از بخش‌هایی جدا کنیم که آزمون‌کردنشان سخت‌تر است. رابط کاربری، کنترلر وب، فریم‌ورک و ابزارهای بیرونی لازم‌اند، اما بهتر است کم‌منطق بمانند.</p>
<p>هرچه بخش سخت‌آزمون کم‌ادعاتر باشد، آزمون‌پذیری سیستم بیشتر می‌شود. تصمیم‌ها، قالب‌بندی‌ها و آماده‌سازی خروجی اگر در Presenter قرار بگیرند، ساده‌تر آزمون می‌شوند و تغییرشان ریسک کمتری دارد.</p>
<p>طراحی خوب همیشه به معنی ساختن لایه‌های زیاد نیست. گاهی یعنی یک تصمیم ساده: چیزی که سخت آزمون می‌شود، فروتن بماند؛ چیزی که مهم و پرمنطق است، به جایی برود که بتوان آن را روشن، سریع و مطمئن آزمود.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="آزمون‌پذیری" term="آزمون‌پذیری"/>
        <category label="Presenter" term="Presenter"/>
        <category label="معماری تمیز" term="معماری تمیز"/>
        <category label="رابط کاربری" term="رابط کاربری"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[گره‌های پنهان در معماری نرم‌افزار]]></title>
        <id>https://example.com/blog/hidden-dependency-cycles-in-software-design</id>
        <link href="https://example.com/blog/hidden-dependency-cycles-in-software-design"/>
        <updated>2026-05-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[نگاهی تحلیلی به وابستگی‌های چرخه‌ای و این‌که چگونه از یک خطای ساده در کد به مسئله‌ای در معماری نرم‌افزار می‌رسند.]]></summary>
        <content type="html"><![CDATA[<p>یک روز برنامه را اجرا می‌کنی و با خطایی روبه‌رو می‌شوی که در نگاه اول ساده به نظر می‌رسد: چیزی در زمان واردسازی پیدا نشده، ماژولی هنوز کامل آماده نیست، یا بخشی از کد زودتر از موعد اجرا شده است.</p>
<p>واکنش نخست معمولاً فنی و سریع است: یک واردسازی را جابه‌جا می‌کنی، آن را به درون تابع می‌بری، نام یک فایل را عوض می‌کنی، یا چند خط کد را کمی عقب و جلو می‌کنی. برنامه دوباره اجرا می‌شود و خطا از بین می‌رود.</p>
<p>اما پرسش اصلی همین‌جاست:</p>
<p><strong>آیا مشکل حل شد، یا فقط صدای هشدار را خاموش کردیم؟</strong></p>
<p>این نوشته درباره‌ی همین هشدارهای کوچک است؛ هشدارهایی که گاهی از مسئله‌ای عمیق‌تر خبر می‌دهند: <strong>وابستگی چرخه‌ای</strong>. یعنی جایی که چند بخش از نرم‌افزار به‌جای آنکه با مرزهای روشن با هم همکاری کنند، آن‌قدر به هم گره می‌خورند که هر کدام برای فهمیده‌شدن یا اجراشدن، به دیگری نیاز دارد.</p>
<p>پایتون نمونه‌ی خوبی برای دیدن این مسئله است، چون خطای واردسازی چرخه‌ای را زود و آشکار نشان می‌دهد. اما موضوع اصلی این نوشته پایتون نیست. موضوع اصلی، شکل وابستگی‌ها در طراحی نرم‌افزار است.</p>
<p><img decoding="async" loading="lazy" alt="نموداری مفهومی از وابستگی‌های چرخه‌ای در طراحی نرم‌افزار" src="https://example.com/assets/images/hidden-dependency-cycles-in-software-design-5e60ca0f21bc145126c9446406318072.png" width="1672" height="941" class="img_m9Pm"></p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>ایده‌ی اصلی</div><div class="admonitionContent_hiHs"><p>گاهی خطای واردسازی فقط یک خطای فنی نیست؛ نشانه‌ای است از اینکه دو بخش نرم‌افزار بیش از اندازه از هم خبر دارند و مرز مسئولیت‌هایشان روشن نیست.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-مثال-ساده-جایی-که-گره-آرام-شکل-میگیرد">یک مثال ساده؛ جایی که گره آرام شکل می‌گیرد<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%8C%DA%A9-%D9%85%D8%AB%D8%A7%D9%84-%D8%B3%D8%A7%D8%AF%D9%87-%D8%AC%D8%A7%DB%8C%DB%8C-%DA%A9%D9%87-%DA%AF%D8%B1%D9%87-%D8%A2%D8%B1%D8%A7%D9%85-%D8%B4%DA%A9%D9%84-%D9%85%DB%8C%DA%AF%DB%8C%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به یک مثال ساده؛ جایی که گره آرام شکل می‌گیرد" title="لینک مستقیم به یک مثال ساده؛ جایی که گره آرام شکل می‌گیرد" translate="no">​</a></h2>
<p>فرض کنیم سامانه‌ای داریم با دو بخش ساده:</p>
<ul>
<li class="">بخش کاربر، برای ساختن و مدیریت حساب‌ها؛</li>
<li class="">بخش پیام‌رسانی، برای فرستادن پیام‌ها.</li>
</ul>
<p>در آغاز، همه‌چیز طبیعی است. بخش کاربر پس از ساختن حساب، باید پیام خوشامدگویی بفرستد. پس به بخش پیام‌رسانی نیاز دارد.</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">بخش کاربر → بخش پیام‌رسانی</span><br></span></code></pre></div></div>
<p>کمی بعد، بخش پیام‌رسانی برای ساختن متن پیام، به نام و وضعیت کاربر نیاز پیدا می‌کند. پس آن هم به بخش کاربر وابسته می‌شود.</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">بخش پیام‌رسانی → بخش کاربر</span><br></span></code></pre></div></div>
<p>حالا اگر این دو خط را کنار هم بگذاریم، با چرخه روبه‌رو می‌شویم:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">بخش کاربر → بخش پیام‌رسانی → بخش کاربر</span><br></span></code></pre></div></div>
<p>هیچ‌کدام از این دو تصمیم، به‌تنهایی عجیب نیست. حتی هر دو قابل دفاع‌اند. اما حاصل آن‌ها طراحی‌ای است که در آن دو بخش دیگر به‌راحتی از هم جدا نمی‌شوند.</p>
<p>مشکل چرخه‌ها معمولاً همین است: با یک تصمیم بزرگ و آشکار وارد سیستم نمی‌شوند؛ با چند تصمیم کوچک و به‌ظاهر منطقی ساخته می‌شوند.</p>
<div class="theme-admonition theme-admonition-caution admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>نشانه‌ی خطر</div><div class="admonitionContent_hiHs"><p>اگر برای فهمیدن یک بخش، مجبور می‌شویم بخش دیگری را بخوانیم و آن بخش هم دوباره ما را به بخش نخست برمی‌گرداند، احتمالاً با یک گره طراحی روبه‌رو هستیم، نه فقط یک مشکل محلی در کد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وابستگی-چرخهای-یعنی-چه">وابستگی چرخه‌ای یعنی چه؟<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%DA%86%D8%B1%D8%AE%D9%87%D8%A7%DB%8C-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به وابستگی چرخه‌ای یعنی چه؟" title="لینک مستقیم به وابستگی چرخه‌ای یعنی چه؟" translate="no">​</a></h2>
<p>برای ساده‌کردن بحث، نرم‌افزار را مثل یک نقشه ببینیم. هر ماژول، بسته یا مؤلفه یک نقطه است. هر وابستگی هم یک پیکان.</p>
<p>اگر «الف» برای کار کردن به «ب» نیاز داشته باشد، پیکان از «الف» به «ب» می‌رود:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">الف → ب</span><br></span></code></pre></div></div>
<p>تا اینجا مسئله‌ای نیست. مشکل از جایی آغاز می‌شود که مسیر پیکان‌ها دوباره به نقطه‌ی آغاز برگردد:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">الف → ب → پ → الف</span><br></span></code></pre></div></div>
<p>در این حالت، دیگر با چند جزء مستقل طرف نیستیم. با مجموعه‌ای طرفیم که اجزایش به هم قفل شده‌اند. «الف» بدون «ب» کامل فهمیده نمی‌شود، «ب» بدون «پ» ناقص است، و «پ» دوباره به «الف» برمی‌گردد.</p>
<p>این همان چیزی است که در معماری نرم‌افزار اهمیت دارد: <strong>جهت وابستگی‌ها</strong>.</p>
<p>معماری فقط انتخاب چارچوب، پایگاه داده یا ابزار استقرار نیست. بخش مهمی از معماری این است که بدانیم کدام بخش‌ها حق دارند به کدام بخش‌ها وابسته باشند، و کدام وابستگی‌ها باید ممنوع، محدود یا دست‌کم آشکار باشند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-این-چرخهها-دردسرساز-میشوند">چرا این چرخه‌ها دردسرساز می‌شوند؟<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DA%86%D8%B1%D8%A7-%D8%A7%DB%8C%D9%86-%DA%86%D8%B1%D8%AE%D9%87%D9%87%D8%A7-%D8%AF%D8%B1%D8%AF%D8%B3%D8%B1%D8%B3%D8%A7%D8%B2-%D9%85%DB%8C%D8%B4%D9%88%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به چرا این چرخه‌ها دردسرساز می‌شوند؟" title="لینک مستقیم به چرا این چرخه‌ها دردسرساز می‌شوند؟" translate="no">​</a></h2>
<p>وابستگی چرخه‌ای همیشه برنامه را همان لحظه خراب نمی‌کند. گاهی خطر آن آرام‌تر و پنهان‌تر است. کد کار می‌کند، اما کم‌کم سخت‌تر فهمیده می‌شود، سخت‌تر آزموده می‌شود و سخت‌تر تغییر می‌کند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۱-فهم-کد-سنگینتر-میشود">۱. فهم کد سنگین‌تر می‌شود<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B1-%D9%81%D9%87%D9%85-%DA%A9%D8%AF-%D8%B3%D9%86%DA%AF%DB%8C%D9%86%D8%AA%D8%B1-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به ۱. فهم کد سنگین‌تر می‌شود" title="لینک مستقیم به ۱. فهم کد سنگین‌تر می‌شود" translate="no">​</a></h3>
<p>در طراحی خوب، می‌توان یک بخش را باز کرد و تا حد زیادی همان‌جا فهمید که چه می‌کند. اما در طراحی چرخه‌ای، خواننده مدام میان فایل‌ها و مفاهیم رفت‌وبرگشت می‌کند.</p>
<p>برای فهمیدن بخش کاربر، باید پیام‌رسانی را دید. برای فهمیدن پیام‌رسانی، باید دوباره بخش کاربر را دید. نتیجه این است که یک تغییر کوچک، به بررسی چند بخش وابسته تبدیل می‌شود.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۲-تغییرها-اثر-موجی-پیدا-میکنند">۲. تغییرها اثر موجی پیدا می‌کنند<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B2-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1%D9%87%D8%A7-%D8%A7%D8%AB%D8%B1-%D9%85%D9%88%D8%AC%DB%8C-%D9%BE%DB%8C%D8%AF%D8%A7-%D9%85%DB%8C%DA%A9%D9%86%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به ۲. تغییرها اثر موجی پیدا می‌کنند" title="لینک مستقیم به ۲. تغییرها اثر موجی پیدا می‌کنند" translate="no">​</a></h3>
<p>وقتی دو بخش به هم قفل شده‌اند، تغییر در یکی ممکن است دیگری را هم بشکند؛ حتی اگر از نظر مفهومی انتظار چنین اثری نداشته باشیم.</p>
<p>این همان جایی است که توسعه‌دهنده‌ها کم‌کم محتاط می‌شوند. نه چون کد حتماً بزرگ است، بلکه چون معلوم نیست تغییر یک بخش، کجای دیگر را تحت تأثیر قرار می‌دهد.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۳-آزمونپذیری-کم-میشود">۳. آزمون‌پذیری کم می‌شود<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B3-%D8%A2%D8%B2%D9%85%D9%88%D9%86%D9%BE%D8%B0%DB%8C%D8%B1%DB%8C-%DA%A9%D9%85-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به ۳. آزمون‌پذیری کم می‌شود" title="لینک مستقیم به ۳. آزمون‌پذیری کم می‌شود" translate="no">​</a></h3>
<p>آزمودن یک بخش وقتی ساده است که بتوان آن را جدا ساخت و اجرا کرد. اما وقتی «الف» برای ساخته‌شدن به «ب» نیاز دارد و «ب» هم به «الف»، جداسازی سخت می‌شود.</p>
<p>در چنین وضعی، آزمون‌های کوچک و روشن جای خود را به آزمون‌های بزرگ‌تر، شکننده‌تر و پر از وصله می‌دهند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۴-راهاندازی-سیستم-حساس-میشود">۴. راه‌اندازی سیستم حساس می‌شود<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B4-%D8%B1%D8%A7%D9%87%D8%A7%D9%86%D8%AF%D8%A7%D8%B2%DB%8C-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D8%AD%D8%B3%D8%A7%D8%B3-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به ۴. راه‌اندازی سیستم حساس می‌شود" title="لینک مستقیم به ۴. راه‌اندازی سیستم حساس می‌شود" translate="no">​</a></h3>
<p>در برخی زبان‌ها و چارچوب‌ها، ترتیب ساخته‌شدن اجزا مهم است. چرخه‌ها این ترتیب را شکننده می‌کنند. در پایتون، این شکنندگی هنگام واردسازی دیده می‌شود. در سامانه‌های بزرگ‌تر، ممکن است هنگام راه‌اندازی سرویس‌ها، ساختن وابستگی‌ها یا بارگذاری تنظیمات خودش را نشان دهد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>نکته‌ی مهم</div><div class="admonitionContent_hiHs"><p>خطر وابستگی چرخه‌ای فقط در «خطا دادن» نیست. گاهی برنامه خطا نمی‌دهد، اما طراحی آن چنان درهم می‌شود که هر تغییر کوچک با ترس و هزینه همراه است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نمونهی-ملموس-در-پایتون">نمونه‌ی ملموس در پایتون<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%D9%86%D9%85%D9%88%D9%86%D9%87%DB%8C-%D9%85%D9%84%D9%85%D9%88%D8%B3-%D8%AF%D8%B1-%D9%BE%D8%A7%DB%8C%D8%AA%D9%88%D9%86" class="hash-link" aria-label="لینک مستقیم به نمونه‌ی ملموس در پایتون" title="لینک مستقیم به نمونه‌ی ملموس در پایتون" translate="no">​</a></h2>
<p>پایتون چرخه‌های وابستگی را زود لو می‌دهد، چون ماژول‌ها هنگام واردسازی اجرا می‌شوند. اگر دو فایل در سطح بالای خود به هم وابسته باشند، ممکن است یکی از آن‌ها به چیزی نیاز داشته باشد که هنوز ساخته نشده است.</p>
<p>این نمونه را ببینیم:</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">users.py</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> notifications </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> send_welcome_message</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">User</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">pass</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">create_user</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> User</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    send_welcome_message</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> user</span><br></span></code></pre></div></div>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_XX_l">notifications.py</div><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> users </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> User</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">send_welcome_message</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> User</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Welcome!"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>در نگاه اول، هر دو واردسازی طبیعی به نظر می‌رسند. فایل <code>users.py</code> برای فرستادن پیام به <code>notifications.py</code> نیاز دارد. فایل <code>notifications.py</code> هم برای اشاره به نوع کاربر، <code>User</code> را از <code>users.py</code> وارد کرده است.</p>
<p>اما همین رابطه‌ی دوطرفه می‌تواند مشکل بسازد. پایتون هنگام واردسازی، فایل‌ها را اجرا می‌کند. اگر یکی از فایل‌ها هنوز کامل آماده نشده باشد و فایل دیگر بخواهد از آن نامی را بردارد، خطا رخ می‌دهد.</p>
<p>راه‌حل سریع ممکن است این باشد که واردسازی دوم را به درون تابع ببریم یا برای اشاره‌های نوعی از روش‌هایی مثل <code>TYPE_CHECKING</code> استفاده کنیم. این روش‌ها گاهی درست و لازم‌اند. اما نباید پرسش اصلی را پنهان کنند:</p>
<p><strong>آیا پیام‌رسانی واقعاً باید کلاس کامل کاربر را بشناسد؟</strong></p>
<p>شاید کافی باشد فقط نام و رایانامه‌ی کاربر را بگیرد. شاید باید یک قرارداد کوچک‌تر تعریف شود. شاید هم مفهوم مشترکی وجود دارد که باید از هر دو ماژول بیرون کشیده شود.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>پرسش طراحی</div><div class="admonitionContent_hiHs"><p>وقتی با واردسازی چرخه‌ای روبه‌رو می‌شویم، نخستین پرسش نباید این باشد که «چطور خطا را دور بزنم؟» پرسش بهتر این است: «چرا این دو ماژول تا این اندازه به جزئیات هم وابسته‌اند؟»</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آیا-هر-چرخهای-بد-است">آیا هر چرخه‌ای بد است؟<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%D8%A2%DB%8C%D8%A7-%D9%87%D8%B1-%DA%86%D8%B1%D8%AE%D9%87%D8%A7%DB%8C-%D8%A8%D8%AF-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به آیا هر چرخه‌ای بد است؟" title="لینک مستقیم به آیا هر چرخه‌ای بد است؟" translate="no">​</a></h2>
<p>نه. پاسخ دقیق‌تر این است: <strong>چرخه‌ها یکسان نیستند</strong>.</p>
<p>گاهی چرخه فقط برای بررسی نوع‌ها ساخته شده است و در زمان اجرای برنامه نقشی ندارد. در این حالت، می‌توان آن را با روش‌هایی مثل واردسازی مشروط از مسیر اجرا بیرون برد.</p>
<p>گاهی هم چرخه کوچک، محدود و آگاهانه است. شاید شکستن آن طراحی را پیچیده‌تر کند و سود چندانی نداشته باشد. در چنین وضعی، پذیرش چرخه می‌تواند تصمیمی عملی باشد؛ به شرطی که پنهان و بی‌مرز رها نشود.</p>
<p>اما چرخه زمانی نگران‌کننده می‌شود که در مسیر اصلی اجرای برنامه باشد، مرز لایه‌ها را بشکند، آزمون‌پذیری را کم کند، یا تغییر یک بخش را به تغییر بخش‌های نامرتبط گره بزند.</p>
<p>پس پرسش درست این نیست:</p>
<blockquote>
<p>آیا هر چرخه‌ای بد است؟</p>
</blockquote>
<p>پرسش دقیق‌تر این است:</p>
<blockquote>
<p>این چرخه چه چیزهایی را به هم گره زده، چه زمانی فعال می‌شود، و چه هزینه‌ای به فهم، آزمون و تغییر سیستم تحمیل می‌کند؟</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نگاه-معماری-نرمافزار">نگاه معماری نرم‌افزار<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%D9%86%DA%AF%D8%A7%D9%87-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D9%86%D8%B1%D9%85%D8%A7%D9%81%D8%B2%D8%A7%D8%B1" class="hash-link" aria-label="لینک مستقیم به نگاه معماری نرم‌افزار" title="لینک مستقیم به نگاه معماری نرم‌افزار" translate="no">​</a></h2>
<p>در معماری نرم‌افزار، وابستگی‌ها موضوعی حاشیه‌ای نیستند. بسیاری از تصمیم‌های مهم معماری، در نهایت، درباره‌ی همین پرسش‌اند:</p>
<p><strong>چه چیزی مجاز است به چه چیزی وابسته باشد؟</strong></p>
<p>رابرت مارتین در بحث طراحی مؤلفه‌ها از اصلی به نام <strong>اصل وابستگی‌های بدون چرخه</strong> یاد می‌کند. مضمون این اصل ساده است: گراف وابستگی میان مؤلفه‌ها نباید چرخه داشته باشد.</p>
<p>این اصل را نباید مثل یک قانون خشک خواند. ارزش آن در این است که ما را وادار می‌کند جهت وابستگی‌ها را ببینیم. وقتی دو مؤلفه به هم نیاز دارند، شاید یکی از این پرسش‌ها بی‌پاسخ مانده است:</p>
<ul>
<li class="">آیا مسئولیت‌ها درست جدا شده‌اند؟</li>
<li class="">آیا یک مفهوم مشترک باید به جای سومی منتقل شود؟</li>
<li class="">آیا یکی از بخش‌ها باید به قرارداد وابسته باشد، نه به پیاده‌سازی مشخص؟</li>
<li class="">آیا جهت وابستگی با جهت تغییرهای واقعی سیستم سازگار است؟</li>
</ul>
<p>از این زاویه، خطای واردسازی در پایتون فقط یک نشانه‌ی محلی است. مسئله‌ی اصلی ممکن است این باشد که طراحی، مرزهای روشنی برای وابستگی‌ها نگذاشته است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="با-چرخهها-چه-کنیم">با چرخه‌ها چه کنیم؟<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%D8%A8%D8%A7-%DA%86%D8%B1%D8%AE%D9%87%D9%87%D8%A7-%DA%86%D9%87-%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به با چرخه‌ها چه کنیم؟" title="لینک مستقیم به با چرخه‌ها چه کنیم؟" translate="no">​</a></h2>
<p>برای برخورد با وابستگی چرخه‌ای، یک نسخه‌ی همیشگی وجود ندارد. اما چند راهکار معمولاً کمک می‌کنند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۱-مفهوم-مشترک-را-بیرون-بکشیم">۱. مفهوم مشترک را بیرون بکشیم<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B1-%D9%85%D9%81%D9%87%D9%88%D9%85-%D9%85%D8%B4%D8%AA%D8%B1%DA%A9-%D8%B1%D8%A7-%D8%A8%DB%8C%D8%B1%D9%88%D9%86-%D8%A8%DA%A9%D8%B4%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به ۱. مفهوم مشترک را بیرون بکشیم" title="لینک مستقیم به ۱. مفهوم مشترک را بیرون بکشیم" translate="no">​</a></h3>
<p>اگر دو بخش به یک نوع، ثابت، تابع یا قرارداد مشترک نیاز دارند، شاید آن مفهوم واقعاً متعلق به هیچ‌کدام از آن دو نیست.</p>
<p>به‌جای این:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">users.py → notifications.py → users.py</span><br></span></code></pre></div></div>
<p>می‌توانیم به این برسیم:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">users.py          → shared.py</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">notifications.py → shared.py</span><br></span></code></pre></div></div>
<p>با این کار، دو بخش هنوز از یک مفهوم مشترک استفاده می‌کنند، اما دیگر به هم قفل نشده‌اند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۲-به-قرارداد-وابسته-شویم-نه-به-جزئیات">۲. به قرارداد وابسته شویم، نه به جزئیات<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B2-%D8%A8%D9%87-%D9%82%D8%B1%D8%A7%D8%B1%D8%AF%D8%A7%D8%AF-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%D9%87-%D8%B4%D9%88%DB%8C%D9%85-%D9%86%D9%87-%D8%A8%D9%87-%D8%AC%D8%B2%D8%A6%DB%8C%D8%A7%D8%AA" class="hash-link" aria-label="لینک مستقیم به ۲. به قرارداد وابسته شویم، نه به جزئیات" title="لینک مستقیم به ۲. به قرارداد وابسته شویم، نه به جزئیات" translate="no">​</a></h3>
<p>گاهی یک ماژول فقط به بخش کوچکی از اطلاعات ماژول دیگر نیاز دارد، اما به کل آن وابسته شده است. برای نمونه، پیام‌رسانی شاید فقط نام و رایانامه‌ی کاربر را بخواهد، نه کل کلاس کاربر را.</p>
<p>هرچه وابستگی کوچک‌تر و دقیق‌تر باشد، تغییرهای آینده کم‌هزینه‌تر می‌شوند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۳-جهت-وابستگی-را-برگردانیم">۳. جهت وابستگی را برگردانیم<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B3-%D8%AC%D9%87%D8%AA-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%D8%B1%D8%A7-%D8%A8%D8%B1%DA%AF%D8%B1%D8%AF%D8%A7%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به ۳. جهت وابستگی را برگردانیم" title="لینک مستقیم به ۳. جهت وابستگی را برگردانیم" translate="no">​</a></h3>
<p>گاهی جهت وابستگی اشتباه است. برای نمونه، منطق دامنه نباید مستقیماً به ابزار پیام‌رسانی وابسته شود. شاید بهتر باشد دامنه فقط یک رویداد تولید کند: «کاربر ساخته شد». سپس بخش دیگری آن رویداد را بشنود و پیام بفرستد.</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">دامنه → رویداد</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">شنونده‌ی رویداد → پیام‌رسانی</span><br></span></code></pre></div></div>
<p>در این طراحی، دامنه از جزئیات پیام‌رسانی بی‌خبر می‌ماند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۴-واردسازی-محلی-را-با-احتیاط-به-کار-ببریم">۴. واردسازی محلی را با احتیاط به کار ببریم<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DB%B4-%D9%88%D8%A7%D8%B1%D8%AF%D8%B3%D8%A7%D8%B2%DB%8C-%D9%85%D8%AD%D9%84%DB%8C-%D8%B1%D8%A7-%D8%A8%D8%A7-%D8%A7%D8%AD%D8%AA%DB%8C%D8%A7%D8%B7-%D8%A8%D9%87-%DA%A9%D8%A7%D8%B1-%D8%A8%D8%A8%D8%B1%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به ۴. واردسازی محلی را با احتیاط به کار ببریم" title="لینک مستقیم به ۴. واردسازی محلی را با احتیاط به کار ببریم" translate="no">​</a></h3>
<p>در پایتون، بردن واردسازی به درون تابع گاهی راهکاری درست است. اما اگر این کار فقط برای پنهان‌کردن درهم‌تنیدگی دو ماژول باشد، مشکل طراحی همچنان باقی است.</p>
<p>واردسازی محلی می‌تواند درمان باشد؛ اما گاهی فقط مسکن است.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>معیار تصمیم</div><div class="admonitionContent_hiHs"><p>اگر چرخه فقط از ابزار بررسی نوع آمده، راهکار فنی کافی است. اما اگر چرخه از منطق اصلی برنامه آمده، بهتر است طراحی وابستگی‌ها بازبینی شود.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرخهها-را-باید-دید-نه-فقط-حذف-کرد">چرخه‌ها را باید دید، نه فقط حذف کرد<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%DA%86%D8%B1%D8%AE%D9%87%D9%87%D8%A7-%D8%B1%D8%A7-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%AF%DB%8C%D8%AF-%D9%86%D9%87-%D9%81%D9%82%D8%B7-%D8%AD%D8%B0%D9%81-%DA%A9%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به چرخه‌ها را باید دید، نه فقط حذف کرد" title="لینک مستقیم به چرخه‌ها را باید دید، نه فقط حذف کرد" translate="no">​</a></h2>
<p>هدف این نیست که هر چرخه‌ای را بی‌درنگ ممنوع کنیم. هدف این است که چرخه‌ها پنهان نمانند.</p>
<p>در پروژه‌های کوچک، شاید بتوان با خواندن چند فایل چرخه را دید. اما در پروژه‌های بزرگ، بهتر است از ابزارهای تحلیل ایستا کمک گرفت. این ابزارها می‌توانند گراف وابستگی ماژول‌ها را بسازند و چرخه‌ها را گزارش کنند.</p>
<p>حتی می‌توان در فرایند یکپارچه‌سازی پیوسته، قاعده‌ای گذاشت که چرخه‌های تازه را آشکار کند. این کار شبیه آزمون‌نوشتن برای معماری است: فقط رفتار برنامه را نمی‌سنجیم، شکل وابستگی‌های آن را هم زیر نظر می‌گیریم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>وابستگی چرخه‌ای را نه باید بیش از اندازه بزرگ کرد، نه باید ساده از کنار آن گذشت.</p>
<p>گاهی با یک اصلاح کوچک، مشکل اجرایی حل می‌شود. اما اگر همان چرخه نشان دهد که دو بخش بیش از اندازه به هم گره خورده‌اند، حل واقعی در طراحی است، نه در جابه‌جایی چند خط کد.</p>
<p>پرسش پایانی این است:</p>
<p><strong>آیا این چرخه حاصل یک نیاز آگاهانه و کنترل‌شده است، یا نشانه‌ای از مرزبندی مبهم میان مسئولیت‌ها؟</strong></p>
<p>اگر پاسخ روشن باشد، شاید چرخه‌ای کوچک پذیرفتنی باشد. اما اگر پاسخ مبهم بماند، همان چرخه‌ی کوچک می‌تواند سرنخی باشد از گره‌ای پنهان در طراحی نرم‌افزار.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>برای بازبینی کد</div><div class="admonitionContent_hiHs"><p>پیش از آنکه خطای واردسازی را فقط با جابه‌جایی چند خط خاموش کنیم، بهتر است یک بار نقشه‌ی وابستگی‌ها را نگاه کنیم. گاهی خطا فقط پیام‌آور است، نه خود مسئله.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="منابع-پیشنهادی-برای-مطالعهی-بیشتر">منابع پیشنهادی برای مطالعه‌ی بیشتر<a href="https://example.com/blog/hidden-dependency-cycles-in-software-design#%D9%85%D9%86%D8%A7%D8%A8%D8%B9-%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF%DB%8C-%D8%A8%D8%B1%D8%A7%DB%8C-%D9%85%D8%B7%D8%A7%D9%84%D8%B9%D9%87%DB%8C-%D8%A8%DB%8C%D8%B4%D8%AA%D8%B1" class="hash-link" aria-label="لینک مستقیم به منابع پیشنهادی برای مطالعه‌ی بیشتر" title="لینک مستقیم به منابع پیشنهادی برای مطالعه‌ی بیشتر" translate="no">​</a></h2>
<ul>
<li class="">Robert C. Martin, <em>Clean Architecture: A Craftsman’s Guide to Software Structure and Design</em>, 2017.</li>
<li class="">Robert C. Martin, <em>Agile Software Development: Principles, Patterns, and Practices</em>, 2002.</li>
<li class="">Mark Richards and Neal Ford, <em>Fundamentals of Software Architecture: An Engineering Approach</em>, 2020.</li>
<li class="">Neal Ford, Rebecca Parsons, and Patrick Kua, <em>Building Evolutionary Architectures: Support Constant Change</em>, 2017.</li>
<li class="">Python Documentation, <em>The import system</em>.</li>
<li class="">Python Documentation, <em>Programming FAQ: Circular imports</em>.</li>
<li class="">mypy Documentation, <em>Annotation issues at runtime</em>.</li>
</ul>]]></content>
        <category label="طراحی نرم‌افزار" term="طراحی نرم‌افزار"/>
        <category label="معماری نرم‌افزار" term="معماری نرم‌افزار"/>
        <category label="پایتون" term="پایتون"/>
        <category label="وابستگی چرخه‌ای" term="وابستگی چرخه‌ای"/>
        <category label="مدولاریت" term="مدولاریت"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[وقتی «برنامه‌نویس» هنوز شغل نبود]]></title>
        <id>https://example.com/blog/dijkstra-programmer-was-not-a-job</id>
        <link href="https://example.com/blog/dijkstra-programmer-was-not-a-job"/>
        <updated>2026-04-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[قصه‌ی ادسخر دایجسترا؛ مردی که پیش از رسمی‌شدن برنامه‌نویسی، آن را جدی گرفت و بعدها از فروتنی در برابر پیچیدگی گفت]]></summary>
        <content type="html"><![CDATA[<p>در فرم ازدواج، یک جای خالی بود: <strong>شغل</strong>.</p>
<p>ادسخر دایجسترا نوشت: <strong>برنامه‌نویس</strong>.</p>
<p>کارمندان آمستردام نپذیرفتند. از نگاه آنان، چنین شغلی وجود نداشت. نه اینکه دایجسترا کاری نمی‌کرد؛ مسئله این بود که جهان هنوز برای کاری که او انجام می‌داد، نام و جایگاه روشنی نداشت.</p>
<p>در سند رسمی ازدواجش، به جای «برنامه‌نویس» نوشتند: <strong>فیزیک‌دان نظری</strong>.</p>
<p>همین صحنه برای آغاز قصه کافی است: مردی که بعدها یکی از چهره‌های بزرگ علوم رایانه شد، در روز ازدواجش حتی نتوانست شغل واقعی خود را روی کاغذ رسمی ثبت کند.</p>
<p><img decoding="async" loading="lazy" alt="تصویر مفهومی درباره‌ی دایجسترا و روزگاری که برنامه‌نویسی هنوز شغل رسمی نبود" src="https://example.com/assets/images/dijkstra-programmer-was-not-a-job-19b0062dad549855603dca6cc947f075.png" width="1672" height="941" class="img_m9Pm"></p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>دایجسترا همان <strong>ادسخر ویبه دایجسترا</strong> است؛ دانشمند هلندی علوم رایانه که نامش با الگوریتم کوتاه‌ترین مسیر، نگاه دقیق به برنامه‌نویسی، و سخنرانی معروف <strong>برنامه‌نویس فروتن</strong> گره خورده است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیش-از-آنکه-برنامهنویسی-نامی-داشته-باشد">پیش از آنکه برنامه‌نویسی نامی داشته باشد<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%D9%BE%DB%8C%D8%B4-%D8%A7%D8%B2-%D8%A2%D9%86%DA%A9%D9%87-%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87%D9%86%D9%88%DB%8C%D8%B3%DB%8C-%D9%86%D8%A7%D9%85%DB%8C-%D8%AF%D8%A7%D8%B4%D8%AA%D9%87-%D8%A8%D8%A7%D8%B4%D8%AF" class="hash-link" aria-label="لینک مستقیم به پیش از آنکه برنامه‌نویسی نامی داشته باشد" title="لینک مستقیم به پیش از آنکه برنامه‌نویسی نامی داشته باشد" translate="no">​</a></h2>
<p>دهه‌ی ۱۹۵۰ را تصور کنیم.</p>
<p>رایانه هنوز وارد زندگی روزمره نشده بود. نه رایانه‌ی شخصی در کار بود، نه تلفن هوشمند، نه نقشه‌ای که مسیر را در چند ثانیه نشان دهد. رایانه‌ها دستگاه‌هایی بزرگ، کمیاب و گران بودند؛ بیشتر در آزمایشگاه‌ها و مراکز پژوهشی دیده می‌شدند، نه در خانه و خیابان و محل کار مردم.</p>
<p>در چنین زمانی، «برنامه‌نویسی» هنوز آن معنایی را نداشت که امروز دارد. امروز وقتی کسی می‌گوید برنامه‌نویس است، اصل شغلش برای دیگران روشن است. شاید بپرسند در چه حوزه‌ای کار می‌کند، چه سامانه‌هایی می‌سازد، یا با چه زبان‌هایی می‌نویسد؛ اما کمتر کسی می‌پرسد: «اصلاً چنین شغلی وجود دارد؟»</p>
<p>برای دایجسترا، ماجرا این‌قدر ساده نبود.</p>
<p>او در جوانی میان دو راه ایستاده بود. یک راه، فیزیک نظری بود؛ رشته‌ای شناخته‌شده، جدی و محترم. راه دیگر، کار با رایانه و برنامه‌نویسی بود؛ مسیری تازه، مبهم و هنوز بی‌اعتبار در چشم بسیاری از مردم.</p>
<p>دایجسترا باید تصمیم می‌گرفت: آیا باید در راهی قدم بگذارد که آینده‌اش روشن نیست، یا به مسیر آشناتر فیزیک برگردد؟</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>بعضی انتخاب‌ها میان «درست» و «نادرست» نیستند؛ میان یک راه آماده و یک راه هنوز ساخته‌نشده‌اند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="گفتوگویی-که-مسیر-را-عوض-کرد">گفت‌وگویی که مسیر را عوض کرد<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%DA%AF%D9%81%D8%AA%D9%88%DA%AF%D9%88%DB%8C%DB%8C-%DA%A9%D9%87-%D9%85%D8%B3%DB%8C%D8%B1-%D8%B1%D8%A7-%D8%B9%D9%88%D8%B6-%DA%A9%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به گفت‌وگویی که مسیر را عوض کرد" title="لینک مستقیم به گفت‌وگویی که مسیر را عوض کرد" translate="no">​</a></h2>
<p>دایجسترا بعدها در سخنرانی جایزه‌ی تورینگ خود، که با نام <strong>برنامه‌نویس فروتن</strong> شناخته می‌شود، از این تردید گفت. او پس از چند سال برنامه‌نویسی هنوز مطمئن نبود این کار واقعاً حرفه‌ای محترم است یا نه.</p>
<p>برای پیدا کردن پاسخ، نزد <strong>آدریان فان وینگاردن</strong> رفت؛ کسی که در مرکز ریاضی آمستردام نقش مهمی در مسیر فکری او داشت.</p>
<p>پرسش دایجسترا ساده بود، اما پشت آن یک نگرانی بزرگ پنهان شده بود:</p>
<blockquote>
<p>آیا برنامه‌نویسی چیزی هست که بتوان یک عمر را صرف آن کرد؟</p>
</blockquote>
<p>پاسخ فان وینگاردن سرنوشت‌ساز بود. او به دایجسترا نگفت مسیر آماده است. نگفت همه‌چیز روشن و پذیرفته‌شده است. برعکس، گفت رایانه‌ها آمده‌اند که بمانند، و چون برنامه‌نویسی هنوز رشته‌ای بالغ نشده، کسانی باید باشند که به ساختن آن کمک کنند.</p>
<p>این پاسخ، وعده‌ی آسایش نبود؛ دعوت به ساختن بود.</p>
<p>دایجسترا وارد حرفه‌ای نشد که نام، رسمیت و معیارهایش از پیش آماده باشد. او وارد جهانی شد که هنوز باید برای خود زبان، روش و شأن فکری می‌ساخت.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-واژه-که-زمانه-هنوز-آن-را-نمیفهمید">یک واژه که زمانه هنوز آن را نمی‌فهمید<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%DB%8C%DA%A9-%D9%88%D8%A7%DA%98%D9%87-%DA%A9%D9%87-%D8%B2%D9%85%D8%A7%D9%86%D9%87-%D9%87%D9%86%D9%88%D8%B2-%D8%A2%D9%86-%D8%B1%D8%A7-%D9%86%D9%85%DB%8C%D9%81%D9%87%D9%85%DB%8C%D8%AF" class="hash-link" aria-label="لینک مستقیم به یک واژه که زمانه هنوز آن را نمی‌فهمید" title="لینک مستقیم به یک واژه که زمانه هنوز آن را نمی‌فهمید" translate="no">​</a></h2>
<p>حالا دوباره به آن فرم ازدواج برگردیم.</p>
<p>دایجسترا نوشت: برنامه‌نویس.<br>
<!-- -->کارمندان نپذیرفتند.<br>
<!-- -->در سند نوشتند: فیزیک‌دان نظری.</p>
<p>این صحنه، در ظاهر اداری و ساده است؛ اما معنای عمیقی دارد. گاهی یک واژه زودتر از زمانه‌ی خود پدیدار می‌شود. هست، اما هنوز در فرم‌ها جا ندارد. واقعی است، اما هنوز رسمیت پیدا نکرده است.</p>
<p>«برنامه‌نویس» برای دایجسترا واقعی بود.<br>
<!-- -->برای اداره‌ی ثبت، نه.</p>
<p>شاید آن کارمند هرگز تصور نمی‌کرد چند دهه بعد، جهان پر از آدم‌هایی شود که خود را برنامه‌نویس، مهندس نرم‌افزار، پژوهشگر الگوریتم، معمار نرم‌افزار یا متخصص سامانه‌های رایانشی می‌نامند.</p>
<p>برای او، برنامه‌نویسی هنوز شغل نبود.<br>
<!-- -->برای دایجسترا، آینده بود.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>این قصه فقط یک خاطره‌ی بامزه از گذشته نیست. یادآوری می‌کند که بسیاری از کارهای مهم امروز، روزی آن‌قدر تازه بودند که زبان رسمی جامعه هنوز آن‌ها را نمی‌شناخت.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-کافه-یک-فنجان-قهوه-یک-ایده">یک کافه، یک فنجان قهوه، یک ایده<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%DB%8C%DA%A9-%DA%A9%D8%A7%D9%81%D9%87-%DB%8C%DA%A9-%D9%81%D9%86%D8%AC%D8%A7%D9%86-%D9%82%D9%87%D9%88%D9%87-%DB%8C%DA%A9-%D8%A7%DB%8C%D8%AF%D9%87" class="hash-link" aria-label="لینک مستقیم به یک کافه، یک فنجان قهوه، یک ایده" title="لینک مستقیم به یک کافه، یک فنجان قهوه، یک ایده" translate="no">​</a></h2>
<p>قصه‌ی دایجسترا به آن سند ازدواج ختم نمی‌شود.</p>
<p>روزی دایجسترا و ریا، همسر آینده‌اش، در آمستردام خرید کرده بودند. خسته شدند و روی تراس یک کافه نشستند تا قهوه‌ای بنوشند.</p>
<p>نه آزمایشگاهی در کار بود.<br>
<!-- -->نه جلسه‌ای رسمی.<br>
<!-- -->نه تخته‌ای پر از فرمول.<br>
<!-- -->نه جمعی از استادان.</p>
<p>فقط یک روز معمولی بود، یک کافه، یک فنجان قهوه، و ذهنی که از اندیشیدن بازنمی‌ایستاد.</p>
<p>دایجسترا در همان فضا به پرسشی ساده فکر کرد:</p>
<blockquote>
<p>کوتاه‌ترین مسیر میان دو شهر چیست؟</p>
</blockquote>
<p>مثلاً از روتردام تا خرونینگن.</p>
<p>امروز این پرسش برای ما بدیهی است. مقصد را در نقشه وارد می‌کنیم و چند لحظه بعد، مسیر پیشنهادی را می‌بینیم. اما در آن زمان، تبدیل چنین پرسشی به روشی دقیق و قابل اجرا برای رایانه، کاری تازه و جدی بود.</p>
<p>زیبایی مسئله در سادگی آن بود. هر کسی می‌فهمید کوتاه‌ترین مسیر یعنی چه. اما هنر دایجسترا این بود که پشت این پرسش ساده، ساختاری دقیق دید.</p>
<p>شهرها می‌توانستند نقطه باشند.<br>
<!-- -->راه‌ها می‌توانستند پیوند میان نقطه‌ها باشند.<br>
<!-- -->فاصله‌ها می‌توانستند وزن این پیوندها باشند.</p>
<p>به زبان علوم رایانه، مسئله به یک <strong>گراف وزن‌دار</strong> تبدیل می‌شد: شبکه‌ای از نقطه‌ها و پیوندها که هر پیوند هزینه یا فاصله‌ای دارد.</p>
<p>پرسش حالا شکل روشن‌تری پیدا کرده بود:</p>
<blockquote>
<p>از یک نقطه آغاز کنیم؛ چگونه کوتاه‌ترین راه را تا نقطه‌های دیگر پیدا کنیم؟</p>
</blockquote>
<p>طبق روایتی که از خود دایجسترا نقل شده، ایده‌ی این روش در حدود بیست دقیقه به ذهنش رسید.</p>
<p>بیست دقیقه؛ اما نه بیست دقیقه‌ی خالی.</p>
<p>پشت آن لحظه، سال‌ها دقت، تمرین ذهنی و زندگی با مسئله بود. بسیاری از ایده‌های بزرگ ناگهانی به نظر می‌رسند، اما فقط برای ذهنی پدیدار می‌شوند که از پیش آماده‌ی دیدن آن‌هاست.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>ایده‌های مهم گاهی در لحظه‌های معمولی پدیدار می‌شوند؛ اما معمولاً فقط به ذهن‌هایی می‌رسند که مدت‌ها با مسئله زندگی کرده‌اند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="دایجسترا-فقط-یک-الگوریتم-نساخت">دایجسترا فقط یک الگوریتم نساخت<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%D8%AF%D8%A7%DB%8C%D8%AC%D8%B3%D8%AA%D8%B1%D8%A7-%D9%81%D9%82%D8%B7-%DB%8C%DA%A9-%D8%A7%D9%84%DA%AF%D9%88%D8%B1%DB%8C%D8%AA%D9%85-%D9%86%D8%B3%D8%A7%D8%AE%D8%AA" class="hash-link" aria-label="لینک مستقیم به دایجسترا فقط یک الگوریتم نساخت" title="لینک مستقیم به دایجسترا فقط یک الگوریتم نساخت" translate="no">​</a></h2>
<p>امروز بسیاری از دانشجویان علوم رایانه، دایجسترا را با <strong>الگوریتم کوتاه‌ترین مسیر</strong> می‌شناسند. الگوریتم، یعنی روشی گام‌به‌گام برای حل یک مسئله. در اینجا مسئله این است که در یک شبکه‌ی وزن‌دار، کوتاه‌ترین مسیر را از یک نقطه به نقطه‌های دیگر پیدا کنیم.</p>
<p>اما اگر دایجسترا را فقط با همین الگوریتم بشناسیم، بخش مهمی از قصه را از دست داده‌ایم.</p>
<p>اهمیت او فقط در ساختن یک روش محاسباتی نبود. اهمیت او در نوع نگاهش بود.</p>
<p>دایجسترا می‌توانست از دل یک پرسش روزمره، مدلی روشن بسازد. مسیرهای واقعی جهان را به شبکه تبدیل کند. فاصله‌ها را به وزن. حرکت میان شهرها را به مسئله‌ای قابل محاسبه. سپس برای آن مسئله، روشی دقیق و فهمیدنی پیشنهاد دهد.</p>
<p>این همان جایی است که برنامه‌نویسی از نوشتن چند دستور فراتر می‌رود.</p>
<p>برنامه‌نویسی، در معنای جدی خود، یعنی دیدن نظم پنهان در مسئله‌ها؛ یعنی ساختن مدلی که هم انسان بتواند آن را بفهمد و هم ماشین بتواند آن را اجرا کند.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>اطلاع</div><div class="admonitionContent_hiHs"><p>برنامه‌نویسی فقط نوشتن کد نیست. گاهی یعنی تبدیل بخشی از جهان به مدلی روشن، قابل فهم و قابل محاسبه.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="برنامهنویس-فروتن-یعنی-چه">برنامه‌نویس فروتن یعنی چه؟<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87%D9%86%D9%88%DB%8C%D8%B3-%D9%81%D8%B1%D9%88%D8%AA%D9%86-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به برنامه‌نویس فروتن یعنی چه؟" title="لینک مستقیم به برنامه‌نویس فروتن یعنی چه؟" translate="no">​</a></h2>
<p>سال‌ها بعد، دایجسترا سخنرانی معروف خود را با عنوان <strong>برنامه‌نویس فروتن</strong> ارائه کرد. این عنوان، اگر درست فهمیده شود، یکی از زیباترین توصیف‌ها از کار برنامه‌نویسی است.</p>
<p>فروتنی در اینجا به معنای کوچک شمردن برنامه‌نویس نیست. دایجسترا نمی‌خواست بگوید برنامه‌نویسی کار کم‌ارزشی است. برعکس، او برنامه‌نویسی را کاری چنان جدی می‌دانست که به فروتنی نیاز دارد.</p>
<p>فروتنی یعنی بدانیم ذهن انسان محدود است.<br>
<!-- -->یعنی پیچیدگی را دست‌کم نگیریم.<br>
<!-- -->یعنی خیال نکنیم چون برنامه امروز اجرا شد، پس آن را فهمیده‌ایم.<br>
<!-- -->یعنی از کدی که فقط «به‌زحمت کار می‌کند» راضی نباشیم.</p>
<p>برای دایجسترا، پرسش مهم فقط این نبود که برنامه کار می‌کند یا نه. پرسش مهم‌تر این بود:</p>
<blockquote>
<p>آیا می‌توانیم بفهمیم چرا درست کار می‌کند؟</p>
</blockquote>
<p>این نگاه هنوز هم تازه است.</p>
<p>در کار روزمره‌ی نرم‌افزار، وسوسه زیاد است: راه‌حلی سریع بنویسیم، چیزی را وصله کنیم، پیچیدگی را عقب بیندازیم، نامی مبهم بگذاریم، ساختاری نامفهوم بسازیم و فقط خوشحال باشیم که برنامه فعلاً اجرا می‌شود.</p>
<p>اما دایجسترا به ما یادآوری می‌کند که «فعلاً کار می‌کند» پایان راه نیست. گاهی تازه آغاز خطر است.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>برای دایجسترا، کار کردن برنامه کافی نبود. برنامه باید تا حد ممکن فهمیدنی، ساده، قابل دفاع و قابل استدلال باشد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="فروتنی-در-برابر-پیچیدگی">فروتنی در برابر پیچیدگی<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%D9%81%D8%B1%D9%88%D8%AA%D9%86%DB%8C-%D8%AF%D8%B1-%D8%A8%D8%B1%D8%A7%D8%A8%D8%B1-%D9%BE%DB%8C%DA%86%DB%8C%D8%AF%DA%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به فروتنی در برابر پیچیدگی" title="لینک مستقیم به فروتنی در برابر پیچیدگی" translate="no">​</a></h2>
<p>برنامه‌نویس فروتن کسی نیست که اعتمادبه‌نفس ندارد. کسی است که پیچیدگی را جدی می‌گیرد.</p>
<p>او می‌داند سامانه‌های نرم‌افزاری خیلی زود از کنترل ذهن انسان بیرون می‌روند. می‌داند هر نام مبهم، هر شاخه‌ی بی‌دلیل، هر وابستگی پنهان و هر تصمیم شتاب‌زده، می‌تواند فهم برنامه را سخت‌تر کند.</p>
<p>پس تلاش می‌کند کد را روشن‌تر بنویسد.<br>
<!-- -->مسئله را درست‌تر صورت‌بندی کند.<br>
<!-- -->مرزها را دقیق‌تر ببیند.<br>
<!-- -->نام‌ها را با دقت انتخاب کند.<br>
<!-- -->و پیچیدگی را نه پنهان، بلکه مهار کند.</p>
<p>این نگاه، امروز حتی مهم‌تر شده است. ابزارها قدرتمندتر شده‌اند، زبان‌ها پیشرفته‌تر شده‌اند، سامانه‌ها بزرگ‌تر شده‌اند، و حتی هوش مصنوعی می‌تواند برای ما کد تولید کند. اما یک چیز تغییر نکرده است: محدودیت ذهن انسان.</p>
<p>اگر کدی را نفهمیم، مالک واقعی آن نیستیم؛ حتی اگر اجرا شود.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>فروتنی در برنامه‌نویسی یعنی پذیرفتن اینکه پیچیدگی از ما نیرومندتر است، مگر آنکه آگاهانه مهارش کنیم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-این-قصه-هنوز-زنده-است">چرا این قصه هنوز زنده است؟<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%DA%86%D8%B1%D8%A7-%D8%A7%DB%8C%D9%86-%D9%82%D8%B5%D9%87-%D9%87%D9%86%D9%88%D8%B2-%D8%B2%D9%86%D8%AF%D9%87-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به چرا این قصه هنوز زنده است؟" title="لینک مستقیم به چرا این قصه هنوز زنده است؟" translate="no">​</a></h2>
<p>ممکن است بگوییم این‌ها مربوط به گذشته است. امروز دیگر برنامه‌نویسی شغلی شناخته‌شده است. پس چرا باید هنوز قصه‌ی دایجسترا را خواند؟</p>
<p>چون مسئله فقط نام یک شغل نیست.</p>
<p>هنوز هم کارهای تازه‌ای هستند که در آغاز جدی گرفته نمی‌شوند. هنوز هم نقش‌هایی پدیدار می‌شوند که فرم‌های رسمی، ساختارهای سازمانی و حتی زبان روزمره هنوز برایشان آماده نیست. هنوز هم آدم‌هایی هستند که باید میان مسیر امن و مسیر ناشناخته یکی را انتخاب کنند.</p>
<p>از سوی دیگر، خود برنامه‌نویسی هم نیاز دارد بارها از نو جدی گرفته شود.</p>
<p>هر بار که آن را فقط به تولید کد فرو می‌کاهیم، چیزی از معنای آن را از دست می‌دهیم.<br>
<!-- -->هر بار که پیچیدگی را نادیده می‌گیریم، از روح نگاه دایجسترا دور می‌شویم.<br>
<!-- -->هر بار که به «فعلاً کار می‌کند» قانع می‌شویم، فراموش می‌کنیم برنامه‌نویسی فقط اجرا گرفتن از ماشین نیست.</p>
<p>برنامه‌نویسی یعنی دقیق دیدن مسئله.<br>
<!-- -->یعنی ساختن مدل روشن.<br>
<!-- -->یعنی مهار پیچیدگی.<br>
<!-- -->یعنی نوشتن چیزی که فردا هم بتوان آن را فهمید.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="تصویر-آخر">تصویر آخر<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%D8%AA%D8%B5%D9%88%DB%8C%D8%B1-%D8%A2%D8%AE%D8%B1" class="hash-link" aria-label="لینک مستقیم به تصویر آخر" title="لینک مستقیم به تصویر آخر" translate="no">​</a></h2>
<p>برای من، ماندگارترین تصویر از این قصه، دایجسترا در همان کافه است.</p>
<p>نه روی صحنه‌ی جایزه.<br>
<!-- -->نه پشت تریبون دانشگاه.<br>
<!-- -->نه کنار رایانه‌ای بزرگ در آزمایشگاه.</p>
<p>بلکه در یک روز عادی، پس از خرید، کنار ریا، با یک فنجان قهوه.</p>
<p>مردی که عنوان شغلش را در سند ازدواج نپذیرفته بودند، در سکوت به مسئله‌ای فکر می‌کرد که بعدها میلیون‌ها دانشجو و مهندس آن را خواهند آموخت.</p>
<p>این تصویر دلنشین است، چون نشان می‌دهد تاریخ فناوری همیشه در جاهای پرزرق‌وبرق ساخته نمی‌شود. گاهی در یک گفت‌وگو ساخته می‌شود. گاهی در یک فرم اداری که واژه‌ای تازه را نمی‌پذیرد. گاهی هم روی تراس یک کافه، در میانه‌ی خستگی یک روز معمولی.</p>
<p>دایجسترا فقط برنامه ننویخت. او کمک کرد برنامه‌نویسی به کاری تبدیل شود که بتوان درباره‌اش جدی اندیشید، برایش معیار داشت و آن را بخشی از تفکر علمی و مهندسی دانست.</p>
<p>روزی برنامه‌نویسی آن‌قدر ناشناخته بود که در سند ازدواج جایی نداشت.</p>
<p>امروز جهان بدون برنامه‌نویسی، حتی مسیر خانه تا مقصد را هم به سختی پیدا می‌کند.</p>
<p>و شاید همین، بهترین تصویر از دایجسترا باشد:</p>
<p>مردی که در زمانی برنامه‌نویس شد که برنامه‌نویس بودن، هنوز شغل به حساب نمی‌آمد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>گاهی آینده، پیش از آنکه در فرم‌ها و عنوان‌های رسمی جا شود، در ذهن چند انسان کنجکاو آغاز می‌شود.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="منابع">منابع<a href="https://example.com/blog/dijkstra-programmer-was-not-a-job#%D9%85%D9%86%D8%A7%D8%A8%D8%B9" class="hash-link" aria-label="لینک مستقیم به منابع" title="لینک مستقیم به منابع" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.cs.utexas.edu/~EWD/transcriptions/EWD03xx/EWD340.html" target="_blank" rel="noopener noreferrer" class="">سخنرانی جایزه‌ی تورینگ دایجسترا: The Humble Programmer</a></li>
<li class=""><a href="https://www.cwi.nl/en/about/history/e-w-dijkstra-brilliant-colourful-and-opinionated/" target="_blank" rel="noopener noreferrer" class="">زندگی‌نامه‌ی دایجسترا در مرکز ریاضی و علوم رایانه‌ی هلند</a></li>
<li class=""><a href="https://www.cs.utexas.edu/~EWD/" target="_blank" rel="noopener noreferrer" class="">آرشیو نوشته‌های EWD در دانشگاه تگزاس</a></li>
</ul>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="تاریخ-رایانه" term="تاریخ-رایانه"/>
        <category label="برنامه‌نویسی" term="برنامه‌نویسی"/>
        <category label="دایجسترا" term="دایجسترا"/>
        <category label="الگوریتم" term="الگوریتم"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[قابلیت اداره‌پذیری سیستم‌ها چیست و چرا فقط با مانیتورینگ به دست نمی‌آید؟]]></title>
        <id>https://example.com/blog/system-operability-not-monitoring</id>
        <link href="https://example.com/blog/system-operability-not-monitoring"/>
        <updated>2026-04-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[روایتی تحلیلی درباره‌ی قابلیت اداره‌پذیری سامانه‌ها، تفاوت آن با مانیتورینگ و مشاهده‌پذیری، و نقش طراحی، داده و فرایندهای عملیاتی در اداره‌ی سامانه‌های واقعی.]]></summary>
        <content type="html"><![CDATA[<p>فرض کن نسخه‌ی تازه‌ی سرویس سفارش را مستقر کرده‌ایم. چند دقیقه‌ی اول، همه‌چیز عادی به نظر
می‌رسد: نمودارها وضعیت بدی نشان نمی‌دهند، نرخ خطا بالا نرفته، و از بیرون انگار سامانه دارد
کار می‌کند. کمی بعد، پشتیبانی خبر می‌دهد که چند پرداخت موفق بوده، اما سفارش در پنل کاربر
دیده نمی‌شود. لاگ‌ها را باز می‌کنیم، متریک‌ها را می‌بینیم، تریس‌ها را دنبال می‌کنیم؛ داده
کم نیست، اما جواب روشن پیدا نمی‌شود. معلوم نیست مشکل از پرداخت است، صف پیام، موجودی،
نسخه‌ی تازه، یا داده‌ای که در میانه‌ی مسیر نیمه‌کاره مانده است.</p>
<p>من معمولاً همین‌جا تفاوت میان «کار کردن» و «قابل اداره بودن» را می‌بینم. سامانه ممکن است
تا حدی زنده باشد، درخواست بگیرد و حتی بیشتر مسیرها را درست پاسخ بدهد، اما در لحظه‌ی تغییر
یا اختلال، برای تیم قابل فهم نباشد. قابلیت اداره‌پذیری، یا <code>operability</code>، درباره‌ی همین
لحظه‌هاست: لحظه‌هایی که باید بفهمیم چه رخ داده، آسیب را محدود کنیم، تصمیمی امن بگیریم،
و اگر لازم شد از مسیر اشتباه برگردیم.</p>
<p><img decoding="async" loading="lazy" alt="تصویر مفهومی درباره‌ی قابلیت اداره‌پذیری سامانه‌ها" src="https://example.com/assets/images/system-operability-39ea965b9b4e89692cc1ce9d0da694a5.png" width="1536" height="1024" class="img_m9Pm"></p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>حرف اصلی</div><div class="admonitionContent_hiHs"><p>مانیتورینگ می‌گوید چیزی از آستانه گذشته است. مشاهده‌پذیری کمک می‌کند بفهمیم چه رخ داده
است. قابلیت اداره‌پذیری باید کمک کند بفهمیم حالا چه کار امنی می‌توان انجام داد.</p></div></div>
<p>وقتی می‌گویم سامانه قابل اداره است، منظورم فقط این نیست که داشبورد دارد. حتی منظورم این
نیست که لاگ، متریک و تریس فراوان تولید می‌کند. منظورم سامانه‌ای است که در زمان ابهام،
تیم را با حدس، حافظه‌ی افراد و دستورهای دستی تنها نمی‌گذارد. خطاهایش معنا دارند،
وابستگی‌هایش قابل ردگیری‌اند، مسیر بازگشتش تمرین شده، و عملیات تکراری‌اش تا جای ممکن
قابل بازبینی و امن شده است.</p>
<p>برای همین من اداره‌پذیری را با مفاهیم نزدیک به آن یکی نمی‌گیرم:</p>
<table><thead><tr><th>مفهوم</th><th>پرسش اصلی</th><th>چرا کافی نیست؟</th></tr></thead><tbody><tr><td>پایایی، یا reliability</td><td>آیا سامانه معمولاً درست کار می‌کند؟</td><td>سامانه‌ی پایا هم ممکن است در زمان خرابی سخت بازیابی شود.</td></tr><tr><td>مشاهده‌پذیری، یا observability</td><td>آیا می‌توان رفتار سامانه را از بیرون فهمید؟</td><td>فهمیدن رخداد کافی نیست؛ باید مسیر اقدام هم داشته باشیم.</td></tr><tr><td>نگهداشت‌پذیری</td><td>آیا تغییر کد با هزینه‌ی معقول ممکن است؟</td><td>عملیات زنده فقط تغییر کد نیست؛ استقرار، rollback، داده و وابستگی هم هست.</td></tr><tr><td>استفاده‌پذیری</td><td>آیا کاربر با محصول راحت کار می‌کند؟</td><td>این مفهوم درباره‌ی کاربر محصول است، نه تیمی که سامانه را اداره می‌کند.</td></tr></tbody></table>
<p>ممکن است سرویس پرداخت پایا باشد و متریک‌های خوبی هم داشته باشد، اما اگر نتوانیم یک درگاه
معیوب را موقتاً غیرفعال کنیم، درگاه جایگزین را امن فعال کنیم، یا سفارش‌های نامشخص را
بازپردازش کنیم، هنوز با سامانه‌ای سخت‌اداره طرفیم. مشاهده‌پذیری به ما نشان می‌دهد کجا
را باید نگاه کنیم؛ اداره‌پذیری می‌پرسد بعد از دیدن، دستمان برای چه کاری باز است.</p>
<p>مشکل بسیاری از سامانه‌ها این نیست که هیچ نشانه‌ای تولید نمی‌کنند؛ مشکل این است که نشانه‌ها
به تصمیم وصل نمی‌شوند. هشداری که فقط می‌گوید «خطا زیاد شده است» من را جلو نمی‌برد. من
از هشدار خوب انتظار دارم بگوید چه چیزی آسیب دیده، دامنه‌ی اثر کجاست، اقدام نخست چیست، و
از کجا می‌فهمیم رخداد تمام شده است.</p>
<p>این نمونه‌ی کوچک، پیکربندی واقعی یک ابزار هشدار نیست. فقط می‌خواهم با آن تفاوت هشدار
مبهم و هشدار قابل اقدام را نشان بدهم:</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">noisy_alert </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"name"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"errors_are_high"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"condition"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"error_count &gt; 100"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"suggested_action"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"investigate"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">actionable_alert </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"name"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"checkout_payment_error_rate_is_high"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"condition"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"checkout_payment_error_rate &gt; 0.05 for 10m"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"context"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">"service"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"checkout"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">"dependency"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"payment_gateway"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">"region"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"tehran"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"user_impact"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"users may fail to complete payment"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"first_safe_action"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"enable_fallback_payment_provider"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"runbook"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"runbooks/checkout/payment_degradation.md"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"recovery_condition"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"checkout_payment_error_rate &lt; 0.01 for 20m"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در نمونه‌ی دوم، هشدار فقط پرجزئیات‌تر نیست؛ تصمیم‌پذیرتر است. کسی که آن را می‌بیند، بهتر
می‌فهمد مشکل به کدام سرویس و وابستگی مربوط است، اثر احتمالی روی کاربر چیست، اقدام نخست
کدام است، و چه زمانی می‌توان رخداد را پایان‌یافته دانست.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>زیاد کردن لاگ، متریک و تریس به‌تنهایی اداره‌پذیری نمی‌سازد. اگر این نشانه‌ها به فهم و
اقدام امن وصل نشوند، فقط نویز بیشتری تولید می‌کنند.</p></div></div>
<p>حالا دوباره به همان سفارش برگردیم. پرداخت موفق شده، اما سفارش در پنل نیست. اگر مسیر داده
روشن نباشد، باید حدس بزنیم: آیا پیام <code>order_created</code> دیر به صف رسیده؟ آیا موجودی هنوز
به‌روز نشده؟ آیا گزارش مالی پرداخت را دیده اما پنل پشتیبانی سفارش را نه؟ آیا نسخه‌ی تازه
قرارداد پیام را تغییر داده و مصرف‌کننده‌ی قدیمی هنوز با شکل جدید پیام سازگار نیست؟</p>
<p>در چنین رخدادی هیچ سرویس واحدی الزاماً «خاموش» نیست. ممکن است همه‌چیز از دید مانیتورینگ
زنده باشد، اما روایت کلی سامانه ناقص باشد. این همان جایی است که سامانه‌های داده‌محور
سخت‌تر اداره می‌شوند. داده ممکن است دیر برسد، دوبار پردازش شود، از ترتیب خارج شود، یا
در چند بخش سامانه چند روایت متفاوت بسازد. کد را شاید بتوان سریع rollback کرد، اما داده‌ی
نیمه‌کاره یا ناسازگار ممکن است ساعت‌ها اثر بگذارد.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>نکته‌ی داده‌ای</div><div class="admonitionContent_hiHs"><p>برای سامانه‌ی داده‌محور، زنده بودن سرویس کافی نیست. باید تازگی داده، تأخیر صف‌ها، امکان
بازپردازش، و مسیر حرکت داده هم قابل فهم باشد.</p></div></div>
<p>اینجاست که می‌شود فهمید اداره‌پذیری از طراحی شروع می‌شود، نه از داشبورد. اگر مرز سرویس‌ها
مبهم باشد، خرابی راحت از یک بخش به بخش دیگر سرایت می‌کند. اگر خطاها فقط <code>failed</code> یا
<code>internal error</code> باشند، نمی‌فهمیم خطا گذراست یا پایدار، تکرار درخواست امن است یا نه، و
کدام کاربر آسیب دیده است. اگر rollback فقط روی کاغذ وجود داشته باشد، در زمان بحران به کار
نمی‌آید؛ چون باید با داده، صف، کش و نسخه‌های هم‌زمان هم سازگار باشد.</p>
<p>حتی تصمیم‌هایی که ظاهراً کوچک‌اند، در عملیات اثر بزرگ دارند. مهلت انتظار نامناسب می‌تواند
زنجیره‌ی درخواست‌ها را قفل کند. تلاش دوباره‌ی بی‌مهار هنگام کندی، بار سامانه را بیشتر
می‌کند. نبود اجرای دوباره‌ی امن می‌تواند یک درخواست تکراری را به سفارش یا پرداخت تکراری
تبدیل کند. گاهی هم بهترین تصمیم این است که بخشی از قابلیت را موقتاً کم کنیم تا کل مسیر
کاربر خراب نشود؛ یعنی کاهش سطح سرویس باید از قبل طراحی شده باشد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>پرسش کاربردی</div><div class="admonitionContent_hiHs"><p>برای هر قابلیت تازه بپرسید: اگر این بخش کند شد، نصفه اجرا شد، دوبار اجرا شد، یا لازم شد
برگردد، تیم دقیقاً چه چیزی را می‌بیند و چه کاری می‌تواند امن انجام دهد؟</p></div></div>
<p>اگر بخواهم این حرف‌ها را به چند عادت عملی تبدیل کنم، از همین‌جا شروع می‌کنم: هر هشدار
باید مالک، اثر کاربری، اقدام نخست و معیار پایان داشته باشد. هر استقرار باید مسیر بازگشت
تمرین‌شده داشته باشد. هر وابستگی بیرونی باید رفتار مشخصی در کندی، قطعی و پاسخ ناسازگار
داشته باشد. هر مسیر داده باید داستان روشنی برای تازگی داده، بازپردازش و اصلاح داده‌ی خراب
داشته باشد. هر کار دستی تکراری هم باید یا حذف شود، یا به ابزار امن و قابل بازبینی تبدیل
شود.</p>
<p>این‌ها فقط وظیفه‌ی تیم عملیات نیست. تیم مهندسی باید خطا، rollback، تلاش دوباره و رفتار
وابستگی‌ها را در طراحی ببیند. تیم پلتفرم باید استقرار، مانیتورینگ، پیکربندی و بازیابی را
ساده و امن کند. تیم داده باید تازگی، کیفیت و مسیر داده را قابل پیگیری کند. معماری هم باید
مرزها و الگوهای خرابی را در سطح کل سامانه ببیند. اگر این تیم‌ها زبان مشترک نداشته باشند،
هرکدام از زاویه‌ی خودشان حرف درستی می‌زنند، اما سامانه همچنان سخت اداره می‌شود.</p>
<p>در پایان، اگر بخواهم همه‌ی بحث را به یک جمله برگردانم، می‌گویم سامانه‌ی قابل اداره الزاماً
بی‌خطا نیست؛ تفاوتش این است که وقتی خطا رخ می‌دهد، تیم می‌داند چه چیزی را ببیند، چه چیزی
را دست نزند، چه اقدامی امن‌تر است، و از کجا باید برگردد. مانیتورینگ و مشاهده‌پذیری برای
رسیدن به این نقطه لازم‌اند، اما کافی نیستند. سامانه‌ی خوب فقط سامانه‌ای نیست که در روزهای
عادی کار کند؛ سامانه‌ی خوب سامانه‌ای است که هنگام تغییر و خرابی هم قابل فهم و قابل اداره
بماند.</p>
<p>اگر قرار باشد بعد از خواندن این متن فقط چند چیز در ذهن بماند، یکی این است که اداره‌پذیری
یعنی توان فهم، تغییر، عیب‌یابی، مهار آسیب و بازیابی سامانه در محیط واقعی. دیگری این است
که هشدار خوب باید اثر، دامنه، اقدام نخست و معیار پایان داشته باشد. و شاید مهم‌تر از همه:
در سامانه‌های داده‌محور، مسیر داده به‌اندازه‌ی زنده بودن سرویس مهم است.</p>
<p>برای ادامه‌ی مطالعه، کتاب‌ها و نوشته‌هایی مثل <code>Designing Data-Intensive Applications</code>،
<code>Site Reliability Engineering</code>، <code>The Site Reliability Workbook</code>، <code>Release It!</code>،
<code>Team Guide to Software Operability</code> و <code>Team Topologies</code> نقطه‌ی شروع خوبی هستند.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="معماری سامانه" term="معماری سامانه"/>
        <category label="عملیات نرم‌افزار" term="عملیات نرم‌افزار"/>
        <category label="مشاهده‌پذیری" term="مشاهده‌پذیری"/>
        <category label="سامانه‌های داده‌محور" term="سامانه‌های داده‌محور"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[کد مهم‌تر نباید به کد کم‌اهمیت‌تر وابسته باشد]]></title>
        <id>https://example.com/blog/dependency-inversion-high-level-policy</id>
        <link href="https://example.com/blog/dependency-inversion-high-level-policy"/>
        <updated>2026-04-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[توضیحی درباره‌ی اینکه سیاست‌های سطح بالا نباید به جزئیات سطح پایین مانند دیتابیس، HTTP یا فریم‌ورک وابسته شوند.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="هسته‌ی روشن سیستم در مرکز که ابزارهایی مانند دیتابیس و وب در پیرامون آن قرار دارند و جهت وابستگی از ابزارها به سمت هسته است." src="https://example.com/assets/images/dependency-inversion-high-level-policy-95d3ce0247e0c45e9c01d633fb71a2de.png" width="1536" height="1024" class="img_m9Pm"></p>
<p>فرض کنید در یک سامانه‌ی مالی، مهم‌ترین قانون کسب‌وکار این است که فقط کاربرانی که موجودی کافی دارند بتوانند درخواست برداشت ثبت کنند. این قانون، از جنس سیاست اصلی سیستم است؛ یعنی همان چیزی که اگر روزی دیتابیس، فریم‌ورک، یا روش ارتباط با سرویس‌های بیرونی عوض شود، هنوز باید معتبر بماند.</p>
<p>اما در عمل، خیلی وقت‌ها این قانون را مستقیم به جزئیات فنی می‌دوزیم. چیزی شبیه این:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">PrismaClient</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@prisma/client'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> axios </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'axios'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> prisma </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">PrismaClient</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">WithdrawService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">withdraw</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> prisma</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findUnique</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      where</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> userId</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'User not found'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">balance </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> amount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'Insufficient balance'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> axios</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'https://wallet-service.example.com/withdraw'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> prisma</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">withdrawRequest</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      data</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        status</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'submitted'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این کد، منطق اصلی برداشت مستقیماً هم به ORM وابسته است، هم به یک کارخواه وب. یعنی مهم‌ترین بخش تصمیم‌گیری سیستم، به چیزهایی وصل شده که در اصل فقط ابزار اجرا هستند.</p>
<p>مسئله فقط این نیست که کد کمی شلوغ شده است. مسئله این است که جهت وابستگی برعکس چیزی است که طراحی خوب می‌خواهد. این‌جا سیاست سطح بالا به جزئیات سطح پایین آویزان شده است.</p>
<p>این همان جایی است که اصل وارونگی وابستگی وارد می‌شود.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>وقتی از «سیاست سطح بالا» حرف می‌زنیم، منظور بخشی از سیستم است که قانون‌ها و تصمیم‌های اصلی کسب‌وکار را بیان می‌کند؛ نه جزئیات فنی اجرا.</p><p>برای نمونه، این‌ها معمولاً سیاست سطح بالا هستند:</p><ul>
<li class="">شرط پذیرش یا رد یک عملیات</li>
<li class="">قواعد قیمت‌گذاری، کارمزد، اعتبارسنجی و مجوز</li>
<li class="">جریان اصلی یک مورد کاربرد</li>
<li class="">منطق اصلی‌ای که اگر ابزارها عوض شوند، هنوز باید پابرجا بماند</li>
</ul><p>در مقابل، چیزهایی مثل دیتابیس، HTTP، صف پیام، ORM، یا چارچوب وب، معمولاً جزئیات سطح پایین‌اند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مسئله-دقیقاً-چیست">مسئله دقیقاً چیست؟<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D9%85%D8%B3%D8%A6%D9%84%D9%87-%D8%AF%D9%82%DB%8C%D9%82%D8%A7%D9%8B-%DA%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به مسئله دقیقاً چیست؟" title="لینک مستقیم به مسئله دقیقاً چیست؟" translate="no">​</a></h2>
<p>در مثال بالا، <code>WithdrawService</code> قرار است یک تصمیم مهم بگیرد: آیا برداشت مجاز است یا نه؟ این همان بخش مهم و پایدار سیستم است. اما برای انجام این کار، مستقیماً به <code>PrismaClient</code> و <code>axios</code> چسبیده است.</p>
<p>این وابستگی چند دردسر می‌سازد:</p>
<ul>
<li class="">اگر روش دسترسی به داده عوض شود، منطق اصلی هم باید دست بخورد.</li>
<li class="">اگر سرویس بیرونی تغییر کند، باز هم همان کد مهم باید تغییر کند.</li>
<li class="">آزمون‌کردن این منطق سخت‌تر می‌شود، چون باید ابزارهای واقعی یا شبیه‌سازی پیچیده را وارد کنیم.</li>
<li class="">فهم مرز بین «قانون سیستم» و «روش اجرا» سخت‌تر می‌شود.</li>
</ul>
<p>یعنی سیاست اصلی، به‌جای اینکه روی پای خودش بایستد، روی شانه‌ی ابزارها ایستاده است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وارونگی-وابستگی-چه-میگوید">وارونگی وابستگی چه می‌گوید؟<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D9%88%D8%A7%D8%B1%D9%88%D9%86%DA%AF%DB%8C-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%DA%86%D9%87-%D9%85%DB%8C%DA%AF%D9%88%DB%8C%D8%AF" class="hash-link" aria-label="لینک مستقیم به وارونگی وابستگی چه می‌گوید؟" title="لینک مستقیم به وارونگی وابستگی چه می‌گوید؟" translate="no">​</a></h2>
<p>اصل وارونگی وابستگی (Dependency Inversion Principle) می‌گوید:</p>
<ul>
<li class="">ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند.</li>
<li class="">هر دو باید به انتزاع وابسته باشند.</li>
<li class="">انتزاع نباید به جزئیات وابسته باشد.</li>
<li class="">جزئیات باید به انتزاع وابسته باشند.</li>
</ul>
<p>این اصل در ظاهر کمی انتزاعی به نظر می‌رسد، اما معنایش بسیار عملی است: قانون اصلی سیستم نباید بداند دیتابیس دقیقاً چیست، یا درخواست HTTP دقیقاً چگونه ارسال می‌شود.</p>
<p>به‌جای این وابستگی مستقیم، باید یک مرز انتزاعی تعریف کنیم؛ چیزی که سیاست سطح بالا با آن کار کند، و ابزارهای واقعی پشت آن قرار بگیرند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="برگرداندن-جهت-وابستگی">برگرداندن جهت وابستگی<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D8%A8%D8%B1%DA%AF%D8%B1%D8%AF%D8%A7%D9%86%D8%AF%D9%86-%D8%AC%D9%87%D8%AA-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به برگرداندن جهت وابستگی" title="لینک مستقیم به برگرداندن جهت وابستگی" translate="no">​</a></h2>
<p>بیایید همان مثال برداشت را بازنویسی کنیم. نخست، به‌جای اینکه سرویس مستقیماً سراغ ORM و HTTP برود، دو درگاه تعریف می‌کنیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">UserBalanceReader</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">User </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">WithdrawRequestGateway</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">submit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">saveRequest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا سیاست اصلی فقط به این انتزاع‌ها وابسته می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">WithdrawService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> userBalanceReader</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> UserBalanceReader</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> withdrawRequestGateway</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> WithdrawRequestGateway</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">withdraw</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userBalanceReader</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'User not found'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">balance </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> amount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'Insufficient balance'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">withdrawRequestGateway</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">submit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">withdrawRequestGateway</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">saveRequest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این نسخه، منطق اصلی دیگر نمی‌داند Prisma چیست یا <code>axios</code> چه می‌کند. او فقط با قراردادهایی کار می‌کند که برای نیاز خودش تعریف شده‌اند.</p>
<p>حالا پیاده‌سازی‌های فنی به این قراردادها وابسته می‌شوند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">PrismaClient</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@prisma/client'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> axios </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'axios'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> prisma </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">PrismaClient</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">PrismaUserBalanceReader</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">UserBalanceReader</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> prisma</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findUnique</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      where</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> userId</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">WalletWithdrawGateway</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">WithdrawRequestGateway</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">submit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> axios</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'https://wallet-service.example.com/withdraw'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">saveRequest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> prisma</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">withdrawRequest</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      data</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        status</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'submitted'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا جهت وابستگی عوض شده است. قبلاً منطق اصلی به ابزارها وابسته بود. حالا ابزارها به قراردادی وابسته‌اند که منطق اصلی تعیین کرده است.</p>
<p>این همان وارونگی وابستگی است.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>یکی از خطاهای رایج این است که سیاست‌های اصلی سیستم را مستقیماً به ابزارها گره بزنیم؛ مثلاً:</p><ul>
<li class="">سرویس کسب‌وکار مستقیم ORM را صدا بزند</li>
<li class="">منطق اصلی مستقیم درخواست HTTP بفرستد</li>
<li class="">قانون‌های کسب‌وکار مستقیم به چارچوب وب یا صف پیام وابسته باشند</li>
</ul><p>در این حالت، هر تغییر در ابزارها به‌راحتی به قلب سیستم نفوذ می‌کند. نتیجه معمولاً این است که مهم‌ترین کد شما، ناپایدارتر از چیزی می‌شود که باید صرفاً یک جزئیات اجرایی باشد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-این-اصل-مهم-است">چرا این اصل مهم است؟<a href="https://example.com/blog/dependency-inversion-high-level-policy#%DA%86%D8%B1%D8%A7-%D8%A7%DB%8C%D9%86-%D8%A7%D8%B5%D9%84-%D9%85%D9%87%D9%85-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به چرا این اصل مهم است؟" title="لینک مستقیم به چرا این اصل مهم است؟" translate="no">​</a></h2>
<p>شاید در پروژه‌ی کوچک، این‌همه مرزبندی اضافه به نظر برسد. اما وقتی سیستم رشد می‌کند، اهمیت این اصل روشن می‌شود.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۱-تغییر-ابزار-قانون-اصلی-را-تکان-نمیدهد">۱. تغییر ابزار، قانون اصلی را تکان نمی‌دهد<a href="https://example.com/blog/dependency-inversion-high-level-policy#%DB%B1-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1-%D8%A7%D8%A8%D8%B2%D8%A7%D8%B1-%D9%82%D8%A7%D9%86%D9%88%D9%86-%D8%A7%D8%B5%D9%84%DB%8C-%D8%B1%D8%A7-%D8%AA%DA%A9%D8%A7%D9%86-%D9%86%D9%85%DB%8C%D8%AF%D9%87%D8%AF" class="hash-link" aria-label="لینک مستقیم به ۱. تغییر ابزار، قانون اصلی را تکان نمی‌دهد" title="لینک مستقیم به ۱. تغییر ابزار، قانون اصلی را تکان نمی‌دهد" translate="no">​</a></h3>
<p>اگر امروز از Prisma استفاده می‌کنید و فردا سراغ ابزار دیگری می‌روید، یا اگر امروز با HTTP به یک سرویس بیرونی وصل می‌شوید و فردا از صف پیام استفاده می‌کنید، نباید لازم باشد قانون برداشت را بازنویسی کنید.</p>
<p>سیاست اصلی باید تا حد ممکن از این تغییرها مصون بماند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۲-آزمونپذیری-بهتر-میشود">۲. آزمون‌پذیری بهتر می‌شود<a href="https://example.com/blog/dependency-inversion-high-level-policy#%DB%B2-%D8%A2%D8%B2%D9%85%D9%88%D9%86%D9%BE%D8%B0%DB%8C%D8%B1%DB%8C-%D8%A8%D9%87%D8%AA%D8%B1-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به ۲. آزمون‌پذیری بهتر می‌شود" title="لینک مستقیم به ۲. آزمون‌پذیری بهتر می‌شود" translate="no">​</a></h3>
<p>وقتی سرویس اصلی به انتزاع وابسته باشد، آزمون‌کردن آن خیلی ساده‌تر می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">FakeUserBalanceReader</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">UserBalanceReader</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> balance</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_000_000</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">FakeWithdrawRequestGateway</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">WithdrawRequestGateway</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  submitted</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Array</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">}</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">submit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">submitted</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">saveRequest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و آزمون هم ساده می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'submits withdraw request when balance is sufficient'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> userReader </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">FakeUserBalanceReader</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> gateway </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">FakeWithdrawRequestGateway</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> service </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">WithdrawService</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userReader</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> gateway</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> service</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">withdraw</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'user-1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">gateway</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">submitted</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toEqual</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'user-1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>در این آزمون، نه ORM واقعی در کار است، نه HTTP واقعی، نه وابستگی به محیط بیرونی. ما فقط قانون اصلی را می‌سنجیم.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۳-مرز-معماری-روشنتر-میشود">۳. مرز معماری روشن‌تر می‌شود<a href="https://example.com/blog/dependency-inversion-high-level-policy#%DB%B3-%D9%85%D8%B1%D8%B2-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D8%B1%D9%88%D8%B4%D9%86%D8%AA%D8%B1-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به ۳. مرز معماری روشن‌تر می‌شود" title="لینک مستقیم به ۳. مرز معماری روشن‌تر می‌شود" translate="no">​</a></h3>
<p>وقتی جهت وابستگی درست باشد، خیلی بهتر می‌فهمیم کدام بخش از سیستم «هسته» است و کدام بخش «ابزار». این تمایز فقط سلیقه‌ی معماری نیست؛ کمک می‌کند تصمیم‌های فنی را در جای درست بگیریم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="انتزاع-را-چه-کسی-باید-تعریف-کند">انتزاع را چه کسی باید تعریف کند؟<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D8%A7%D9%86%D8%AA%D8%B2%D8%A7%D8%B9-%D8%B1%D8%A7-%DA%86%D9%87-%DA%A9%D8%B3%DB%8C-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%AA%D8%B9%D8%B1%DB%8C%D9%81-%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به انتزاع را چه کسی باید تعریف کند؟" title="لینک مستقیم به انتزاع را چه کسی باید تعریف کند؟" translate="no">​</a></h2>
<p>نکته‌ی مهم این است که انتزاع معمولاً باید نزدیک به نیاز سیاست سطح بالا تعریف شود، نه نزدیک به ابزار.</p>
<p>مثلاً اگر سرویس برداشت فقط نیاز دارد کاربر را پیدا کند و درخواست برداشت ثبت کند، انتزاع هم باید همین نیاز را بازتاب دهد. نه اینکه از همان ابتدا درگاه‌هایی بسازیم که بوی دیتابیس یا HTTP بدهند.</p>
<p>برای نمونه، این قرارداد خوب نیست:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">PrismaCompatibleRepository</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findUnique</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">query</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">unknown</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token builtin">unknown</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">data</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">unknown</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token builtin">unknown</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>چون این دیگر انتزاع واقعی نیست؛ فقط نام دیتابیس را پنهان کرده‌ایم. سیاست سطح بالا هنوز عملاً به منطق ابزار وابسته است.</p>
<p>در عوض، این بهتر است:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">UserBalanceReader</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">User </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>چون بر پایه‌ی نیاز کسب‌وکار تعریف شده، نه بر پایه‌ی شکل ابزار.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="این-اصل-با-معماری-تمیز-چه-نسبتی-دارد">این اصل با معماری تمیز چه نسبتی دارد؟<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D8%A7%DB%8C%D9%86-%D8%A7%D8%B5%D9%84-%D8%A8%D8%A7-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D8%AA%D9%85%DB%8C%D8%B2-%DA%86%D9%87-%D9%86%D8%B3%D8%A8%D8%AA%DB%8C-%D8%AF%D8%A7%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به این اصل با معماری تمیز چه نسبتی دارد؟" title="لینک مستقیم به این اصل با معماری تمیز چه نسبتی دارد؟" translate="no">​</a></h2>
<p>اصل وارونگی وابستگی یکی از ستون‌های اصلی معماری تمیز (Clean Architecture) است. در معماری تمیز، لایه‌های درونی که منطق اصلی سیستم را در خود دارند، نباید به لایه‌های بیرونی وابسته باشند. لایه‌های بیرونی باید خودشان را با نیاز هسته سازگار کنند.</p>
<p>یعنی اگر هسته‌ی سیستم بگوید «من برای خواندن موجودی، چنین قراردادی می‌خواهم»، این دیتابیس و وب و چارچوب‌اند که باید خودشان را با این قرارداد وفق دهند، نه برعکس.</p>
<p>به همین دلیل است که در طراحی تمیز، خیلی وقت‌ها درگاه‌ها یا واسط‌ها در بخش درونی‌تر تعریف می‌شوند، اما پیاده‌سازی آن‌ها در لایه‌های بیرونی قرار می‌گیرد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آیا-همیشه-باید-برای-همهچیز-انتزاع-بسازیم">آیا همیشه باید برای همه‌چیز انتزاع بسازیم؟<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D8%A2%DB%8C%D8%A7-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A8%D8%B1%D8%A7%DB%8C-%D9%87%D9%85%D9%87%DA%86%DB%8C%D8%B2-%D8%A7%D9%86%D8%AA%D8%B2%D8%A7%D8%B9-%D8%A8%D8%B3%D8%A7%D8%B2%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به آیا همیشه باید برای همه‌چیز انتزاع بسازیم؟" title="لینک مستقیم به آیا همیشه باید برای همه‌چیز انتزاع بسازیم؟" translate="no">​</a></h2>
<p>نه. این هم یکی از جاهایی است که می‌شود افراط کرد.</p>
<p>اگر برای هر چیز کوچک، بی‌دلیل چندین لایه‌ی انتزاعی بسازیم، طراحی ممکن است بی‌جهت سنگین شود. اصل وارونگی وابستگی قرار نیست ما را وادار کند که برای هر تابع ساده یک اینترفیس جدا تعریف کنیم.</p>
<p>این اصل زمانی بیشترین ارزش را دارد که:</p>
<ul>
<li class="">با یک سیاست مهم و پایدار طرف هستیم</li>
<li class="">ابزارهای بیرونی امکان تغییر دارند</li>
<li class="">آزمون‌پذیری مهم است</li>
<li class="">وابستگی مستقیم به ابزار، هزینه‌ی تغییر بالایی ایجاد می‌کند</li>
</ul>
<p>پس هدف، انتزاع‌سازی افراطی نیست؛ هدف، محافظت از هسته‌ی مهم سیستم در برابر جزئیات ناپایدار است.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>برای طراحی درگاه، از خودت بپرس: «این بخش مهم سیستم، برای انجام کارش دقیقاً به چه نیازی دارد؟»</p><p>درگاه خوب باید:</p><ul>
<li class="">زبان نیاز کسب‌وکار را بازتاب دهد</li>
<li class="">کمینه و متمرکز باشد</li>
<li class="">بوی ابزار خاص ندهد</li>
<li class="">مصرف‌کننده را به جزئیات اجرایی آلوده نکند</li>
</ul><p>اگر قراردادی که تعریف کرده‌ای بیشتر شبیه API دیتابیس یا چارچوب است تا نیاز واقعی مورد کاربرد، احتمالاً هنوز درگاه را از زاویه‌ی درست طراحی نکرده‌ای.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-نمونه-از-ساختار-پوشه">یک نمونه از ساختار پوشه<a href="https://example.com/blog/dependency-inversion-high-level-policy#%DB%8C%DA%A9-%D9%86%D9%85%D9%88%D9%86%D9%87-%D8%A7%D8%B2-%D8%B3%D8%A7%D8%AE%D8%AA%D8%A7%D8%B1-%D9%BE%D9%88%D8%B4%D9%87" class="hash-link" aria-label="لینک مستقیم به یک نمونه از ساختار پوشه" title="لینک مستقیم به یک نمونه از ساختار پوشه" translate="no">​</a></h2>
<p>اگر این مرزبندی را در ساختار پروژه نشان دهیم، شاید چیزی شبیه این داشته باشیم:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">withdraw/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  application/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    withdraw-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    user-balance-reader.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    withdraw-request-gateway.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  infrastructure/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    prisma-user-balance-reader.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    wallet-withdraw-gateway.ts</span><br></span></code></pre></div></div>
<p>در این ساختار:</p>
<ul>
<li class=""><code>application</code> محل منطق اصلی و قراردادهای موردنیاز آن است.</li>
<li class=""><code>infrastructure</code> محل پیاده‌سازی‌های وابسته به ابزارهاست.</li>
</ul>
<p>این یعنی هسته‌ی سیستم می‌گوید «من چه می‌خواهم»، و زیرساخت می‌گوید «من آن را چگونه اجرا می‌کنم».</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>اگر می‌خواهی اصل وارونگی وابستگی را در یک کد موجود بررسی کنی، از جاهایی شروع کن که منطق مهم سیستم مستقیم با ابزارها حرف می‌زند.</p>
<p>چیزهایی که باید بررسی شوند:</p>
<ul>
<li class="">آیا یک سرویس کسب‌وکار مستقیم ORM، HTTP client، صف پیام یا چارچوب وب را صدا می‌زند؟</li>
<li class="">آیا تغییر ابزارهای زیرساختی، فایل‌های مربوط به قانون‌های اصلی را هم مجبور به تغییر می‌کند؟</li>
<li class="">آیا آزمون منطق اصلی بدون راه‌اندازی ابزارهای بیرونی سخت است؟</li>
<li class="">آیا می‌توان نیاز واقعی سیاست سطح بالا را با یک درگاه کوچک و روشن بیان کرد؟</li>
<li class="">آیا انتزاع‌های فعلی واقعاً زبان کسب‌وکار دارند یا فقط شکل ابزار را پنهان کرده‌اند؟</li>
</ul>
<p>چیزهایی که نباید بی‌دلیل تغییر کنند:</p>
<ul>
<li class="">کدهای ساده‌ای که هنوز با ابزار بیرونی درگیر نیستند</li>
<li class="">جاهایی که ساخت انتزاع فقط پیچیدگی تزئینی می‌سازد</li>
<li class="">قراردادهای خوب و کوچکی که همین حالا هم بر پایه‌ی نیاز واقعی تعریف شده‌اند</li>
<li class="">منطق اصلی‌ای که از قبل از جزئیات جدا شده و مرز درستی دارد</li>
</ul>
<p>برای اعتبارسنجی تغییر، دست‌کم این گام‌ها را اجرا کنید:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">npm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run lint</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run typecheck</span><br></span></code></pre></div></div>
<p>اگر پروژه‌ی شما این فرمان‌ها را ندارد، معادل آن‌ها را اجرا کنید. بهتر است علاوه بر آزمون‌های کلی، برای خود سیاست سطح بالا هم آزمون‌هایی داشته باشید که بدون ORM، بدون HTTP واقعی، و بدون وابستگی به چارچوب اجرا شوند. اگر چنین آزمون‌هایی ساده شده‌اند، احتمال زیادی دارد که جهت وابستگی را درست برگردانده باشید.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/dependency-inversion-high-level-policy#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>اصل وارونگی وابستگی می‌گوید کد مهم‌تر نباید به کد کم‌اهمیت‌تر وابسته باشد. این حرف در ظاهر ساده است، اما اثر بزرگی روی طراحی دارد. چون بسیاری از دردهای نگه‌داری، از همین‌جا آغاز می‌شوند که منطق اصلی سیستم را به ابزارهایی می‌دوزیم که باید صرفاً نقش اجرایی داشته باشند.</p>
<p>دیتابیس، HTTP، صف پیام، ORM و فریم‌ورک مهم‌اند، اما نه به‌اندازه‌ی قانون اصلی سیستم. این‌ها باید در خدمت هسته باشند، نه اینکه هسته را کنترل کنند.</p>
<p>طراحی خوب، جهت وابستگی را طوری می‌چیند که سیاست‌های اصلی پابرجا بمانند و جزئیات دور آن‌ها بچرخند. اصل وارونگی وابستگی دقیقاً همین را به ما یادآوری می‌کند: هسته باید تعیین کند چه می‌خواهد؛ ابزارها باید خودشان را با هسته سازگار کنند.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="SOLID" term="SOLID"/>
        <category label="وارونگی وابستگی" term="وارونگی وابستگی"/>
        <category label="معماری تمیز" term="معماری تمیز"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[اینترفیس بزرگ، کلاس کوچک را هم آلوده می‌کند]]></title>
        <id>https://example.com/blog/interface-segregation-fat-interfaces</id>
        <link href="https://example.com/blog/interface-segregation-fat-interfaces"/>
        <updated>2026-04-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[بررسی اینکه چرا مصرف‌کننده نباید به متدهایی وابسته شود که به آن‌ها نیازی ندارد.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="یک پریز بزرگ با کابل‌های زیاد که یک دستگاه کوچک فقط به یکی از آن‌ها نیاز دارد." src="https://example.com/assets/images/interface-segregation-fat-interfaces-758689f0613c1c629e8a965277a20ac9.png" width="1536" height="1024" class="img_m9Pm"></p>
<p>فرض کنید در یک سامانه‌ی مالی، یک ریپازیتوری بزرگ برای کار با تراکنش‌ها داریم. این ریپازیتوری همه‌چیز را با هم دارد: گرفتن تراکنش، ساخت گزارش، بستن حساب، بازسازی داده، خروجی گرفتن، و چند کار دیگر. روی کاغذ، شاید این طراحی «مرکزی» و «یک‌پارچه» به نظر برسد. اما کافی است یک مصرف‌کننده‌ی کوچک وارد ماجرا شود تا مشکل خودش را نشان دهد.</p>
<p>برای نمونه، یک سرویس داریم که فقط می‌خواهد یک تراکنش را بر اساس شناسه پیدا کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">TransactionRepository</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Transaction </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">exportDailyReport</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">date</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">ReportFile</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">rebuildBalances</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">closeMonth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">month</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">archiveOldTransactions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">before</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token builtin">number</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و مصرف‌کننده‌ی ما فقط همین را لازم دارد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">TransactionDetailsService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> repository</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionRepository</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getDetails</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">repository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در نگاه نخست، شاید کسی بگوید: «خب چه اشکالی دارد؟ این سرویس که فقط از <code>findById</code> استفاده می‌کند.» اما مسئله دقیقاً همین‌جاست. این سرویس، حتی اگر فقط به یک متد نیاز داشته باشد، به اینترفیس بزرگی وابسته شده که پر از چیزهای نامربوط است. یعنی یک کلاس کوچک، ناخواسته بار یک قرارداد بزرگ را روی دوش می‌کشد.</p>
<p>این‌جا پای یکی از مهم‌ترین اصل‌های طراحی به میان می‌آید: اصل جداسازی اینترفیس.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>اطلاع</div><div class="admonitionContent_hiHs"><p>اصل جداسازی اینترفیس (Interface Segregation Principle) می‌گوید مصرف‌کننده نباید به متدهایی وابسته شود که به آن‌ها نیازی ندارد.</p><p>به بیان ساده‌تر، به‌جای یک اینترفیس بزرگ و همه‌فن‌حریف، بهتر است چند اینترفیس کوچک‌تر و دقیق‌تر داشته باشیم؛ طوری که هر مصرف‌کننده فقط به همان چیزی وابسته باشد که واقعاً از آن استفاده می‌کند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مشکل-فقط-زیادیبودن-متدها-نیست">مشکل فقط زیادی‌بودن متدها نیست<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D9%85%D8%B4%DA%A9%D9%84-%D9%81%D9%82%D8%B7-%D8%B2%DB%8C%D8%A7%D8%AF%DB%8C%D8%A8%D9%88%D8%AF%D9%86-%D9%85%D8%AA%D8%AF%D9%87%D8%A7-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به مشکل فقط زیادی‌بودن متدها نیست" title="لینک مستقیم به مشکل فقط زیادی‌بودن متدها نیست" translate="no">​</a></h2>
<p>وقتی از اینترفیس بزرگ حرف می‌زنیم، مسئله فقط این نیست که تعداد متدها زیاد است. مشکل اصلی این است که وابستگی کاذب می‌سازیم. یعنی کلاسی که فقط به یک قابلیت نیاز دارد، در ظاهر به مجموعه‌ای از قابلیت‌های دیگر هم وابسته می‌شود؛ قابلیت‌هایی که نه آن‌ها را صدا می‌زند، نه باید درباره‌شان چیزی بداند.</p>
<p>این وابستگی پنهان معمولاً سه پیامد مهم دارد:</p>
<ol>
<li class="">تغییر پرهزینه‌تر می‌شود.</li>
<li class="">آزمون‌نویسی سخت‌تر می‌شود.</li>
<li class="">مرز مسئولیت‌ها مبهم‌تر می‌شود.</li>
</ol>
<p>بیایید هرکدام را روشن‌تر ببینیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وابستگی-پنهان-چگونه-ساخته-میشود">وابستگی پنهان چگونه ساخته می‌شود؟<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%D9%BE%D9%86%D9%87%D8%A7%D9%86-%DA%86%DA%AF%D9%88%D9%86%D9%87-%D8%B3%D8%A7%D8%AE%D8%AA%D9%87-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به وابستگی پنهان چگونه ساخته می‌شود؟" title="لینک مستقیم به وابستگی پنهان چگونه ساخته می‌شود؟" translate="no">​</a></h2>
<p>در مثال بالا، <code>TransactionDetailsService</code> فقط به <code>findById</code> نیاز دارد. اما چون این متد در دل یک اینترفیس بزرگ قرار گرفته، هر تغییری در تعریف آن اینترفیس می‌تواند روی این سرویس هم اثر بگذارد، حتی اگر آن تغییر هیچ ربط مستقیمی به نیاز این سرویس نداشته باشد.</p>
<p>برای نمونه، فرض کنید تیم دیگری تصمیم بگیرد <code>exportDailyReport</code> باید یک پارامتر تازه بگیرد، یا <code>closeMonth</code> قرارداد متفاوتی پیدا کند. اگر در پروژه‌ی شما پیاده‌سازی‌های آزمایشی، ماک‌ها، یا کلاس‌های ساختگی بر اساس این اینترفیس ساخته شده باشند، ناگهان مصرف‌کننده‌ای که فقط <code>findById</code> می‌خواست هم درگیر می‌شود.</p>
<p>یعنی تغییر در بخشی که برای او بی‌ربط است، او را هم تکان می‌دهد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>اینترفیس چاق، فقط یک قرارداد بزرگ نیست؛ یک سطح پخش وابستگی است.</p><p>وقتی چند نیاز نامرتبط را در یک اینترفیس جمع می‌کنیم، مصرف‌کننده‌ها به‌شکل مصنوعی به هم گره می‌خورند. نتیجه این می‌شود که تغییر در یک متد، روی کدهایی اثر می‌گذارد که اساساً از آن متد استفاده‌ای نمی‌کنند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آزمونها-چرا-سختتر-میشوند">آزمون‌ها چرا سخت‌تر می‌شوند؟<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D8%A2%D8%B2%D9%85%D9%88%D9%86%D9%87%D8%A7-%DA%86%D8%B1%D8%A7-%D8%B3%D8%AE%D8%AA%D8%AA%D8%B1-%D9%85%DB%8C%D8%B4%D9%88%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به آزمون‌ها چرا سخت‌تر می‌شوند؟" title="لینک مستقیم به آزمون‌ها چرا سخت‌تر می‌شوند؟" translate="no">​</a></h2>
<p>یکی از جاهایی که این مشکل خیلی زود خودش را نشان می‌دهد، آزمون‌ها هستند.</p>
<p>اگر بخواهیم <code>TransactionDetailsService</code> را آزمون کنیم، منطقی است که فقط لازم باشد رفتار <code>findById</code> را کنترل کنیم. اما اگر از یک اینترفیس بزرگ استفاده می‌کنیم، ساختن یک پیاده‌سازی آزمایشی از آن می‌تواند آزاردهنده شود.</p>
<p>مثلاً ممکن است در آزمون مجبور شویم چنین چیزی بسازیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">FakeTransactionRepository</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">TransactionRepository</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not implemented'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not implemented'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">exportDailyReport</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">date</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not implemented'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">rebuildBalances</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not implemented'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">closeMonth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">month</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not implemented'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">archiveOldTransactions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">before</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not implemented'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد بوی خوبی نمی‌دهد. چرا برای آزمون سرویسی که فقط یک داده را می‌خواند، باید شش متد بی‌ربط دیگر را هم پر کنیم؟ حتی اگر با <code>throw new Error</code> پر شوند، باز هم نشان می‌دهد که قرارداد ما برای این مصرف‌کننده بیش از حد بزرگ است.</p>
<p>مشکل فقط زشتی کد آزمایشی نیست. این وضعیت به ما می‌گوید طراحی اصلی هم مرز روشنی ندارد. آزمون‌ها معمولاً خیلی زودتر از کد تولیدی، اشکال در مرزبندی را لو می‌دهند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="تغییر-پرهزینهتر-یعنی-چه">تغییر پرهزینه‌تر یعنی چه؟<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1-%D9%BE%D8%B1%D9%87%D8%B2%DB%8C%D9%86%D9%87%D8%AA%D8%B1-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به تغییر پرهزینه‌تر یعنی چه؟" title="لینک مستقیم به تغییر پرهزینه‌تر یعنی چه؟" translate="no">​</a></h2>
<p>هزینه‌ی تغییر فقط به اندازه‌ی خط‌های جدیدی که می‌نویسیم نیست. بخشی از هزینه، از جاهایی می‌آید که ناخواسته درگیر می‌شوند.</p>
<p>اگر اینترفیس بزرگ باشد، هر بار که یکی از متدهای آن تغییر می‌کند، باید این چیزها را دوباره بررسی کنیم:</p>
<ul>
<li class="">همه‌ی پیاده‌سازی‌های اینترفیس</li>
<li class="">همه‌ی ماک‌ها و فیک‌ها</li>
<li class="">همه‌ی مصرف‌کننده‌هایی که از روی نام اینترفیس به آن وابسته‌اند</li>
<li class="">همه‌ی جاهایی که به‌ظاهر از اینترفیس استفاده می‌کنند، حتی اگر فقط یک متد را لازم داشته باشند</li>
</ul>
<p>یعنی هزینه‌ی تغییر، از مرز نیاز واقعی فراتر می‌رود. این همان چیزی است که طراحی خوب باید از آن جلوگیری کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-مثال-واقعیتر-وابستگی-مدیریتی-در-یک-سرویس-کوچک">یک مثال واقعی‌تر: وابستگی مدیریتی در یک سرویس کوچک<a href="https://example.com/blog/interface-segregation-fat-interfaces#%DB%8C%DA%A9-%D9%85%D8%AB%D8%A7%D9%84-%D9%88%D8%A7%D9%82%D8%B9%DB%8C%D8%AA%D8%B1-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%D9%85%D8%AF%DB%8C%D8%B1%DB%8C%D8%AA%DB%8C-%D8%AF%D8%B1-%DB%8C%DA%A9-%D8%B3%D8%B1%D9%88%DB%8C%D8%B3-%DA%A9%D9%88%DA%86%DA%A9" class="hash-link" aria-label="لینک مستقیم به یک مثال واقعی‌تر: وابستگی مدیریتی در یک سرویس کوچک" title="لینک مستقیم به یک مثال واقعی‌تر: وابستگی مدیریتی در یک سرویس کوچک" translate="no">​</a></h2>
<p>فرض کنید یک کنترلر وب فقط می‌خواهد وضعیت یک کاربر را نشان دهد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">UserService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">getUserProfile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">UserProfile</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">updateUserProfile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> data</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> UpdateProfileInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">resetPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">suspendUser</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">exportUsersCsv</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token builtin">string</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">rebuildSearchIndex</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>کنترلر فقط از این استفاده می‌کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">UserProfileController</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> userService</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> UserService</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">show</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getUserProfile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اما حالا این کنترلر، از نظر قراردادی، به بازنشانی گذرواژه، تعلیق کاربر، خروجی گرفتن، و بازسازی نمایه‌ی جست‌وجو هم وابسته شده است. این وابستگی شاید در کد روزمره دیده نشود، اما در معماری، در آزمون، و در هزینه‌ی تغییر کاملاً واقعی است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="راه-بهتر-اینترفیس-را-از-نگاه-مصرفکننده-ببینیم">راه بهتر: اینترفیس را از نگاه مصرف‌کننده ببینیم<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D8%B1%D8%A7%D9%87-%D8%A8%D9%87%D8%AA%D8%B1-%D8%A7%DB%8C%D9%86%D8%AA%D8%B1%D9%81%DB%8C%D8%B3-%D8%B1%D8%A7-%D8%A7%D8%B2-%D9%86%DA%AF%D8%A7%D9%87-%D9%85%D8%B5%D8%B1%D9%81%DA%A9%D9%86%D9%86%D8%AF%D9%87-%D8%A8%D8%A8%DB%8C%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به راه بهتر: اینترفیس را از نگاه مصرف‌کننده ببینیم" title="لینک مستقیم به راه بهتر: اینترفیس را از نگاه مصرف‌کننده ببینیم" translate="no">​</a></h2>
<p>راه‌حل معمولاً این نیست که صرفاً اینترفیس را «تمیزتر» کنیم یا متدها را گروه‌بندی ظاهری بدهیم. راه‌حل اصلی این است که اینترفیس را از زاویه‌ی مصرف‌کننده ببینیم.</p>
<p>در مثال نخست، اگر <code>TransactionDetailsService</code> فقط به خواندن تراکنش نیاز دارد، شاید این قرارداد برای او کافی باشد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">TransactionReader</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Transaction </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و اگر بخشی دیگر مسئول ذخیره‌سازی است، قرارداد خودش را داشته باشد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">TransactionWriter</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>قابلیت‌های مدیریتی یا پردازش‌های سنگین هم می‌توانند اینترفیس‌های جداگانه‌ی خودشان را داشته باشند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">TransactionReportExporter</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">exportDailyReport</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">date</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">ReportFile</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">TransactionMaintenanceService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">rebuildBalances</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">closeMonth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">month</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">archiveOldTransactions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">before</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token builtin">number</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا سرویس جزئیات فقط به چیزی وابسته می‌شود که واقعاً لازم دارد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">TransactionDetailsService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> reader</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionReader</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getDetails</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">reader</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این طراحی ساده‌تر است، اما مهم‌تر از آن، صادقانه‌تر است. چون وابستگی را همان‌طور که هست نشان می‌دهد؛ نه بیشتر.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="این-اصل-با-اصل-مسئولیت-یکتا-همخانواده-است">این اصل با اصل مسئولیت یکتا هم‌خانواده است<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D8%A7%DB%8C%D9%86-%D8%A7%D8%B5%D9%84-%D8%A8%D8%A7-%D8%A7%D8%B5%D9%84-%D9%85%D8%B3%D8%A6%D9%88%D9%84%DB%8C%D8%AA-%DB%8C%DA%A9%D8%AA%D8%A7-%D9%87%D9%85%D8%AE%D8%A7%D9%86%D9%88%D8%A7%D8%AF%D9%87-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به این اصل با اصل مسئولیت یکتا هم‌خانواده است" title="لینک مستقیم به این اصل با اصل مسئولیت یکتا هم‌خانواده است" translate="no">​</a></h2>
<p>اصل جداسازی اینترفیس از نظر روح کلی، به اصل مسئولیت یکتا نزدیک است. آن‌جا می‌گفتیم یک واحد نرم‌افزاری نباید چند محور تغییر نامرتبط را با هم حمل کند. این‌جا می‌گوییم یک مصرف‌کننده نباید بار قراردادهایی را بکشد که به آن‌ها نیاز ندارد.</p>
<p>هر دو اصل، در نهایت دارند از یک چیز محافظت می‌کنند: مرز روشن.</p>
<p>وقتی مرزها روشن باشند:</p>
<ul>
<li class="">مصرف‌کننده فقط همان چیزی را می‌بیند که باید ببیند،</li>
<li class="">تغییرها محدودتر می‌شوند،</li>
<li class="">آزمون‌ها ساده‌تر می‌شوند،</li>
<li class="">و طراحی، کمتر به‌سمت وابستگی‌های پنهان می‌رود.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آیا-همیشه-باید-اینترفیس-را-خرد-کنیم">آیا همیشه باید اینترفیس را خرد کنیم؟<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D8%A2%DB%8C%D8%A7-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A7%DB%8C%D9%86%D8%AA%D8%B1%D9%81%DB%8C%D8%B3-%D8%B1%D8%A7-%D8%AE%D8%B1%D8%AF-%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به آیا همیشه باید اینترفیس را خرد کنیم؟" title="لینک مستقیم به آیا همیشه باید اینترفیس را خرد کنیم؟" translate="no">​</a></h2>
<p>نه. این هم یکی از جاهایی است که اگر بی‌دقت باشیم، از آن‌طرف بام می‌افتیم.</p>
<p>گاهی یک اینترفیس چند متد دارد، اما همه‌ی متدها واقعاً به هم نزدیک‌اند و تقریباً همه‌ی مصرف‌کننده‌ها به بیشتر آن‌ها نیاز دارند. در این حالت، شکستن اینترفیس به چند بخش مصنوعی فقط طراحی را پراکنده‌تر می‌کند.</p>
<p>پس معیار، صرفاً تعداد متدها نیست. معیار این است که آیا مصرف‌کننده‌های متفاوت، زیرمجموعه‌های متفاوتی از متدها را لازم دارند یا نه.</p>
<p>اگر پاسخ مثبت است، احتمال خوبی دارد که اینترفیس بیش از حد پهن شده باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-نشانهی-مهم-متدهای-not-implemented">یک نشانه‌ی مهم: متدهای <code>not implemented</code><a href="https://example.com/blog/interface-segregation-fat-interfaces#%DB%8C%DA%A9-%D9%86%D8%B4%D8%A7%D9%86%D9%87%DB%8C-%D9%85%D9%87%D9%85-%D9%85%D8%AA%D8%AF%D9%87%D8%A7%DB%8C-not-implemented" class="hash-link" aria-label="لینک مستقیم به یک-نشانهی-مهم-متدهای-not-implemented" title="لینک مستقیم به یک-نشانهی-مهم-متدهای-not-implemented" translate="no">​</a></h2>
<p>یکی از نشانه‌های بسیار مهم نقض این اصل، این است که پیاده‌سازی‌ها یا کلاس‌های آزمایشی ناچار می‌شوند بعضی متدها را با چیزی شبیه این پر کنند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not implemented'</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>یا:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'not supported'</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>یا حتی:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><br></span></code></pre></div></div>
<p>فقط برای اینکه امضای اینترفیس را کامل کرده باشند.</p>
<p>این وضعیت معمولاً یعنی این کلاس، مصرف‌کننده، یا پیاده‌سازی، اصلاً نباید به آن بخش از قرارداد وابسته می‌بود. یعنی اینترفیس برای او بزرگ‌تر از حد لازم است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-نمونه-از-شکستن-درست-اینترفیس">یک نمونه از شکستن درست اینترفیس<a href="https://example.com/blog/interface-segregation-fat-interfaces#%DB%8C%DA%A9-%D9%86%D9%85%D9%88%D9%86%D9%87-%D8%A7%D8%B2-%D8%B4%DA%A9%D8%B3%D8%AA%D9%86-%D8%AF%D8%B1%D8%B3%D8%AA-%D8%A7%DB%8C%D9%86%D8%AA%D8%B1%D9%81%DB%8C%D8%B3" class="hash-link" aria-label="لینک مستقیم به یک نمونه از شکستن درست اینترفیس" title="لینک مستقیم به یک نمونه از شکستن درست اینترفیس" translate="no">​</a></h2>
<p>فرض کنید ابتدا چنین ساختاری داشتیم:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">transactions/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  transaction-repository.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  transaction-details-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  daily-report-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  month-close-service.ts</span><br></span></code></pre></div></div>
<p>و <code>transaction-repository.ts</code> همه‌چیز را در خودش جمع کرده بود.</p>
<p>بعد از بازنگری، شاید چنین ساختاری روشن‌تر باشد:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">transactions/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  contracts/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transaction-reader.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transaction-writer.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transaction-report-exporter.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transaction-maintenance-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  services/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transaction-details-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    daily-report-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    month-close-service.ts</span><br></span></code></pre></div></div>
<p>در این ساختار، هر سرویس به قرارداد نزدیک به نیاز خودش وابسته می‌شود. این فقط یک تغییر ظاهری در پوشه‌ها نیست؛ نشانه‌ی این است که مرزهای وابستگی دقیق‌تر شده‌اند.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>برای شکستن اینترفیس، از این پرسش شروع کنید: «این مصرف‌کننده دقیقاً کدام متدها را لازم دارد؟»</p><p>اگر چند مصرف‌کننده‌ی مختلف، هرکدام فقط بخشی از یک اینترفیس را استفاده می‌کنند، معمولاً وقت آن رسیده که قرارداد را به چند اینترفیس کوچک‌تر و منسجم‌تر بشکنید.</p><p>بهتر است این شکستن بر اساس الگوی مصرف انجام شود، نه صرفاً بر اساس حدس یا سلیقه.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آزمون-بعد-از-جداسازی-چهقدر-سادهتر-میشود">آزمون بعد از جداسازی، چه‌قدر ساده‌تر می‌شود؟<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D8%A2%D8%B2%D9%85%D9%88%D9%86-%D8%A8%D8%B9%D8%AF-%D8%A7%D8%B2-%D8%AC%D8%AF%D8%A7%D8%B3%D8%A7%D8%B2%DB%8C-%DA%86%D9%87%D9%82%D8%AF%D8%B1-%D8%B3%D8%A7%D8%AF%D9%87%D8%AA%D8%B1-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به آزمون بعد از جداسازی، چه‌قدر ساده‌تر می‌شود؟" title="لینک مستقیم به آزمون بعد از جداسازی، چه‌قدر ساده‌تر می‌شود؟" translate="no">​</a></h2>
<p>بعد از جداکردن اینترفیس، آزمون <code>TransactionDetailsService</code> خیلی تمیزتر می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">FakeTransactionReader</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">TransactionReader</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و خود آزمون هم سرراست‌تر می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'returns transaction details by id'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> reader </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">FakeTransactionReader</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> service </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">TransactionDetailsService</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">reader</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> service</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getDetails</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'tx-1'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toEqual</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'tx-1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>در این نسخه، دیگر خبری از متدهای اضافی، قراردادهای بی‌ربط، یا پیاده‌سازی‌های ساختگی زورکی نیست. آزمون دقیقاً همان چیزی را می‌سنجد که سرویس به آن وابسته است.</p>
<p>این سادگی، فقط یک مزیت جانبی نیست؛ نشانه‌ی بهترشدن خود طراحی است.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>آزمون ساده‌تر فقط یعنی کار تست‌نویس راحت‌تر نشده است؛ معمولاً یعنی وابستگی‌های واقعی هم شفاف‌تر شده‌اند.</p><p>وقتی بتوانید یک مصرف‌کننده را با یک اینترفیس کوچک و دقیق آزمون کنید، احتمال خوبی هست که مرز طراحی‌تان هم روشن‌تر شده باشد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>اگر حدس می‌زنید در پروژه‌تان اینترفیس‌های چاق وجود دارد، لازم نیست همه‌چیز را از نو طراحی کنید. از مصرف‌کننده‌های کوچک شروع کنید؛ همان‌جاهایی که فقط یک یا دو متد از یک قرارداد بزرگ را استفاده می‌کنند.</p>
<p>چیزهایی که باید بررسی شوند:</p>
<ul>
<li class="">آیا یک کلاس فقط از بخش کوچکی از یک اینترفیس استفاده می‌کند؟</li>
<li class="">آیا در آزمون‌ها مجبور می‌شوید چند متد بی‌ربط را ساختگی پیاده‌سازی کنید؟</li>
<li class="">آیا پیاده‌سازی‌ها بعضی متدها را با <code>not implemented</code> یا <code>not supported</code> پر می‌کنند؟</li>
<li class="">آیا تغییر در یک متد کم‌اهمیت، روی مصرف‌کننده‌های نامرتبط هم اثر می‌گذارد؟</li>
<li class="">آیا می‌توان قرارداد را بر اساس الگوی مصرف به چند بخش منسجم‌تر شکست؟</li>
</ul>
<p>چیزهایی که نباید بی‌دلیل تغییر کنند:</p>
<ul>
<li class="">اینترفیس‌های کوچک و منسجمی که مصرف‌کننده‌ها واقعاً به همان شکل از آن‌ها استفاده می‌کنند.</li>
<li class="">مرزهایی که در پروژه جا افتاده‌اند و هنوز درد واقعی ایجاد نکرده‌اند.</li>
<li class="">نام‌گذاری‌ها و ساختارهایی که فقط برای «بیشتر خرد کردن» قرار است تغییر کنند، نه برای حل یک مسئله‌ی واقعی.</li>
<li class="">طراحی‌هایی که با شکستن بیش از حد، فهم سیستم را سخت‌تر می‌کنند.</li>
</ul>
<p>برای اعتبارسنجی تغییر، دست‌کم این گام‌ها را اجرا کنید:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">npm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run lint</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run typecheck</span><br></span></code></pre></div></div>
<p>اگر پروژه‌ی شما این فرمان‌ها را ندارد، معادل آن‌ها را اجرا کنید. علاوه بر این، بعد از شکستن اینترفیس، خوب است یک بار دیگر آزمون‌ها را مرور کنید و ببینید آیا فیک‌ها و ماک‌ها ساده‌تر شده‌اند یا نه. اگر نشده‌اند، شاید هنوز مرز درست را پیدا نکرده‌اید.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/interface-segregation-fat-interfaces#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>اصل جداسازی اینترفیس از ما می‌خواهد مصرف‌کننده را به چیزی وابسته نکنیم که به آن نیاز ندارد. این حرف در ظاهر ساده است، اما اثرش عمیق است. چون خیلی از وابستگی‌های پرهزینه، نه از کدهای خیلی پیچیده، بلکه از قراردادهای بیش از حد بزرگ و مبهم می‌آیند.</p>
<p>اینترفیس چاق، کلاس کوچک را هم آلوده می‌کند. چون وابستگی‌هایی را به آن تحمیل می‌کند که واقعاً مال او نیستند. نتیجه، آزمون سخت‌تر، تغییر پرهزینه‌تر، و مرزهای مبهم‌تر است.</p>
<p>طراحی خوب همیشه با افزودن لایه‌های بیشتر به دست نمی‌آید. گاهی فقط باید قراردادها را به‌اندازه‌ی نیاز مصرف‌کننده‌ها صادقانه‌تر تعریف کنیم. اصل جداسازی اینترفیس دقیقاً همین را به ما یادآوری می‌کند: کوچک‌کردن وابستگی، اغلب مهم‌تر از بزرگ‌کردن توانایی ظاهری یک اینترفیس است.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="SOLID" term="SOLID"/>
        <category label="اینترفیس" term="اینترفیس"/>
        <category label="وابستگی" term="وابستگی"/>
        <category label="طراحی نرم‌افزار" term="طراحی نرم‌افزار"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[هر چیزی که شبیه چیز دیگر است، جایگزین آن نیست]]></title>
        <id>https://example.com/blog/liskov-substitution-honest-inheritance</id>
        <link href="https://example.com/blog/liskov-substitution-honest-inheritance"/>
        <updated>2026-04-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[توضیحی درباره‌ی اینکه رابطه‌ی ارث‌بری یا پیاده‌سازی اینترفیس باید از نظر رفتاری درست باشد، نه فقط از نظر ظاهری.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="چند قطعه‌ی شبیه به هم که فقط یکی از آن‌ها واقعاً در جای قطعه‌ی اصلی می‌نشیند." src="https://example.com/assets/images/liskov-substitution-honest-inheritance-59ad325c19f637b915e886db71131fae.png" width="1672" height="941" class="img_m9Pm"></p>
<p>فرض کنید در یک سامانه‌ی پرداخت، کلاسی داریم که قرار است رسید پرداخت را تولید و ذخیره کند. برای همین، یک قرارداد ساده تعریف کرده‌ایم: هر «ذخیره‌ساز رسید» باید بتواند رسید را بگیرد و بدون غافل‌گیری آن را ذخیره کند.</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">ReceiptStore</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا یک پیاده‌سازی معمولی داریم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">DatabaseReceiptStore</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">ReceiptStore</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">receipts</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>تا این‌جا همه‌چیز روشن است. اما بعدتر کسی می‌گوید: «ما یک نسخه‌ی فقط‌خواندنی هم لازم داریم. همان را هم از همین اینترفیس ارث ببریم.» و نتیجه چیزی شبیه این می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">ReadOnlyReceiptStore</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">ReceiptStore</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'This store is read-only'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>از نظر نام و ساختار، این کلاس شبیه یک <code>ReceiptStore</code> است. همان اینترفیس را پیاده‌سازی کرده، همان متد را دارد، و حتی شاید از نظر ابزارهای ایستا هیچ خطایی هم نداشته باشد. اما از نظر رفتاری، قرارداد را شکسته است. مصرف‌کننده‌ای که به <code>ReceiptStore</code> اعتماد کرده بود، انتظار داشت <code>save</code> رسید را ذخیره کند، نه اینکه در زمان اجرا غافلگیر شود.</p>
<p>اینجا مسئله فقط یک استثنا یا یک خطای کوچک نیست. مسئله این است که ما چیزی ساخته‌ایم که شبیه نوع اصلی است، اما جایگزین آن نیست.</p>
<p>اصل جایگزینی لیسکوف دقیقاً از همین‌جا آغاز می‌شود.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>وقتی از «قرارداد رفتاری» حرف می‌زنیم، فقط امضای تابع یا نام متد منظور نیست. قرارداد رفتاری یعنی مصرف‌کننده بر اساس چه انتظارهایی از یک نوع استفاده می‌کند.</p><p>برای نمونه، اگر یک تابع <code>save</code> نام دارد، مصرف‌کننده معمولاً انتظار دارد:</p><ul>
<li class="">با گرفتن ورودی معتبر، عملیات اصلی را انجام دهد.</li>
<li class="">به‌دلیل محدودیت پنهان و نامنتظر شکست نخورد.</li>
<li class="">نتیجه‌ای بدهد که با معنای آن قرارداد سازگار باشد.</li>
</ul><p>پس قرارداد، فقط آن چیزی نیست که در اینترفیس نوشته شده؛ بخشی از آن در رفتار مورد انتظار نهفته است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="شباهت-ظاهری-کافی-نیست">شباهت ظاهری کافی نیست<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D8%B4%D8%A8%D8%A7%D9%87%D8%AA-%D8%B8%D8%A7%D9%87%D8%B1%DB%8C-%DA%A9%D8%A7%D9%81%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به شباهت ظاهری کافی نیست" title="لینک مستقیم به شباهت ظاهری کافی نیست" translate="no">​</a></h2>
<p>در طراحی شیءگرا، به‌ویژه وقتی از ارث‌بری یا پیاده‌سازی اینترفیس استفاده می‌کنیم، وسوسه‌ای پنهان وجود دارد: اگر دو چیز از نظر شکل شبیه‌اند، پس حتماً می‌توان یکی را جای دیگری گذاشت.</p>
<p>اما این فقط نیمی از ماجراست.</p>
<p>ممکن است دو کلاس:</p>
<ul>
<li class="">نام‌های مشابه داشته باشند،</li>
<li class="">متدهای یکسانی ارائه دهند،</li>
<li class="">از یک والد مشترک ارث ببرند،</li>
<li class="">یا یک اینترفیس مشترک را پیاده‌سازی کنند،</li>
</ul>
<p>ولی همچنان از نظر رفتار، ناسازگار باشند.</p>
<p>اصل جایگزینی لیسکوف (Liskov Substitution Principle) می‌گوید اگر <code>B</code> زیرنوع <code>A</code> است، باید بتوان <code>B</code> را هر جا که <code>A</code> انتظار می‌رود قرار داد، بی‌آنکه درستی برنامه به‌هم بخورد. به بیان ساده‌تر، زیرنوع باید قول‌های نوع پایه را نگه دارد.</p>
<p>این اصل ما را از طراحی‌هایی دور می‌کند که فقط از نظر ظاهری منظم‌اند، اما در عمل مصرف‌کننده را فریب می‌دهند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مثال-آشناتر-مستطیل-و-مربع">مثال آشناتر: مستطیل و مربع<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D9%85%D8%AB%D8%A7%D9%84-%D8%A2%D8%B4%D9%86%D8%A7%D8%AA%D8%B1-%D9%85%D8%B3%D8%AA%D8%B7%DB%8C%D9%84-%D9%88-%D9%85%D8%B1%D8%A8%D8%B9" class="hash-link" aria-label="لینک مستقیم به مثال آشناتر: مستطیل و مربع" title="لینک مستقیم به مثال آشناتر: مستطیل و مربع" translate="no">​</a></h2>
<p>نمونه‌ی کلاسیک این اصل، رابطه‌ی مستطیل و مربع است. در نگاه نخست، خیلی طبیعی به نظر می‌رسد که بگوییم «مربع نوعی مستطیل است». از نظر هندسی شاید چنین باشد. اما در طراحی نرم‌افزار، مسئله فقط شباهت مفهومی نیست؛ مسئله رفتار قراردادی است.</p>
<p>فرض کنید چنین مدلی داریم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">Rectangle</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">protected</span><span class="token plain"> width </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">protected</span><span class="token plain"> height </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">setWidth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> width</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">setHeight</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">height </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> height</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">getArea</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">height</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا کسی می‌گوید مربع هم که مستطیل است؛ پس از آن ارث می‌برد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">Square</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">extends</span><span class="token plain"> </span><span class="token class-name">Rectangle</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">setWidth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> width</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">height </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> width</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">setHeight</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> height</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">height </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> height</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>از نظر ظاهر، همه‌چیز مرتب است. اما حالا به این تابع نگاه کنید:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">resizeAndMeasure</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">rectangle</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Rectangle</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  rectangle</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setWidth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  rectangle</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setHeight</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> rectangle</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">getArea</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اگر یک <code>Rectangle</code> معمولی بدهیم، خروجی <code>20</code> می‌شود. اما اگر <code>Square</code> بدهیم، خروجی <code>16</code> می‌شود. چرا؟ چون <code>Square</code> قرارداد رفتاری ضمنی <code>Rectangle</code> را تغییر داده است. مصرف‌کننده انتظار داشت تغییر عرض و ارتفاع مستقل باشند، اما این انتظار در زیرنوع شکسته شد.</p>
<p>این همان جایی است که باید از خودمان بپرسیم: آیا این واقعاً یک زیرنوع صادق است، یا فقط یک شباهت ظاهری ما را فریب داده است؟</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>ارث‌بری دروغین معمولاً از جایی آغاز می‌شود که می‌خواهیم یک رابطه‌ی مفهومی یا زبانی را به رابطه‌ی فنی تبدیل کنیم، بدون اینکه رفتار را بررسی کنیم.</p><p>جمله‌هایی مثل این‌ها خطرناک‌اند:</p><ul>
<li class="">«خب این هم یک نوع از همان است.»</li>
<li class="">«اسمش شبیه است، پس از همان ارث ببرد.»</li>
<li class="">«فقط برای استفاده‌ی دوباره از کد، این را زیرکلاس آن می‌کنیم.»</li>
</ul><p>اگر زیرنوع ناچار است بخشی از رفتار والد را خراب کند، محدود کند، یا با استثنا و شرط‌های عجیب دور بزند، احتمال زیادی دارد که با یک ارث‌بری نادرست روبه‌رو باشیم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-اعتماد-مصرفکننده-مهمتر-از-شباهت-ظاهری-است">چرا اعتماد مصرف‌کننده مهم‌تر از شباهت ظاهری است؟<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%DA%86%D8%B1%D8%A7-%D8%A7%D8%B9%D8%AA%D9%85%D8%A7%D8%AF-%D9%85%D8%B5%D8%B1%D9%81%DA%A9%D9%86%D9%86%D8%AF%D9%87-%D9%85%D9%87%D9%85%D8%AA%D8%B1-%D8%A7%D8%B2-%D8%B4%D8%A8%D8%A7%D9%87%D8%AA-%D8%B8%D8%A7%D9%87%D8%B1%DB%8C-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به چرا اعتماد مصرف‌کننده مهم‌تر از شباهت ظاهری است؟" title="لینک مستقیم به چرا اعتماد مصرف‌کننده مهم‌تر از شباهت ظاهری است؟" translate="no">​</a></h2>
<p>مصرف‌کننده‌ی یک نوع، معمولاً بر پایه‌ی قرارداد آن تصمیم می‌گیرد؛ نه بر پایه‌ی جزئیات پنهان پیاده‌سازی.</p>
<p>وقتی تابعی یک <code>ReceiptStore</code> می‌گیرد، می‌خواهد رسید را ذخیره کند. وقتی بخشی از سامانه <code>Rectangle</code> می‌گیرد، انتظار رفتاری دارد که با معنای آن سازگار است. وقتی ما زیرنوعی می‌سازیم که این انتظارها را نقض می‌کند، در واقع اعتماد مصرف‌کننده را می‌شکنیم.</p>
<p>این شکست معمولاً خودش را در چند نشانه نشان می‌دهد:</p>
<ul>
<li class="">مصرف‌کننده ناچار می‌شود قبل از استفاده، نوع واقعی شیء را بررسی کند.</li>
<li class="">بخشی از کد برای بعضی زیرنوع‌ها کار می‌کند و برای بعضی دیگر نه.</li>
<li class="">شرط‌های <code>if instance of ...</code> یا شاخه‌های استثنایی زیاد می‌شوند.</li>
<li class="">آزمون‌هایی که باید یکسان باشند، برای زیرنوع‌ها رفتارهای جداگانه می‌خواهند.</li>
<li class="">نام کلاس‌ها می‌گویند «من جایگزین‌پذیرم»، ولی رفتارشان می‌گوید «با احتیاط از من استفاده کن».</li>
</ul>
<p>وقتی به این نقطه می‌رسیم، مشکل فقط در یک کلاس نیست. طراحی دارد به ما هشدار می‌دهد که مرز نوع‌ها درست انتخاب نشده است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیادهسازی-اینترفیس-هم-میتواند-اصل-را-بشکند">پیاده‌سازی اینترفیس هم می‌تواند اصل را بشکند<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D9%BE%DB%8C%D8%A7%D8%AF%D9%87%D8%B3%D8%A7%D8%B2%DB%8C-%D8%A7%DB%8C%D9%86%D8%AA%D8%B1%D9%81%DB%8C%D8%B3-%D9%87%D9%85-%D9%85%DB%8C%D8%AA%D9%88%D8%A7%D9%86%D8%AF-%D8%A7%D8%B5%D9%84-%D8%B1%D8%A7-%D8%A8%D8%B4%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به پیاده‌سازی اینترفیس هم می‌تواند اصل را بشکند" title="لینک مستقیم به پیاده‌سازی اینترفیس هم می‌تواند اصل را بشکند" translate="no">​</a></h2>
<p>گاهی فکر می‌کنیم اصل جایگزینی لیسکوف فقط درباره‌ی ارث‌بری است. اما این اصل در مورد هر نوع رابطه‌ی جانشینی صادق است؛ چه ارث‌بری باشد، چه پیاده‌سازی اینترفیس.</p>
<p>برای نمونه، فرض کنید قراردادی برای ارسال اعلان داریم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">NotificationSender</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>یک فرستنده‌ی ایمیل داریم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">EmailSender</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">NotificationSender</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    emailClient</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>بعد یک پیاده‌سازی دیگر اضافه می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">DisabledNotificationSender</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">NotificationSender</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'Sending is disabled'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اگر هدف این کلاس فقط این است که در بعضی محیط‌ها چیزی ارسال نکند، شاید بهتر باشد به‌جای شکستن قرارداد، رفتاری سازگارتر ارائه دهد؛ مثلاً یک پیاده‌سازی بی‌اثر داشته باشیم که پیام را نادیده بگیرد، یا اصلاً قرارداد جداگانه‌ای تعریف کنیم که با این سناریو سازگار باشد.</p>
<p>در هر صورت، نکته این است که صرف پیاده‌سازی یک اینترفیس، جایگزین‌پذیری را تضمین نمی‌کند. باید از نظر رفتاری هم راست‌گو باشیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="زیرنوع-صادق-چه-ویژگیای-دارد">زیرنوع صادق چه ویژگی‌ای دارد؟<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D8%B2%DB%8C%D8%B1%D9%86%D9%88%D8%B9-%D8%B5%D8%A7%D8%AF%D9%82-%DA%86%D9%87-%D9%88%DB%8C%DA%98%DA%AF%DB%8C%D8%A7%DB%8C-%D8%AF%D8%A7%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به زیرنوع صادق چه ویژگی‌ای دارد؟" title="لینک مستقیم به زیرنوع صادق چه ویژگی‌ای دارد؟" translate="no">​</a></h2>
<p>زیرنوع صادق، نوعی است که اگر آن را جای نوع پایه بگذاریم، مصرف‌کننده مجبور نشود منطقش را عوض کند. به بیان عملی، زیرنوع نباید:</p>
<ul>
<li class="">پیش‌شرط‌ها را سخت‌تر کند،</li>
<li class="">پس‌شرط‌ها را ضعیف‌تر کند،</li>
<li class="">رفتار مورد انتظار را نقض کند،</li>
<li class="">یا محدودیت‌های تازه‌ای تحمیل کند که در نوع پایه وجود نداشته‌اند.</li>
</ul>
<p>مثلاً اگر نوع پایه می‌گوید «این تابع با هر فایل معتبر کار می‌کند»، زیرنوع نباید ناگهان بگوید «من فقط با فایل‌های کوچک‌تر از یک مگابایت کار می‌کنم»، مگر اینکه این محدودیت بخشی از همان قرارداد اصلی بوده باشد.</p>
<p>اگر نوع پایه قول می‌دهد «این متد داده را ذخیره می‌کند»، زیرنوع نباید بگوید «من در بعضی حالت‌ها عمداً هیچ کاری نمی‌کنم» یا «برای من ذخیره‌سازی ممنوع است»، مگر اینکه قرارداد از ابتدا چنین چیزی را مجاز دانسته باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="اگر-ارثبری-درست-نیست-چه-کنیم">اگر ارث‌بری درست نیست، چه کنیم؟<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D8%A7%DA%AF%D8%B1-%D8%A7%D8%B1%D8%AB%D8%A8%D8%B1%DB%8C-%D8%AF%D8%B1%D8%B3%D8%AA-%D9%86%DB%8C%D8%B3%D8%AA-%DA%86%D9%87-%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به اگر ارث‌بری درست نیست، چه کنیم؟" title="لینک مستقیم به اگر ارث‌بری درست نیست، چه کنیم؟" translate="no">​</a></h2>
<p>وقتی می‌فهمیم رابطه‌ی ارث‌بری یا پیاده‌سازی فعلی صادقانه نیست، معمولاً چند راه بهتر داریم.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۱-قرارداد-را-دقیقتر-تعریف-کنیم">۱. قرارداد را دقیق‌تر تعریف کنیم<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%DB%B1-%D9%82%D8%B1%D8%A7%D8%B1%D8%AF%D8%A7%D8%AF-%D8%B1%D8%A7-%D8%AF%D9%82%DB%8C%D9%82%D8%AA%D8%B1-%D8%AA%D8%B9%D8%B1%DB%8C%D9%81-%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به ۱. قرارداد را دقیق‌تر تعریف کنیم" title="لینک مستقیم به ۱. قرارداد را دقیق‌تر تعریف کنیم" translate="no">​</a></h3>
<p>گاهی مشکل از این است که نوع پایه بیش از حد کلی یا مبهم تعریف شده است. شاید چیزی که ما <code>ReceiptStore</code> نامیده‌ایم، در واقع دو مفهوم متفاوت را در یک قرارداد ریخته است.</p>
<p>برای نمونه، شاید بهتر باشد به‌جای این:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">ReceiptStore</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>چنین مرزبندی‌ای داشته باشیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">ReceiptWriter</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">ReceiptReader</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Receipt </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا یک پیاده‌سازی فقط‌خواندنی مجبور نیست خودش را به‌زور <code>Writer</code> جا بزند.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۲-بهجای-ارثبری-از-ترکیب-استفاده-کنیم">۲. به‌جای ارث‌بری، از ترکیب استفاده کنیم<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%DB%B2-%D8%A8%D9%87%D8%AC%D8%A7%DB%8C-%D8%A7%D8%B1%D8%AB%D8%A8%D8%B1%DB%8C-%D8%A7%D8%B2-%D8%AA%D8%B1%DA%A9%DB%8C%D8%A8-%D8%A7%D8%B3%D8%AA%D9%81%D8%A7%D8%AF%D9%87-%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به ۲. به‌جای ارث‌بری، از ترکیب استفاده کنیم" title="لینک مستقیم به ۲. به‌جای ارث‌بری، از ترکیب استفاده کنیم" translate="no">​</a></h3>
<p>گاهی شباهت رفتاری کافی نیست، ولی بخشی از پیاده‌سازی مشترک است. در این حالت، ترکیب معمولاً از ارث‌بری سالم‌تر است.</p>
<p>برای نمونه، به‌جای اینکه <code>Square</code> را از <code>Rectangle</code> ارث بدهیم، می‌توانیم هر دو را نوع‌های مستقل با قراردادهای روشن‌تر بدانیم، یا از یک مؤلفه‌ی مشترک برای محاسبه‌ی مساحت و ویژگی‌های هندسی استفاده کنیم.</p>
<h3 class="anchor anchorTargetStickyNavbar_UCMm" id="۳-نوعهای-عمومی-را-بیش-از-حد-عمومی-نکنیم">۳. نوع‌های عمومی را بیش از حد عمومی نکنیم<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%DB%B3-%D9%86%D9%88%D8%B9%D9%87%D8%A7%DB%8C-%D8%B9%D9%85%D9%88%D9%85%DB%8C-%D8%B1%D8%A7-%D8%A8%DB%8C%D8%B4-%D8%A7%D8%B2-%D8%AD%D8%AF-%D8%B9%D9%85%D9%88%D9%85%DB%8C-%D9%86%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به ۳. نوع‌های عمومی را بیش از حد عمومی نکنیم" title="لینک مستقیم به ۳. نوع‌های عمومی را بیش از حد عمومی نکنیم" translate="no">​</a></h3>
<p>بعضی وقت‌ها تلاش می‌کنیم یک نوع را آن‌قدر کلی کنیم که همه‌چیز را پوشش دهد. نتیجه این می‌شود که زیرنوع‌ها ناچارند بخشی از آن قرارداد را بشکنند. طراحی خوب همیشه با «عام‌کردن بیشتر» به دست نمی‌آید. گاهی باید نوع‌های کوچک‌تر، دقیق‌تر و صادق‌تری بسازیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آزمون-رفتاری-نه-فقط-آزمون-واحد">آزمون رفتاری، نه فقط آزمون واحد<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D8%A2%D8%B2%D9%85%D9%88%D9%86-%D8%B1%D9%81%D8%AA%D8%A7%D8%B1%DB%8C-%D9%86%D9%87-%D9%81%D9%82%D8%B7-%D8%A2%D8%B2%D9%85%D9%88%D9%86-%D9%88%D8%A7%D8%AD%D8%AF" class="hash-link" aria-label="لینک مستقیم به آزمون رفتاری، نه فقط آزمون واحد" title="لینک مستقیم به آزمون رفتاری، نه فقط آزمون واحد" translate="no">​</a></h2>
<p>یکی از بهترین راه‌ها برای تشخیص شکستن اصل جایگزینی لیسکوف، آزمودن رفتار مشترک زیرنوع‌هاست.</p>
<p>اگر یک قرارداد واقعاً درست تعریف شده باشد، باید بتوانیم مجموعه‌ای از آزمون‌های مشترک برای همه‌ی پیاده‌سازی‌ها بنویسیم. اگر بعضی پیاده‌سازی‌ها ناگهان از این آزمون‌ها رد نمی‌شوند، احتمال خوبی هست که زیرنوع صادقی نباشند.</p>
<p>برای نمونه، درباره‌ی <code>ReceiptStore</code> می‌توانیم چنین آزمونی داشته باشیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">shouldBehaveLikeReceiptStore</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">store</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> ReceiptStore</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> receipt </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'r-1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> saved </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">receipts</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'r-1'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">saved</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toEqual</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اگر <code>DatabaseReceiptStore</code> از این آزمون رد شود ولی <code>ReadOnlyReceiptStore</code> نه، مسئله روشن است: پیاده‌سازی دوم قرارداد را نگه نداشته است.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>برای آزمودن اصل جایگزینی لیسکوف، به‌جای آنکه فقط هر کلاس را جداگانه بررسی کنید، یک مجموعه آزمون مشترک برای قرارداد اصلی بنویسید و آن را روی همه‌ی پیاده‌سازی‌ها اجرا کنید.</p><p>اگر یک زیرنوع برای عبور از این آزمون‌ها نیاز به استثنا، شاخه‌ی ویژه، یا تغییر انتظارها داشته باشد، این نشانه‌ی خوبی نیست. معمولاً یعنی رابطه‌ی جانشینی صادقانه تعریف نشده است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-نشانهی-مهم-آیا-مصرفکننده-باید-حواسش-جمع-باشد">یک نشانه‌ی مهم: آیا مصرف‌کننده باید «حواسش جمع باشد»؟<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%DB%8C%DA%A9-%D9%86%D8%B4%D8%A7%D9%86%D9%87%DB%8C-%D9%85%D9%87%D9%85-%D8%A2%DB%8C%D8%A7-%D9%85%D8%B5%D8%B1%D9%81%DA%A9%D9%86%D9%86%D8%AF%D9%87-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%AD%D9%88%D8%A7%D8%B3%D8%B4-%D8%AC%D9%85%D8%B9-%D8%A8%D8%A7%D8%B4%D8%AF" class="hash-link" aria-label="لینک مستقیم به یک نشانه‌ی مهم: آیا مصرف‌کننده باید «حواسش جمع باشد»؟" title="لینک مستقیم به یک نشانه‌ی مهم: آیا مصرف‌کننده باید «حواسش جمع باشد»؟" translate="no">​</a></h2>
<p>یکی از بهترین پرسش‌ها برای بررسی این اصل این است:</p>
<p>آیا مصرف‌کننده باید بداند دقیقاً با کدام زیرنوع کار می‌کند تا بتواند درست از آن استفاده کند؟</p>
<p>اگر پاسخ مثبت است، احتمالاً جایگزین‌پذیری شکسته شده است.</p>
<p>برای نمونه، اگر جایی در کد به این برسیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">persistReceipt</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">store</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> ReceiptStore</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> receipt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">store </span><span class="token keyword" style="color:#00009f">instanceof</span><span class="token plain"> </span><span class="token class-name">ReadOnlyReceiptStore</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receipt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد عملاً اعتراف می‌کند که <code>ReadOnlyReceiptStore</code> جایگزین صادقی برای <code>ReceiptStore</code> نیست. چون مصرف‌کننده دیگر نمی‌تواند فقط به قرارداد تکیه کند؛ مجبور است نوع واقعی را بشناسد و برای آن شاخه‌ی جدا بسازد.</p>
<p>این همان چیزی است که اصل جایگزینی لیسکوف می‌خواهد از آن پرهیز کنیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>اگر در کدتان ارث‌بری یا اینترفیس‌های زیادی دارید، لازم نیست یک‌باره همه‌چیز را بازطراحی کنید. از جاهایی شروع کنید که نشانه‌های ناسازگاری رفتاری در آن‌ها دیده می‌شود.</p>
<p>چیزهایی که باید بررسی شوند:</p>
<ul>
<li class="">آیا یک زیرنوع بعضی متدهای والد را با <code>throw new Error</code>، <code>not supported</code> یا رفتار بی‌اثر پیاده‌سازی کرده است؟</li>
<li class="">آیا مصرف‌کننده‌ها مجبورند قبل از استفاده، نوع واقعی شیء را بررسی کنند؟</li>
<li class="">آیا یک اینترفیس بیش از حد کلی شده و پیاده‌سازی‌ها فقط بخشی از آن را واقعاً پشتیبانی می‌کنند؟</li>
<li class="">آیا آزمون مشترکی می‌توان برای همه‌ی پیاده‌سازی‌های یک قرارداد نوشت؟</li>
<li class="">آیا نام نوع‌ها از جانشینی خبر می‌دهد، ولی رفتارشان چیز دیگری می‌گوید؟</li>
</ul>
<p>چیزهایی که نباید بی‌دلیل تغییر کنند:</p>
<ul>
<li class="">قراردادهای سالم و جاافتاده‌ای که مصرف‌کننده‌ها به آن‌ها تکیه کرده‌اند.</li>
<li class="">نوع‌های کوچک و دقیق، فقط برای اینکه سلسله‌مراتب ظاهری «تمیزتر» شود.</li>
<li class="">پیاده‌سازی‌های مستقلی که با ترکیب بهتر کار می‌کنند.</li>
<li class="">طراحی‌هایی که با حذف ارث‌بری نادرست ساده‌تر و صادق‌تر می‌شوند.</li>
</ul>
<p>برای اعتبارسنجی تغییر، دست‌کم این گام‌ها را اجرا کنید:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">npm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run lint</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run typecheck</span><br></span></code></pre></div></div>
<p>اگر پروژه‌ی شما این فرمان‌ها را ندارد، معادل همان‌ها را اجرا کنید. در این موضوع، به‌ویژه مهم است که آزمون‌های رفتاری مشترک داشته باشید؛ یعنی آزمون‌هایی که قرارداد پایه را بررسی می‌کنند، نه فقط جزئیات هر کلاس را.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/liskov-substitution-honest-inheritance#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>اصل جایگزینی لیسکوف درباره‌ی این نیست که سلسله‌مراتب ارث‌بری زیبایی بسازیم. درباره‌ی این است که وقتی می‌گوییم «این نوع، گونه‌ای از آن نوع است»، واقعاً از نظر رفتاری هم راست بگوییم.</p>
<p>هر چیزی که نام مشابه دارد، متدهای مشابه دارد، یا از یک والد مشترک ارث می‌برد، لزوماً جایگزین آن نیست. اگر مصرف‌کننده نتواند با اطمینان از قرارداد اصلی استفاده کند، جایگزین‌پذیری از بین رفته است.</p>
<p>در طراحی خوب، شباهت ظاهری ارزش دارد، اما کافی نیست. آنچه واقعاً مهم است، اعتماد رفتاری است. اگر یک زیرنوع این اعتماد را نگه می‌دارد، ارث‌بری یا پیاده‌سازی آن احتمالاً صادقانه است. اگر نه، بهتر است به‌جای پافشاری بر شباهت ظاهری، مرز نوع‌ها را دوباره و صادقانه‌تر تعریف کنیم.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="SOLID" term="SOLID"/>
        <category label="جایگزینی لیسکوف" term="جایگزینی لیسکوف"/>
        <category label="ارث‌بری" term="ارث‌بری"/>
        <category label="طراحی نرم‌افزار" term="طراحی نرم‌افزار"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[چرا یک ریفکتور سالم می‌تواند تست‌ها را بشکند؟]]></title>
        <id>https://example.com/blog/why-good-refactors-break-tests</id>
        <link href="https://example.com/blog/why-good-refactors-break-tests"/>
        <updated>2026-04-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[چرا تست‌های تعامل‌محور با یک ریفکتور سالم می‌شکنند و فیک‌ها چگونه این مشکل را کمتر می‌کنند]]></summary>
        <content type="html"><![CDATA[<p>چند وقت پیش با وضعیتی روبه‌رو شدم که در نگاه اول عجیب به نظر می‌رسید: کد را تمیزتر
کردم، رفتار سیستم را عوض نکردم، اما تست‌ها شکست خوردند. همان‌جا روشن شد که مشکل از
خودِ ریفکتور نبود؛ مسئله از تست‌هایی می‌آمد که به‌جای سنجش رفتار، به جزئیات تعامل‌ها
وابسته شده بودند.</p>
<p><img decoding="async" loading="lazy" alt="مقایسهٔ ماک و فیک در آزمون‌نویسی" src="https://example.com/assets/images/mock-vs-fake-78f4573842df40471e2e4368b074dc8a.jpeg" width="1536" height="1024" class="img_m9Pm"></p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکتهٔ اصلی</div><div class="admonitionContent_hiHs"><p>اگر یک ریفکتور سالم باعث شکست تست شد، قبل از متهم کردن کد، اول از خودِ تست بپرسید:
آیا واقعاً رفتار را می‌سنجد، یا به جزئیات پیاده‌سازی وابسته شده است؟</p></div></div>
<p>برای روشن‌تر شدن بحث، یک مثال ساده را در نظر بگیرید. فرض کنید کلاسی داریم به نام
<code>AccessManager</code>. کار این کلاس فقط این است که تصمیم بگیرد یک کاربر دسترسی دارد یا نه؛
برای نمونه با تابعی مانند <code>user_has_access(user_id)</code>.</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">AccessManager</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">__init__</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> auth_service</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">auth_service </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> auth_service</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">user_has_access</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">auth_service</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">lookup_user</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">not</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">None</span><br></span></code></pre></div></div>
<p>این کلاس برای تصمیم‌گیری به یک وابستگی بیرونی تکیه می‌کند: <code>AuthorizationService</code>. این
سرویس تابعی مانند <code>lookup_user(user_id)</code> دارد. اگر کاربر مجاز باشد، یک <code>User</code> برمی‌گرداند
و اگر مجاز نباشد، <code>null</code>.</p>
<p>در تست، نه می‌خواهیم و نه معمولاً می‌توانیم سرویس واقعی مجوزها را بالا بیاوریم. اینجا
یکی از نخستین انتخاب‌ها، ماک است. با ماک معمولاً می‌گوییم اگر
<code>lookup_user("user123")</code> فراخوانی شد، در یک حالت <code>null</code> برگردان و در حالت دیگر <code>User</code>.
تا اینجا هنوز مشکل جدی‌ای نداریم؛ چون فقط رفتار این وابستگی را برای تست فراهم کرده‌ایم.</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">test_returns_true_when_user_is_authorized</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    auth </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Mock</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    auth</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">lookup_user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">return_value </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> User</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    manager </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> AccessManager</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">auth</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">assert</span><span class="token plain"> manager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user_has_access</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">True</span><br></span></code></pre></div></div>
<p>مسئله از جایی آغاز می‌شود که تست را با بررسی فراخوانی‌ها سخت‌گیرانه‌تر می‌کنیم و
می‌گوییم این تابع حتماً باید فراخوانی شده باشد. از این لحظه، آزمون از رفتارمحور بودن
فاصله می‌گیرد و به یک تست تعامل‌محور تبدیل می‌شود. یعنی به‌جای این‌که بپرسیم «خروجی
درست چیست؟»، می‌پرسیم «درون پیاده‌سازی دقیقاً چه فراخوانی‌هایی رخ داده است؟»</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>محل دردسر</div><div class="admonitionContent_hiHs"><p>از اینجا به بعد، تست شما فقط نتیجه را نمی‌سنجد؛ دارد به مسیر رسیدن به نتیجه هم
وابسته می‌شود. همین وابستگی معمولاً باعث شکست‌های بی‌دلیل بعد از ریفکتور می‌شود.</p></div></div>
<p>اینجا همان نقطه‌ای است که یک ریفکتور سالم می‌تواند تست را بشکند، بی‌آن‌که
سیستم خراب شده باشد. ریفکتور معمولاً نتیجه را عوض نمی‌کند؛ فقط مسیر رسیدن به نتیجه
را تغییر می‌دهد. برای نمونه، فرض کنید یک کش ساده به <code>AccessManager</code> اضافه
کنیم. اگر یک بار فهمیدیم <code>user123</code> مجاز است، دیگر لازم نیست هر بار
<code>lookup_user("user123")</code> را صدا بزنیم.</p>
<p>در این حالت، خروجیِ <code>user_has_access("user123")</code> همچنان درست است، اما تستی که اصرار
داشت <code>lookup_user</code> حتماً فراخوانی شود، حالا شکست می‌خورد. آیا این شکست یعنی سیستم
خراب شده است؟ نه. این فقط نشان می‌دهد که تست به شیوهٔ پیاده‌سازی گیر داده بود، نه
به نتیجه‌ای که برای ما اهمیت دارد.</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">test_checks_access_by_calling_lookup_user</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    auth </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Mock</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    auth</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">lookup_user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">return_value </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> User</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    manager </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> AccessManager</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">auth</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    manager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user_has_access</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    auth</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">lookup_user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">assert_called_once_with</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>اینجاست که فیک معنا پیدا می‌کند. فیک، یک پیاده‌سازی ساده اما واقعی از همان وابستگی
است. برای نمونه می‌توانیم <code>FakeAuthorizationService</code> بسازیم که درون خود مجموعه‌ای از
کاربران مجاز نگه می‌دارد.</p>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">FakeAuthorizationService</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">__init__</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">authorized_users </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">add_authorized_user</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">authorized_users</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">add</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">lookup_user</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> user_id </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">authorized_users</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> User</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">None</span><br></span></code></pre></div></div>
<p>در این صورت، تست می‌تواند چنین شکلی داشته باشد:</p>
<ol>
<li class="">بررسی می‌کنیم <code>user_has_access("user123")</code> باید <code>false</code> باشد.</li>
<li class="">کاربر را با چیزی مانند <code>fake.add_authorized_user("user123")</code> به فهرست کاربران مجاز
اضافه می‌کنیم.</li>
<li class="">دوباره بررسی می‌کنیم که این بار خروجی باید <code>true</code> باشد.</li>
</ol>
<div class="language-python codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-python codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">test_changes_behavior_when_user_becomes_authorized</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    fake </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> FakeAuthorizationService</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    manager </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> AccessManager</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">fake</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">assert</span><span class="token plain"> manager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user_has_access</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">False</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    fake</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">add_authorized_user</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">assert</span><span class="token plain"> manager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user_has_access</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"user123"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">True</span><br></span></code></pre></div></div>
<p>چنین تستی دیگر کاری ندارد که <code>lookup_user</code> چند بار یا از کدام نقطه فراخوانی شده است.
آنچه برایش مهم است، رفتار قابل مشاهدهٔ سیستم است. به همین دلیل، این نوع تست‌ها در
برابر ریفکتورهای سالم پایدارترند و کمتر بی‌دلیل می‌شکنند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>پیشنهاد عملی</div><div class="admonitionContent_hiHs"><p>اگر بین ماک و فیک مردد هستید، پیش‌فرض بهتر این است که از خودتان بپرسید:
«می‌خواهم رفتار سیستم را تست کنم یا فقط مطمئن شوم یک فراخوانی مشخص رخ داده است؟»
پاسخ این سؤال معمولاً انتخاب درست را روشن می‌کند.</p></div></div>
<p>جمع‌بندی من این است:</p>
<ul>
<li class="">اگر فقط می‌خواهید مطمئن شوید یک فراخوانی مشخص رخ داده است، ماک ابزار مناسبی است.</li>
<li class="">اما اگر می‌خواهید تست از رفتار سیستم محافظت کند و با یک ریفکتور سالم بی‌دلیل
نشکند، فیک باید انتخاب پیش‌فرض شما باشد و ماک بیشتر یک استثنا.</li>
</ul>
<p>به بیان کوتاه، هرچه تست به نتیجهٔ نهایی نزدیک‌تر باشد، آزادی شما برای تمیزتر کردن
ساختار داخلی کد بیشتر می‌شود.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="آزمون‌نویسی" term="آزمون‌نویسی"/>
        <category label="بازآرایی" term="بازآرایی"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[وقتی هر قابلیت تازه یعنی دست زدن به کدهای قدیمی]]></title>
        <id>https://example.com/blog/open-closed-principle-change-points</id>
        <link href="https://example.com/blog/open-closed-principle-change-points"/>
        <updated>2026-04-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[بررسی اینکه چرا طراحی خوب باید امکان افزودن رفتار تازه را بدون تغییرهای پرخطر در کد قدیمی فراهم کند.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="یک مسیر قدیمی و پایدار که قابلیت‌های تازه از کنار آن افزوده می‌شوند، بدون اینکه مسیر اصلی تخریب شود." src="https://example.com/assets/images/open-closed-principle-change-points-735e8b227975aacd1442c5fd9805eaa1.png" width="1672" height="941" class="img_m9Pm"></p>
<p>فرض کنید در یک سامانه‌ی مالی، چند نوع تراکنش داریم: واریز، برداشت و انتقال. کد هم مدت‌هاست کار می‌کند. گزارش‌ها درست‌اند، آزمون‌ها سبزند، و کسی دوست ندارد بی‌دلیل به مسیرهای قدیمی دست بزند.</p>
<p>حالا یک نوع تراکنش تازه اضافه می‌شود: «برگشت وجه». در ظاهر، تغییر کوچکی است. اما وقتی سراغ پیاده‌سازی می‌رویم، می‌بینیم باید چند فایل قدیمی را باز کنیم: محاسبه‌ی کارمزد، ساخت گزارش، نمایش وضعیت، اعتبارسنجی، و شاید حتی منطق تسویه.</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">calculateFee</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'deposit'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'withdraw'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.01</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'transfer'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'refund'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در یک فایل دیگر هم همین الگو تکرار شده است:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">buildReportRow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'deposit'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'واریز'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sign</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'+'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'withdraw'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'برداشت'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sign</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'-'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'transfer'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'انتقال'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sign</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'-'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'refund'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'برگشت وجه'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sign</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'+'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>کم‌کم معلوم می‌شود افزودن یک رفتار تازه، فقط افزودن کد تازه نیست؛ بازکردن چند نقطه‌ی قدیمی است. این همان جایی است که اصل باز و بسته خودش را نشان می‌دهد.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>اطلاع</div><div class="admonitionContent_hiHs"><p>اصل باز و بسته (Open-Closed Principle) می‌گوید واحدهای نرم‌افزاری باید برای توسعه باز باشند، اما برای تغییر بسته.</p><p>یعنی بتوانیم رفتار تازه اضافه کنیم، بی‌آنکه مجبور شویم کدهای قدیمی و آزموده‌شده را بی‌دلیل و پرخطر تغییر دهیم. منظور این نیست که هیچ فایلی هیچ‌وقت تغییر نکند؛ منظور این است که نقطه‌های تغییر، مهار شده و قابل‌پیش‌بینی باشند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مسئله-دقیقاً-کجاست">مسئله دقیقاً کجاست؟<a href="https://example.com/blog/open-closed-principle-change-points#%D9%85%D8%B3%D8%A6%D9%84%D9%87-%D8%AF%D9%82%DB%8C%D9%82%D8%A7%D9%8B-%DA%A9%D8%AC%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به مسئله دقیقاً کجاست؟" title="لینک مستقیم به مسئله دقیقاً کجاست؟" translate="no">​</a></h2>
<p>در مثال بالا، مشکل این نیست که از <code>if</code> استفاده شده است. گاهی یک شرط ساده، روشن‌ترین و کم‌هزینه‌ترین راه بیان یک تصمیم است. مشکل از جایی آغاز می‌شود که همان تصمیم در چند جای سامانه پخش می‌شود و هر نوع تازه، ما را مجبور می‌کند چند فایل قدیمی را هم‌زمان تغییر دهیم.</p>
<p>وقتی نوع <code>refund</code> اضافه شد، فقط یک قابلیت تازه نساختیم. هم‌زمان مجبور شدیم چند بخش قدیمی را باز کنیم و به آن‌ها شاخه‌ی تازه اضافه کنیم. این کار دو خطر دارد.</p>
<p>نخست اینکه ممکن است یک نقطه را فراموش کنیم. مثلاً کارمزد برگشت وجه را اضافه کنیم، اما گزارش آن را نه. دوم اینکه ممکن است هنگام تغییر یک مسیر قدیمی، رفتار قبلی را ناخواسته خراب کنیم. مثلاً منطق برداشت یا انتقال به‌خاطر یک تغییر کوچک دچار خطا شود.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>خطر اصلی، «تغییر پراکنده» است؛ یعنی برای افزودن یک قابلیت تازه، مجبور شویم چندین فایل قدیمی را باز کنیم و در هرکدام یک شرط، شاخه یا حالت تازه اضافه کنیم.</p><p>در این وضعیت، هزینه‌ی تغییر فقط به اندازه‌ی قابلیت تازه نیست. باید هزینه‌ی پیدا کردن همه‌ی نقاط تغییر، فهمیدن اثر هرکدام، و اطمینان از خراب‌نشدن رفتارهای قبلی را هم پرداخت کنیم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="اصل-باز-و-بسته-if-را-ممنوع-نمیکند">اصل باز و بسته <code>if</code> را ممنوع نمی‌کند<a href="https://example.com/blog/open-closed-principle-change-points#%D8%A7%D8%B5%D9%84-%D8%A8%D8%A7%D8%B2-%D9%88-%D8%A8%D8%B3%D8%AA%D9%87-if-%D8%B1%D8%A7-%D9%85%D9%85%D9%86%D9%88%D8%B9-%D9%86%D9%85%DB%8C%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به اصل-باز-و-بسته-if-را-ممنوع-نمیکند" title="لینک مستقیم به اصل-باز-و-بسته-if-را-ممنوع-نمیکند" translate="no">​</a></h2>
<p>یکی از سوءبرداشت‌های رایج درباره‌ی اصل باز و بسته این است که «نباید هیچ شرطی در کد داشته باشیم». این برداشت، هم افراطی است و هم در عمل به طراحی‌های پیچیده و مصنوعی می‌رسد.</p>
<p>شرط بد نیست. شرطی بد است که نشانه‌ی پخش‌شدن تصمیم در سراسر سامانه باشد.</p>
<p>برای نمونه، این کد شاید کاملاً پذیرفتنی باشد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">isLargeAmount</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> amount </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000_000</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اینجا یک قانون کوچک و مشخص داریم. اما اگر نوع تراکنش در ده جای مختلف بررسی شود، با هر نوع تازه باید ده نقطه را تغییر دهیم. این دیگر فقط یک <code>if</code> ساده نیست؛ این یک محور تغییر پخش‌شده است.</p>
<p>اصل باز و بسته از ما نمی‌خواهد همه‌ی شرط‌ها را حذف کنیم. از ما می‌خواهد بفهمیم کدام شرط‌ها قرار است زیاد تغییر کنند، کدام‌ها رفتارهای مستقل را از هم جدا می‌کنند، و کدام‌ها بهتر است پشت یک نقطه‌ی توسعه‌ی روشن پنهان شوند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نقطهی-توسعه-یعنی-چه">نقطه‌ی توسعه یعنی چه؟<a href="https://example.com/blog/open-closed-principle-change-points#%D9%86%D9%82%D8%B7%D9%87%DB%8C-%D8%AA%D9%88%D8%B3%D8%B9%D9%87-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به نقطه‌ی توسعه یعنی چه؟" title="لینک مستقیم به نقطه‌ی توسعه یعنی چه؟" translate="no">​</a></h2>
<p>نقطه‌ی توسعه جایی است که سامانه عمداً برای افزودن رفتار تازه آماده شده است. یعنی به‌جای اینکه برای هر نوع تراکنش تازه چند فایل قدیمی را تغییر دهیم، بتوانیم یک قطعه‌ی تازه اضافه کنیم و آن را به سامانه معرفی کنیم.</p>
<p>در مثال تراکنش‌ها، می‌توانیم برای هر نوع تراکنش یک پردازنده داشته باشیم. هر پردازنده می‌داند کارمزد، عنوان گزارش و نشانه‌ی مبلغ آن نوع تراکنش چیست.</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">TransactionPolicy</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionType</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">calculateFee</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">buildReportRow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> ReportRow</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اکنون هر نوع تراکنش، رفتار خودش را در یک جای مشخص نگه می‌دارد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">WithdrawPolicy</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">TransactionPolicy</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionType </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'withdraw'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">calculateFee</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.01</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">buildReportRow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'برداشت'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      sign</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'-'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">RefundPolicy</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">TransactionPolicy</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionType </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'refund'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">calculateFee</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">buildReportRow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'برگشت وجه'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      sign</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'+'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>بعد یک نگاشت مرکزی داریم که سیاست مناسب را پیدا می‌کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">TransactionPolicyRegistry</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> policies</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionPolicy</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionType</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> policy </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">policies</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">item</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> item</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">policy</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Unsupported transaction type: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation keyword" style="color:#00009f">type</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> policy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و کدهای مصرف‌کننده دیگر لازم نیست درباره‌ی همه‌ی نوع‌های تراکنش بدانند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">TransactionReportService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> registry</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionPolicyRegistry</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">buildRow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> policy </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">registry</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> policy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">buildReportRow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">transaction</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>حالا اگر نوع تراکنش تازه‌ای اضافه شود، الزاماً لازم نیست منطق گزارش، کارمزد و چند فایل قدیمی دیگر را باز کنیم. کافی است سیاست تازه را اضافه کنیم و آن را در نقطه‌ی ثبت سیاست‌ها معرفی کنیم.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>برای تشخیص نقطه‌ی توسعه، از خودتان بپرسید: «کدام بخش از این کد احتمالاً در آینده با نمونه‌های تازه گسترش پیدا می‌کند؟»</p><p>اگر پاسخ چیزی شبیه «نوع‌های تراکنش»، «روش‌های پرداخت»، «قالب‌های گزارش» یا «قانون‌های قیمت‌گذاری» است، احتمالاً با یک محور توسعه روبه‌رو هستید. بهتر است این محور یک نقطه‌ی مشخص برای افزودن رفتار تازه داشته باشد، نه اینکه در چند فایل پراکنده شود.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="بستهبودن-یعنی-محافظت-از-کد-قدیمی-نه-قفلکردن-آن">بسته‌بودن یعنی محافظت از کد قدیمی، نه قفل‌کردن آن<a href="https://example.com/blog/open-closed-principle-change-points#%D8%A8%D8%B3%D8%AA%D9%87%D8%A8%D9%88%D8%AF%D9%86-%DB%8C%D8%B9%D9%86%DB%8C-%D9%85%D8%AD%D8%A7%D9%81%D8%B8%D8%AA-%D8%A7%D8%B2-%DA%A9%D8%AF-%D9%82%D8%AF%DB%8C%D9%85%DB%8C-%D9%86%D9%87-%D9%82%D9%81%D9%84%DA%A9%D8%B1%D8%AF%D9%86-%D8%A2%D9%86" class="hash-link" aria-label="لینک مستقیم به بسته‌بودن یعنی محافظت از کد قدیمی، نه قفل‌کردن آن" title="لینک مستقیم به بسته‌بودن یعنی محافظت از کد قدیمی، نه قفل‌کردن آن" translate="no">​</a></h2>
<p>وقتی می‌گوییم کد باید برای تغییر بسته باشد، منظور این نیست که هیچ‌وقت نباید آن را تغییر داد. چنین چیزی نه ممکن است، نه مطلوب. کد زنده تغییر می‌کند. نیازهای محصول عوض می‌شود. مدل کسب‌وکار رشد می‌کند. قانون‌ها دقیق‌تر می‌شوند.</p>
<p>بسته‌بودن یعنی کدهای قدیمی و آزموده‌شده نباید برای هر قابلیت تازه، بی‌دلیل دوباره دست‌کاری شوند. اگر کدی بارها در تولید اجرا شده و رفتار قابل‌اعتمادی دارد، بهتر است افزودن قابلیت تازه تا حد ممکن از کنار آن انجام شود، نه از درون آن.</p>
<p>این تصویر را در نظر بگیرید: یک مسیر اصلی داریم که کاربران سال‌ها از آن عبور کرده‌اند. وقتی مسیر فرعی تازه‌ای لازم می‌شود، طراحی خوب این نیست که هر بار آسفالت مسیر اصلی را بکنیم و دوباره بسازیم. طراحی خوب این است که از جای مناسب، یک انشعاب کنترل‌شده بسازیم؛ طوری که مسیر اصلی پایدار بماند.</p>
<p>اصل باز و بسته دقیقاً همین نگاه را وارد کد می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="هزینهی-پنهان-تغییر-در-کد-قدیمی">هزینه‌ی پنهان تغییر در کد قدیمی<a href="https://example.com/blog/open-closed-principle-change-points#%D9%87%D8%B2%DB%8C%D9%86%D9%87%DB%8C-%D9%BE%D9%86%D9%87%D8%A7%D9%86-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1-%D8%AF%D8%B1-%DA%A9%D8%AF-%D9%82%D8%AF%DB%8C%D9%85%DB%8C" class="hash-link" aria-label="لینک مستقیم به هزینه‌ی پنهان تغییر در کد قدیمی" title="لینک مستقیم به هزینه‌ی پنهان تغییر در کد قدیمی" translate="no">​</a></h2>
<p>هر بار که برای افزودن رفتار تازه، کد قدیمی را تغییر می‌دهیم، چند هزینه‌ی پنهان پرداخت می‌کنیم.</p>
<p>باید دوباره بفهمیم آن کد چه می‌کرده است. باید اثر تغییر را روی رفتارهای قبلی بسنجیم. باید آزمون‌های مرتبط را اجرا کنیم. اگر آزمون کافی نداریم، باید با احتیاط بیشتری رفتارهای قدیمی را دستی بررسی کنیم. اگر کد قدیمی وابستگی‌های زیادی دارد، هر تغییر می‌تواند موجی از خطاهای کوچک بسازد.</p>
<p>این هزینه‌ها در یک تغییر کوچک شاید دیده نشوند. اما وقتی سامانه بزرگ می‌شود، تکرار همین الگو سرعت توسعه را پایین می‌آورد. تیم به‌جای ساختن قابلیت تازه، زمان زیادی را صرف این می‌کند که مطمئن شود چیزهای قبلی خراب نشده‌اند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آیا-همیشه-باید-نقطهی-توسعه-بسازیم">آیا همیشه باید نقطه‌ی توسعه بسازیم؟<a href="https://example.com/blog/open-closed-principle-change-points#%D8%A2%DB%8C%D8%A7-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%D8%A8%D8%A7%DB%8C%D8%AF-%D9%86%D9%82%D8%B7%D9%87%DB%8C-%D8%AA%D9%88%D8%B3%D8%B9%D9%87-%D8%A8%D8%B3%D8%A7%D8%B2%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به آیا همیشه باید نقطه‌ی توسعه بسازیم؟" title="لینک مستقیم به آیا همیشه باید نقطه‌ی توسعه بسازیم؟" translate="no">​</a></h2>
<p>نه. این هم یکی از دام‌های طراحی است.</p>
<p>اگر از همان روز نخست برای هر احتمال دور و مبهم یک سازوکار توسعه بسازیم، کد بیش از حد انتزاعی و سنگین می‌شود. اصل باز و بسته بهانه‌ای برای پیش‌بینی افراطی آینده نیست. قرار نیست برای نیازی که شاید هیچ‌وقت پیش نیاید، چند لایه‌ی تازه بسازیم.</p>
<p>طراحی خوب معمولاً از مشاهده‌ی تغییرهای واقعی می‌آید. وقتی یک نوع تغییر چند بار تکرار شد، یا می‌دانیم به‌زودی چند نمونه‌ی مشابه از آن خواهیم داشت، آن‌وقت ساختن نقطه‌ی توسعه معنا پیدا می‌کند.</p>
<p>برای نمونه، اگر فقط دو نوع تراکنش داریم و سال‌ها قرار نیست نوع تازه‌ای اضافه شود، یک شرط ساده شاید کافی باشد. اما اگر هر ماه نوع‌های تازه، گزارش‌های تازه یا قانون‌های تازه اضافه می‌شوند، ادامه‌دادن با شرط‌های پراکنده هزینه‌ساز می‌شود.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>اصل باز و بسته بیشتر از آنکه درباره‌ی «زیبایی ظاهری کد» باشد، درباره‌ی مدیریت ریسک تغییر است.</p><p>کدی که برای توسعه باز و برای تغییر بسته است، به تیم اجازه می‌دهد رفتار تازه را با دست‌کاری کمتر در مسیرهای قدیمی اضافه کند. این یعنی ریسک کمتر، آزمون‌پذیری بهتر، و اعتماد بیشتر هنگام انتشار.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-نمونه-از-ساختار-پوشه">یک نمونه از ساختار پوشه<a href="https://example.com/blog/open-closed-principle-change-points#%DB%8C%DA%A9-%D9%86%D9%85%D9%88%D9%86%D9%87-%D8%A7%D8%B2-%D8%B3%D8%A7%D8%AE%D8%AA%D8%A7%D8%B1-%D9%BE%D9%88%D8%B4%D9%87" class="hash-link" aria-label="لینک مستقیم به یک نمونه از ساختار پوشه" title="لینک مستقیم به یک نمونه از ساختار پوشه" translate="no">​</a></h2>
<p>اگر محور تغییر شما نوع تراکنش است، ساختار پوشه هم می‌تواند این محور را نشان دهد:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">transactions/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  policies/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    deposit-policy.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    withdraw-policy.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    transfer-policy.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    refund-policy.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  transaction-policy-registry.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  transaction-report-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  transaction-fee-service.ts</span><br></span></code></pre></div></div>
<p>در این ساختار، افزودن نوع تازه معمولاً به معنی افزودن یک فایل سیاست تازه است. البته شاید لازم باشد آن را در فهرست سیاست‌ها هم ثبت کنیم، اما این تغییر متمرکز و قابل‌پیش‌بینی است؛ نه پراکنده در چند بخش نامرتبط.</p>
<p>نمونه‌ی ثبت سیاست‌ها می‌تواند چنین باشد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> transactionPolicies</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> TransactionPolicy</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">DepositPolicy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">WithdrawPolicy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">TransferPolicy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RefundPolicy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><br></span></code></pre></div></div>
<p>ممکن است در بعضی پروژه‌ها همین ثبت دستی کافی باشد. در پروژه‌ای دیگر، شاید ثبت خودکار یا تزریق وابستگی مناسب‌تر باشد. اصل ماجرا ابزار نیست؛ اصل ماجرا این است که نقطه‌ی تغییر روشن و محدود باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/open-closed-principle-change-points#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای به‌کاربردن اصل باز و بسته، اول دنبال جاهایی بگردید که تغییرها تکراری و پراکنده شده‌اند. لازم نیست کل سامانه را بازطراحی کنید. یک محور تغییر واقعی را انتخاب کنید و همان را آرام مهار کنید.</p>
<p>چیزهایی که باید بررسی شوند:</p>
<ul>
<li class="">برای افزودن یک نوع تازه، چند فایل قدیمی باید تغییر کنند؟</li>
<li class="">آیا شرط مشابهی در چند جای کد تکرار شده است؟</li>
<li class="">آیا هر تغییر تازه، آزمون‌های رفتارهای قدیمی را هم درگیر می‌کند؟</li>
<li class="">آیا می‌توان رفتار تازه را با افزودن یک فایل یا یک پیاده‌سازی تازه اضافه کرد؟</li>
<li class="">آیا نقطه‌ی ثبت یا انتخاب رفتارها روشن، محدود و قابل‌آزمون است؟</li>
</ul>
<p>چیزهایی که نباید بی‌دلیل تغییر کنند:</p>
<ul>
<li class="">شرط‌های ساده‌ای که محور تغییر واقعی نیستند.</li>
<li class="">کدهای پایدار و کم‌تغییری که هنوز هزینه‌ای ایجاد نکرده‌اند.</li>
<li class="">نام‌گذاری‌ها و ساختارهای جاافتاده، فقط برای اینکه الگوی طراحی خاصی اعمال شود.</li>
<li class="">مسیرهای قدیمی و حساس تولید، بدون آزمون و بدون دلیل روشن.</li>
</ul>
<p>برای اعتبارسنجی تغییر، دست‌کم این گام‌ها را اجرا کنید:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">npm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run lint</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run typecheck</span><br></span></code></pre></div></div>
<p>اگر این فرمان‌ها در پروژه‌ی شما وجود ندارند، معادلشان را اجرا کنید: آزمون‌ها، بررسی سبک کد، و بررسی نوع‌ها یا قراردادهای اصلی. برای تغییری از جنس اصل باز و بسته، بهتر است علاوه بر آزمون‌های واحد، یک آزمون رفتاری هم داشته باشید که نشان دهد رفتارهای قدیمی پس از افزودن نوع تازه همچنان درست کار می‌کنند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/open-closed-principle-change-points#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>اصل باز و بسته نمی‌گوید کد را طوری بنویسیم که هیچ‌وقت تغییر نکند. چنین هدفی با واقعیت نرم‌افزار سازگار نیست. این اصل می‌گوید وقتی می‌دانیم یک محور رفتاری قرار است رشد کند، بهتر است رشد آن را از مسیر افزودن کد تازه ممکن کنیم، نه از مسیر دست‌کاری پی‌درپی کدهای قدیمی.</p>
<p>اگر هر قابلیت تازه شما را مجبور می‌کند چند فایل قدیمی را باز کنید، چند شرط مشابه را تغییر دهید و دوباره نگران رفتارهای قبلی شوید، احتمالاً نقطه‌های تغییر مهار نشده‌اند.</p>
<p>طراحی خوب همیشه به معنی ساختن لایه‌های بیشتر نیست. گاهی فقط یعنی تشخیص‌دادن اینکه کدام بخش باید پایدار بماند و کدام بخش باید جای امنی برای توسعه داشته باشد. اصل باز و بسته همین مرز را روشن می‌کند: مسیر اصلی را پایدار نگه دار، و قابلیت‌های تازه را از نقطه‌هایی اضافه کن که برای تغییر ساخته شده‌اند.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="SOLID" term="SOLID"/>
        <category label="اصل باز و بسته" term="اصل باز و بسته"/>
        <category label="تغییرپذیری" term="تغییرپذیری"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[مسئولیت یکتا یعنی یک دلیل برای تغییر، نه فقط یک تابع کوچک‌تر]]></title>
        <id>https://example.com/blog/single-responsibility-reason-to-change</id>
        <link href="https://example.com/blog/single-responsibility-reason-to-change"/>
        <updated>2026-04-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[توضیحی دقیق درباره‌ی اینکه اصل مسئولیت یکتا به محور تغییر مربوط است، نه فقط تعداد کارهایی که یک کلاس انجام می‌دهد.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="یک کلاس در مرکز که از چند سمت با خواسته‌های مالی، نمایشی، ذخیره‌سازی و گزارشی کشیده می‌شود." src="https://example.com/assets/images/single-responsibility-reason-to-change-f5b36fcf46263908a99632006cf8c87f.png" width="1731" height="909" class="img_m9Pm"></p>
<p>فرض کنید در یک سامانه‌ی مالی، کلاسی داریم که در نگاه نخست کار ساده‌ای انجام می‌دهد: اطلاعات یک پرداخت را می‌گیرد و آن را پردازش می‌کند. اما کمی که به درونش نگاه می‌کنیم، می‌بینیم این کلاس هم قانون مالی را محاسبه می‌کند، هم خروجی رابط برنامه‌نویسی کاربردی (API) را می‌سازد، هم داده را در پایگاه داده ذخیره می‌کند.</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">PaymentService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">process</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payment</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Payment</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> fee </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.01</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> finalAmount </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> fee</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> savedPayment </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">payments</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      fee</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      finalAmount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      status</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'done'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      fee</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">fee</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      finalAmount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">finalAmount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'پرداخت با موفقیت انجام شد'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کلاس شاید کوتاه باشد. شاید فقط یک تابع عمومی داشته باشد. شاید حتی در بازبینی کد کسی بگوید: «فعلاً که پیچیده نیست.» اما مسئله‌ی اصل مسئولیت یکتا از جای دیگری آغاز می‌شود. پرسش اصلی این نیست که کلاس چند تابع دارد؛ پرسش این است که این کلاس به چند دلیل متفاوت ممکن است تغییر کند.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>یادداشت</div><div class="admonitionContent_hiHs"><p>اصل مسئولیت یکتا (Single Responsibility Principle) می‌گوید هر واحد نرم‌افزاری باید فقط یک دلیل برای تغییر داشته باشد.</p><p>منظور از «واحد نرم‌افزاری» می‌تواند کلاس، ماژول، بسته یا حتی یک بخش بزرگ‌تر از سامانه باشد. نکته‌ی مهم این است که مسئولیت یکتا درباره‌ی «محور تغییر» حرف می‌زند، نه صرفاً اندازه‌ی کد.</p></div></div>
<p>در مثال بالا، دست‌کم سه گروه می‌توانند باعث تغییر این کلاس شوند.</p>
<p>گروه مالی ممکن است بگوید کارمزد از یک درصد به یک مدل پلکانی تغییر کند. تیم محصول ممکن است بخواهد متن پیام، شکل پاسخ یا نام فیلدهای خروجی تغییر کند. تیم زیرساخت یا داده ممکن است ساختار ذخیره‌سازی، جدول، تراکنش یا روش نوشتن در پایگاه داده را تغییر دهد.</p>
<p>همه‌ی این تغییرها واقعی و طبیعی‌اند. اما وقتی همگی در یک کلاس جمع می‌شوند، هر تغییر کوچک می‌تواند کلاس را به میدان کشمکش چند نیاز متفاوت تبدیل کند. این‌جا همان جایی است که مسئولیت یکتا معنا پیدا می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-دلیل-برای-تغییر-یعنی-چه">«یک دلیل برای تغییر» یعنی چه؟<a href="https://example.com/blog/single-responsibility-reason-to-change#%DB%8C%DA%A9-%D8%AF%D9%84%DB%8C%D9%84-%D8%A8%D8%B1%D8%A7%DB%8C-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به «یک دلیل برای تغییر» یعنی چه؟" title="لینک مستقیم به «یک دلیل برای تغییر» یعنی چه؟" translate="no">​</a></h2>
<p>در فصل هفتم کتاب، اصل مسئولیت یکتا با این برداشت توضیح داده می‌شود که هر ماژول باید در برابر یک گروه از تغییرها پاسخ‌گو باشد. این تعریف از ظاهر کد فاصله می‌گیرد و به نیروهایی نگاه می‌کند که از بیرون به کد فشار می‌آورند.</p>
<p>یک دلیل برای تغییر یعنی اگر این بخش از کد تغییر می‌کند، بتوانیم بگوییم این تغییر از یک محور مشخص آمده است؛ مثلاً تغییر در قانون مالی، تغییر در قرارداد خروجی، یا تغییر در شیوه‌ی ذخیره‌سازی.</p>
<p>مشکل وقتی پدید می‌آید که یک کلاس هم‌زمان نماینده‌ی چند محور تغییر باشد. در این حالت، با تغییر قانون مالی ممکن است قالب پاسخ هم ناخواسته آسیب ببیند. یا با تغییر روش ذخیره‌سازی، آزمون‌هایی بشکنند که در اصل باید فقط رفتار مالی را بررسی می‌کردند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>سوءبرداشت رایج این است که اصل مسئولیت یکتا یعنی «هر تابع باید خیلی کوچک باشد» یا «هر کلاس فقط یک متد داشته باشد».</p><p>کوچک‌بودن می‌تواند به خوانایی کمک کند، اما تضمین‌کننده‌ی طراحی درست نیست. ممکن است کلاسی بسیار کوتاه باشد، ولی همچنان چند دلیل مستقل برای تغییر داشته باشد. برعکس، ممکن است کلاسی چند تابع داشته باشد، اما همه‌ی آن‌ها در خدمت یک محور تغییر واحد باشند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="ذینفعان-دلیل-تغییر-میسازند">ذی‌نفعان، دلیل تغییر می‌سازند<a href="https://example.com/blog/single-responsibility-reason-to-change#%D8%B0%DB%8C%D9%86%D9%81%D8%B9%D8%A7%D9%86-%D8%AF%D9%84%DB%8C%D9%84-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1-%D9%85%DB%8C%D8%B3%D8%A7%D8%B2%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به ذی‌نفعان، دلیل تغییر می‌سازند" title="لینک مستقیم به ذی‌نفعان، دلیل تغییر می‌سازند" translate="no">​</a></h2>
<p>برای فهم بهتر این اصل، بهتر است به‌جای شمردن تابع‌ها، ذی‌نفعان را بشماریم. ذی‌نفع در این‌جا فقط کاربر نهایی نیست. هر گروهی که نیازش می‌تواند باعث تغییر کد شود، یک ذی‌نفع است.</p>
<p>در مثال پرداخت، این ذی‌نفعان را داریم:</p>
<table><thead><tr><th>ذی‌نفع</th><th>چیزی که می‌خواهد تغییر دهد</th><th>محور تغییر</th></tr></thead><tbody><tr><td>تیم مالی</td><td>قانون کارمزد، تخفیف، مالیات، تسویه</td><td>منطق مالی</td></tr><tr><td>تیم محصول</td><td>متن پیام، شکل پاسخ، نام فیلدها</td><td>نمایش و قرارداد خروجی</td></tr><tr><td>تیم داده یا زیرساخت</td><td>جدول، تراکنش، شیوه‌ی ذخیره‌سازی</td><td>ماندگاری داده</td></tr><tr><td>تیم گزارش‌گیری</td><td>داده‌های لازم برای گزارش‌ها</td><td>نیازهای تحلیلی</td></tr></tbody></table>
<p>اگر همه‌ی این نیازها در یک کلاس پیاده‌سازی شوند، آن کلاس دیگر فقط «پرداخت را پردازش نمی‌کند». بلکه به‌نوعی هم حسابدار است، هم مترجم خروجی، هم مسئول ذخیره‌سازی، هم تأمین‌کننده‌ی داده‌ی گزارش.</p>
<p>این ترکیب در آغاز شاید سریع و کم‌هزینه به نظر برسد، اما با بزرگ‌شدن سامانه، هزینه‌ی تغییر را بالا می‌برد. چون هر بار باید با احتیاط بررسی کنیم که تغییر یک بخش، بخش‌های دیگر را نشکند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="بازنویسی-با-محور-تغییر">بازنویسی با محور تغییر<a href="https://example.com/blog/single-responsibility-reason-to-change#%D8%A8%D8%A7%D8%B2%D9%86%D9%88%DB%8C%D8%B3%DB%8C-%D8%A8%D8%A7-%D9%85%D8%AD%D9%88%D8%B1-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1" class="hash-link" aria-label="لینک مستقیم به بازنویسی با محور تغییر" title="لینک مستقیم به بازنویسی با محور تغییر" translate="no">​</a></h2>
<p>برای جداکردن مسئولیت‌ها، لازم نیست کد را بی‌دلیل چندتکه کنیم. هدف این نیست که برای هر خط کد یک کلاس تازه بسازیم. هدف این است که محورهای تغییر از هم جدا شوند.</p>
<p>نسخه‌ی ساده‌تر و سالم‌تر می‌تواند چنین ساختاری داشته باشد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">PaymentCalculator</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">calculate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payment</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Payment</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> fee </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.01</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      fee</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      finalAmount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> fee</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">PaymentStore</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> result</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> PaymentCalculationResult</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">payments</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      fee</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">fee</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      finalAmount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">finalAmount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      status</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'done'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">PaymentResponseBuilder</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">build</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">savedPayment</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> SavedPayment</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      fee</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">fee</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      finalAmount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> savedPayment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">finalAmount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'پرداخت با موفقیت انجام شد'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اکنون می‌توانیم هماهنگ‌کننده‌ی اصلی را ساده‌تر نگه داریم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">PaymentService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> calculator</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> PaymentCalculator</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> store</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> PaymentStore</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> responseBuilder</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> PaymentResponseBuilder</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">process</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payment</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Payment</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">calculator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">calculate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payment</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> savedPayment </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payment</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">responseBuilder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">build</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">savedPayment</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این نسخه، هنوز همه‌چیز به هم مربوط است؛ پرداخت بدون محاسبه، ذخیره‌سازی و پاسخ کامل نمی‌شود. اما دلیل تغییر هر بخش روشن‌تر شده است.</p>
<p>اگر قانون مالی تغییر کند، بیشتر به <code>PaymentCalculator</code> مربوط است. اگر قرارداد خروجی تغییر کند، بیشتر به <code>PaymentResponseBuilder</code> مربوط است. اگر پایگاه داده یا روش ذخیره‌سازی تغییر کند، بیشتر <code>PaymentStore</code> درگیر می‌شود.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>اطلاع</div><div class="admonitionContent_hiHs"><p>جداسازی مسئولیت‌ها به معنی قطع‌کردن ارتباط بخش‌ها نیست. نرم‌افزار از همکاری بخش‌ها ساخته می‌شود. مسئله این است که هر بخش بداند در برابر کدام نوع تغییر پاسخ‌گوست.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="آیا-همیشه-باید-جدا-کنیم">آیا همیشه باید جدا کنیم؟<a href="https://example.com/blog/single-responsibility-reason-to-change#%D8%A2%DB%8C%D8%A7-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%AC%D8%AF%D8%A7-%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به آیا همیشه باید جدا کنیم؟" title="لینک مستقیم به آیا همیشه باید جدا کنیم؟" translate="no">​</a></h2>
<p>نه. این‌جا همان نقطه‌ای است که طراحی خوب از طراحی نمایشی جدا می‌شود.</p>
<p>اگر کدی کوچک است، عمر کوتاهی دارد، فقط یک مسیر مصرف دارد و هنوز محورهای تغییر آن روشن نیست، جداکردن زودهنگام ممکن است فقط پیچیدگی بسازد. اصل مسئولیت یکتا قرار نیست بهانه‌ای برای تولید پوشه‌ها، کلاس‌ها و نام‌های بیشتر باشد.</p>
<p>اما اگر نشانه‌های زیر را می‌بینیم، بهتر است مکث کنیم:</p>
<ul>
<li class="">تغییرهای نامرتبط مدام به یک فایل می‌خورند.</li>
<li class="">آزمون‌های مربوط به یک رفتار، به دلیل تغییر در رفتار دیگری می‌شکنند.</li>
<li class="">توضیح‌دادن کار یک کلاس با یک جمله‌ی روشن سخت شده است.</li>
<li class="">چند تیم یا چند نیاز مستقل روی یک بخش از کد فشار می‌آورند.</li>
<li class="">برای تغییر قالب خروجی مجبور می‌شویم منطق مالی را هم لمس کنیم.</li>
<li class="">برای تغییر ذخیره‌سازی مجبور می‌شویم آزمون‌های مربوط به پاسخ را بازنویسی کنیم.</li>
</ul>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نکته</div><div class="admonitionContent_hiHs"><p>برای یافتن محور تغییر، از خودتان بپرسید: «چه کسی ممکن است از من بخواهد این کد را تغییر دهم، و چرا؟»</p><p>اگر پاسخ‌ها از چند جنس متفاوت‌اند، احتمالاً چند مسئولیت در یک جا جمع شده‌اند. مثلاً «تیم مالی برای تغییر کارمزد»، «تیم محصول برای تغییر خروجی»، و «تیم زیرساخت برای تغییر ذخیره‌سازی» سه محور تغییر متفاوت‌اند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مسئولیت-یکتا-و-آزمونپذیری">مسئولیت یکتا و آزمون‌پذیری<a href="https://example.com/blog/single-responsibility-reason-to-change#%D9%85%D8%B3%D8%A6%D9%88%D9%84%DB%8C%D8%AA-%DB%8C%DA%A9%D8%AA%D8%A7-%D9%88-%D8%A2%D8%B2%D9%85%D9%88%D9%86%D9%BE%D8%B0%DB%8C%D8%B1%DB%8C" class="hash-link" aria-label="لینک مستقیم به مسئولیت یکتا و آزمون‌پذیری" title="لینک مستقیم به مسئولیت یکتا و آزمون‌پذیری" translate="no">​</a></h2>
<p>وقتی مسئولیت‌ها در هم قاطی می‌شوند، آزمون‌ها هم معمولاً مبهم می‌شوند. برای بررسی قانون کارمزد، ناگهان باید پایگاه داده را آماده کنیم. برای بررسی قالب پاسخ، باید محاسبه‌ی مالی هم درست اجرا شود. برای آزمودن ذخیره‌سازی، به متن پیام خروجی وابسته می‌شویم.</p>
<p>این وابستگی‌ها نشانه‌ی خوبی نیستند. آزمون خوب باید تا حد ممکن به همان رفتاری بچسبد که می‌خواهد بررسی کند.</p>
<p>برای نمونه، آزمون قانون مالی می‌تواند فقط محاسبه را بررسی کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'calculates payment fee and final amount'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> calculator </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">PaymentCalculator</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> calculator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">calculate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'user-1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toEqual</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    fee</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    finalAmount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">99_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>این آزمون نه به پایگاه داده نیاز دارد، نه به قالب پاسخ. چون محور تغییر آن فقط قانون مالی است.</p>
<p>از سوی دیگر، آزمون خروجی می‌تواند جداگانه بررسی کند که پاسخ نهایی چه شکلی دارد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'builds payment response'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> builder </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">PaymentResponseBuilder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">build</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'payment-1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    fee</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    finalAmount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">99_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toBe</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'پرداخت با موفقیت انجام شد'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">finalAmount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toBe</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">99_000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre></div></div>
<p>این جداسازی باعث نمی‌شود تعداد آزمون‌ها الزاماً کمتر شود، اما باعث می‌شود هر آزمون دلیل روشن‌تری برای شکست داشته باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="ساختار-پوشه-هم-باید-از-محور-تغییر-پیروی-کند">ساختار پوشه هم باید از محور تغییر پیروی کند<a href="https://example.com/blog/single-responsibility-reason-to-change#%D8%B3%D8%A7%D8%AE%D8%AA%D8%A7%D8%B1-%D9%BE%D9%88%D8%B4%D9%87-%D9%87%D9%85-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A7%D8%B2-%D9%85%D8%AD%D9%88%D8%B1-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1-%D9%BE%DB%8C%D8%B1%D9%88%DB%8C-%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به ساختار پوشه هم باید از محور تغییر پیروی کند" title="لینک مستقیم به ساختار پوشه هم باید از محور تغییر پیروی کند" translate="no">​</a></h2>
<p>گاهی کد را از نظر کلاس‌ها جدا می‌کنیم، اما در ساختار پوشه دوباره همه‌چیز را بر اساس نوع فنی فایل‌ها می‌چینیم:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">payments/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  controllers/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  services/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  repositories/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  utils/</span><br></span></code></pre></div></div>
<p>این ساختار همیشه بد نیست، اما ممکن است محورهای واقعی تغییر را پنهان کند. در بعضی سامانه‌ها، ساختار زیر خواناتر است:</p>
<div class="language-txt codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-txt codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">payments/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  calculation/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    payment-calculator.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    payment-calculator.test.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  persistence/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    payment-store.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  presentation/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    payment-response-builder.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  payment-service.ts</span><br></span></code></pre></div></div>
<p>در این ساختار، روشن‌تر است که هر بخش برای چه نوع تغییری ساخته شده است. البته این فقط یک نمونه است، نه نسخه‌ی همیشگی. در پروژه‌های گوناگون، نام‌گذاری و مرزبندی باید با زبان و معماری همان پروژه سازگار باشد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>هشدار</div><div class="admonitionContent_hiHs"><p>نباید اصل مسئولیت یکتا را با «لایه‌بندی مکانیکی» اشتباه بگیریم. اینکه هر چیزی را به controller، service و repository تقسیم کنیم، به‌تنهایی طراحی خوب نمی‌سازد. اگر مرزها بر اساس دلیل تغییر نباشند، فقط پیچیدگی را جابه‌جا کرده‌ایم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/single-responsibility-reason-to-change#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای به‌کاربردن اصل مسئولیت یکتا، سراغ بازنویسی هیجانی نروید. نخست رفتار فعلی را بفهمید و بعد مرزها را آرام و قابل‌اعتبارسنجی تغییر دهید.</p>
<p>چیزهایی که باید بررسی شوند:</p>
<ul>
<li class="">این کلاس یا ماژول به چه دلیل‌هایی در چند ماه گذشته تغییر کرده است؟</li>
<li class="">چه تیم‌ها یا نقش‌هایی درخواست تغییر روی آن داده‌اند؟</li>
<li class="">آیا تغییر در قانون کسب‌وکار باعث تغییر در قالب خروجی یا ذخیره‌سازی شده است؟</li>
<li class="">آیا آزمون‌ها به خاطر جزئیاتی می‌شکنند که به موضوع اصلی آزمون ربطی ندارد؟</li>
<li class="">آیا می‌توان کار این بخش را با یک جمله‌ی دقیق توضیح داد؟</li>
</ul>
<p>چیزهایی که نباید بی‌دلیل تغییر کنند:</p>
<ul>
<li class="">نام‌ها و ساختارهایی که در پروژه جا افتاده‌اند و هنوز درد واقعی ایجاد نکرده‌اند.</li>
<li class="">کدهای کوچک و کم‌تغییری که هنوز محور تغییر جداگانه ندارند.</li>
<li class="">قراردادهای عمومی پاسخ، مگر اینکه تغییرشان واقعاً لازم باشد.</li>
<li class="">ساختار پایگاه داده، فقط برای اینکه کد «تمیزتر» به نظر برسد.</li>
</ul>
<p>برای اعتبارسنجی تغییر، بسته به پروژه، دست‌کم این گام‌ها را اجرا کنید:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">npm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run lint</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">npm run typecheck</span><br></span></code></pre></div></div>
<p>اگر پروژه‌ی شما این فرمان‌ها را ندارد، معادل همان‌ها را اجرا کنید: آزمون‌ها، بررسی سبک کد، و بررسی نوع‌ها یا قراردادهای اصلی. هدف این است که مطمئن شوید جداسازی مسئولیت‌ها رفتار بیرونی سامانه را تغییر نداده است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/single-responsibility-reason-to-change#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>اصل مسئولیت یکتا درباره‌ی کوچک‌کردن افراطی کلاس‌ها نیست. درباره‌ی این است که بفهمیم هر بخش از نرم‌افزار زیر فشار کدام تغییرها قرار دارد و آیا چند فشار مستقل را بی‌دلیل در یک جا جمع کرده‌ایم یا نه.</p>
<p>یک کلاس می‌تواند کوتاه باشد و همچنان چند مسئولیت داشته باشد. یک ماژول می‌تواند چند تابع داشته باشد و همچنان از نظر طراحی منسجم باشد. معیار اصلی این است: وقتی تغییری رخ می‌دهد، آیا دلیل آن تغییر روشن، محدود و قابل‌ردیابی است؟</p>
<p>اگر پاسخ مثبت باشد، کد نه‌تنها خواناتر است، بلکه تغییرپذیرتر هم می‌شود. و در نرم‌افزار واقعی، جایی که نیازهای مالی، محصولی، فنی و گزارشی مدام تغییر می‌کنند، همین تغییرپذیری کنترل‌شده یکی از مهم‌ترین نشانه‌های طراحی خوب است.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="SOLID" term="SOLID"/>
        <category label="مسئولیت یکتا" term="مسئولیت یکتا"/>
        <category label="طراحی نرم‌افزار" term="طراحی نرم‌افزار"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[بیشتر باگ‌ها از جایی آغاز می‌شوند که وضعیت عوض می‌شود]]></title>
        <id>https://example.com/blog/functional-programming-and-state</id>
        <link href="https://example.com/blog/functional-programming-and-state"/>
        <updated>2026-04-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[تحلیلی درباره‌ی نقش وضعیت تغییرپذیر در پیچیدگی نرم‌افزار و اینکه تابعی‌نویسی چه هشداری برای معماری دارد.]]></summary>
        <content type="html"><![CDATA[<p>در یک سامانه‌ی مالی، همه‌چیز ساده به نظر می‌رسید. کاربر درخواست برداشت می‌داد، موجودی کیف پول بررسی می‌شد، مبلغ کم می‌شد و نتیجه در پایگاه داده ذخیره می‌شد. اما گاهی دو درخواست نزدیک به هم، هر دو موفق می‌شدند و موجودی نهایی با چیزی که انتظار می‌رفت نمی‌خواند. در جایی دیگر، پردازش‌گر پیام یک رویداد را دوبار می‌دید و دوبار روی وضعیت سفارش اثر می‌گذاشت. در نمونه‌ای دیگر، یک کش قدیمی باعث می‌شد تصمیمی بر اساس داده‌ای گرفته شود که دیگر معتبر نبود.</p>
<p>وجه مشترک این خطاها معمولاً یک چیز است: وضعیت تغییرپذیر. همان‌جایی که داده‌ای مشترک وجود دارد و چند مسیر مختلف می‌توانند آن را بخوانند، تغییر دهند، یا بر اساس نسخه‌ای قدیمی از آن تصمیم بگیرند. بسیاری از خطاهای دشوار نرم‌افزاری نه از الگوریتم‌های عجیب، بلکه از همین نقطه آغاز می‌شوند: از جایی که وضعیت عوض می‌شود و ما کنترل کافی روی این تغییر نداریم.</p>
<p><img decoding="async" loading="lazy" alt="چند جریان هم‌زمان که روی یک وضعیت مشترک اثر می‌گذارند و در کنار آن، نسخه‌ای آرام‌تر با داده‌ی تغییرناپذیر دیده می‌شود." src="https://example.com/assets/images/functional-programming-and-state-a2aeb31d0994c425bcaf794b1317c0c1.png" width="1731" height="909" class="img_m9Pm"></p>
<p>فصل ششم کتاب <em>معماری تمیز</em> (Clean Architecture) درباره‌ی تابعی‌نویسی (Functional Programming) است. اما پیام مهم این فصل این نیست که همه باید از فردا سامانه را با سبک تابعی بازنویسی کنند. نکته‌ی اصلی، هشداری معماری است: وضعیت تغییرپذیر، منبع مهمی از پیچیدگی است و هرجا بی‌محابا وارد سامانه شود، باید منتظر هزینه‌هایش باشیم.</p>
<p>تابعی‌نویسی در اینجا بیشتر از آن‌که یک سلیقه‌ی زبانی باشد، یک یادآوری فنی است: تا جای ممکن، از تغییر وضعیت فاصله بگیر؛ اگر ناچار به تغییر وضعیت هستی، آن را محدود، آشکار و قابل‌کنترل نگه دار.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>وضعیت تغییرپذیر همیشه فقط یک متغیر ساده نیست</div><div class="admonitionContent_hiHs"><p>گاهی وضعیت تغییرپذیر خودش را در قالب موجودی کیف پول، وضعیت سفارش، شمارنده‌ی کش، صف پردازش‌نشده، نشست کاربر یا حتی یک شیء در حافظه نشان می‌دهد. هرجا چند بخش از سامانه بتوانند روی یک داده اثر بگذارند، خطر پیچیدگی بالا می‌رود.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-وضعیت-تغییرپذیر-مسئلهساز-است">چرا وضعیت تغییرپذیر مسئله‌ساز است؟<a href="https://example.com/blog/functional-programming-and-state#%DA%86%D8%B1%D8%A7-%D9%88%D8%B6%D8%B9%DB%8C%D8%AA-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1%D9%BE%D8%B0%DB%8C%D8%B1-%D9%85%D8%B3%D8%A6%D9%84%D9%87%D8%B3%D8%A7%D8%B2-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به چرا وضعیت تغییرپذیر مسئله‌ساز است؟" title="لینک مستقیم به چرا وضعیت تغییرپذیر مسئله‌ساز است؟" translate="no">​</a></h2>
<p>وقتی داده‌ای تغییر می‌کند، باید چند چیز را هم‌زمان درست نگه داریم: ترتیب تغییرها، دیده‌شدن تغییرها، سازگاری تصمیم‌ها با آخرین وضعیت، و هماهنگی میان چند بخش از سامانه. این کار تا وقتی فقط یک مسیر ساده داریم، قابل‌مدیریت به نظر می‌رسد. اما کافی است چند درخواست موازی، یک پردازش پس‌زمینه، یک کش یا چند رویداد بیرونی وارد ماجرا شوند تا مسئله سخت‌تر شود.</p>
<p>در کیف پول، اگر دو درخواست برداشت تقریباً هم‌زمان بیایند و هر دو بر اساس یک موجودی یکسان تصمیم بگیرند، ممکن است هر دو مجاز به نظر برسند. در پردازش سفارش، اگر یک رویداد دوبار مصرف شود و تغییر وضعیت بر همان سفارش دوباره اعمال شود، نتیجه خراب می‌شود. در کش، اگر داده‌ای به‌روز نشده باشد، ممکن است منطق تصمیم‌گیری بر پایه‌ی واقعیتی منقضی بنا شود.</p>
<p>مشکل این نیست که وضعیت به خودی خود بد است. مشکل این است که تغییرپذیری، بار ذهنی زیادی می‌آورد. باید همیشه بپرسیم: این مقدار الان چه ارزشی دارد؟ چه کسی آن را عوض کرده؟ آیا این نسخه آخرین نسخه است؟ اگر هم‌زمان دو مسیر آن را تغییر دهند چه می‌شود؟ اگر دوبار همان پیام را ببینیم چه؟</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="تابعینویسی-چه-درسی-برای-معماری-دارد">تابعی‌نویسی چه درسی برای معماری دارد؟<a href="https://example.com/blog/functional-programming-and-state#%D8%AA%D8%A7%D8%A8%D8%B9%DB%8C%D9%86%D9%88%DB%8C%D8%B3%DB%8C-%DA%86%D9%87-%D8%AF%D8%B1%D8%B3%DB%8C-%D8%A8%D8%B1%D8%A7%DB%8C-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D8%AF%D8%A7%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به تابعی‌نویسی چه درسی برای معماری دارد؟" title="لینک مستقیم به تابعی‌نویسی چه درسی برای معماری دارد؟" translate="no">​</a></h2>
<p>تابعی‌نویسی در ساده‌ترین بیان، به ما می‌گوید تا می‌توانی از داده‌ی تغییرناپذیر و تابع‌های بدون اثر جانبی استفاده کن. این حرف در معماری مهم است، چون بخشی از دشواری سامانه‌ها دقیقاً از اثرهای جانبی و تغییر وضعیت می‌آید.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>هشدار اصلی تابعی‌نویسی</div><div class="admonitionContent_hiHs"><p>پیام مهم تابعی‌نویسی این نیست که همه‌چیز باید خالص و ریاضی‌وار باشد؛ پیامش این است که هر تغییر وضعیت باید به‌عنوان یک نقطه‌ی پرخطر دیده شود. هرچه بتوانیم بخش بیشتری از منطق را بدون تکیه بر وضعیت مشترک و تغییرپذیر بنویسیم، فهم، آزمون و تغییر آن ساده‌تر می‌شود.</p></div></div>
<p>برای نمونه، قانون محاسبه‌ی کارمزد برداشت اگر فقط از ورودی‌اش استفاده کند و عددی برگرداند، فهم و آزمون آن ساده است:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">calculateWithdrawalFee</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">&lt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_000_000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5_000</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">floor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.01</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اما اگر همین منطق به چند وضعیت بیرونی وابسته شود، مثل تاریخ روز، تنظیمات سراسری قابل‌تغییر، کش محلی و یک شمارنده‌ی مشترک، فهمیدن رفتار آن سخت‌تر می‌شود. هرچه منطق بیشتری را بتوانیم به این شکل ورودی-خروجی روشن نزدیک کنیم، از نظر معماری در جای امن‌تری هستیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="ماجرا-حذف-کامل-وضعیت-نیست">ماجرا حذف کامل وضعیت نیست<a href="https://example.com/blog/functional-programming-and-state#%D9%85%D8%A7%D8%AC%D8%B1%D8%A7-%D8%AD%D8%B0%D9%81-%DA%A9%D8%A7%D9%85%D9%84-%D9%88%D8%B6%D8%B9%DB%8C%D8%AA-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به ماجرا حذف کامل وضعیت نیست" title="لینک مستقیم به ماجرا حذف کامل وضعیت نیست" translate="no">​</a></h2>
<p>گاهی وقتی درباره‌ی تابعی‌نویسی حرف می‌زنیم، بحث خیلی سریع به واژه‌هایی مثل تابع خالص و بی‌حالتی محدود می‌شود. این واژه‌ها مهم‌اند، اما اگر بحث در همان سطح بماند، پیام معماری گم می‌شود. در یک سامانه‌ی واقعی، همه‌چیز را نمی‌توان بدون وضعیت ساخت. بالاخره باید چیزی ذخیره شود، رویدادی ثبت شود، پیامی فرستاده شود و وضعیت کسب‌وکار تغییر کند.</p>
<p>پس پرسش درست این نیست که آیا وضعیت را کاملاً حذف کنیم. پرسش بهتر این است: چگونه اثرهای ناشی از وضعیت را محدود کنیم؟ می‌توانیم منطق تصمیم‌گیری را از بخش تغییر وضعیت جدا کنیم، داده‌ها را تا حد امکان به‌شکل ورودی و خروجی روشن جابه‌جا کنیم، وضعیت مشترک را کم کنیم، و نقطه‌هایی را که واقعاً وضعیت را تغییر می‌دهند محدود و قابل‌مشاهده نگه داریم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مثال-پردازش-پیام-و-وضعیت-سفارش">مثال: پردازش پیام و وضعیت سفارش<a href="https://example.com/blog/functional-programming-and-state#%D9%85%D8%AB%D8%A7%D9%84-%D9%BE%D8%B1%D8%AF%D8%A7%D8%B2%D8%B4-%D9%BE%DB%8C%D8%A7%D9%85-%D9%88-%D9%88%D8%B6%D8%B9%DB%8C%D8%AA-%D8%B3%D9%81%D8%A7%D8%B1%D8%B4" class="hash-link" aria-label="لینک مستقیم به مثال: پردازش پیام و وضعیت سفارش" title="لینک مستقیم به مثال: پردازش پیام و وضعیت سفارش" translate="no">​</a></h2>
<p>فرض کنید سامانه‌ای داریم که با دریافت پیام «پرداخت موفق»، وضعیت سفارش را تغییر می‌دهد. شکل خام ماجرا ممکن است این باشد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handlePaymentSucceeded</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ordersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orderId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'paid'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'paid'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">paidAt </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Date</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ordersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">save</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> inventoryService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">reserve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> notificationService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sendPaymentSuccess</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد چند خطر پنهان دارد. اگر پیام دوبار مصرف شود چه؟ اگر پس از ذخیره‌ی سفارش، رزرو موجودی شکست بخورد چه؟ اگر هم‌زمان مسیر دیگری هم بخواهد وضعیت سفارش را عوض کند چه؟ اگر بخواهیم منطق اصلی را بدون پایگاه داده و سرویس‌های بیرونی آزمون کنیم چه؟</p>
<p>نسخه‌ی معماری‌پذیرتر می‌تواند تصمیم را از اجرای اثر جانبی جدا کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">PaymentSucceededResult</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'ignore'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'mark-as-paid'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> orderId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> OrderItem</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">decidePaymentSucceeded</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> Order</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> PaymentSucceededResult </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">status </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'paid'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'ignore'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'mark-as-paid'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    orderId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و بعد بخش اجرایی بر اساس این تصمیم عمل کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handlePaymentSucceeded</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ordersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">message</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orderId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> decision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">decidePaymentSucceeded</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">decision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'ignore'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ordersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">markAsPaid</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">decision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orderId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> inventoryService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">reserve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">decision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> notificationService</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sendPaymentSuccess</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">decision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>هنوز وضعیت تغییر می‌کند. هنوز اثر جانبی وجود دارد. اما تصمیم اصلی روشن‌تر، آزمون‌پذیرتر و کم‌وابسته‌تر شده است. این همان جایی است که درس تابعی‌نویسی به معماری کمک می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="دادهی-تغییرناپذیر-چرا-آرامتر-است">داده‌ی تغییرناپذیر چرا آرام‌تر است؟<a href="https://example.com/blog/functional-programming-and-state#%D8%AF%D8%A7%D8%AF%D9%87%DB%8C-%D8%AA%D8%BA%DB%8C%DB%8C%D8%B1%D9%86%D8%A7%D9%BE%D8%B0%DB%8C%D8%B1-%DA%86%D8%B1%D8%A7-%D8%A2%D8%B1%D8%A7%D9%85%D8%AA%D8%B1-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به داده‌ی تغییرناپذیر چرا آرام‌تر است؟" title="لینک مستقیم به داده‌ی تغییرناپذیر چرا آرام‌تر است؟" translate="no">​</a></h2>
<p>وقتی داده را تغییرناپذیر در نظر می‌گیریم، به جای این‌که یک شیء در چند جای سامانه دست‌کاری شود، نسخه‌ای تازه از آن ساخته می‌شود یا تصمیم فقط بر اساس ورودی موجود گرفته می‌شود. این روش فهم جریان داده را ساده‌تر می‌کند، اثر جانبی پنهان را کمتر می‌کند، آزمون‌ها را روشن‌تر می‌کند و بازتولید خطاها را آسان‌تر می‌کند.</p>
<p>برای نمونه، اگر یک تابع فقط سفارش را بگیرد و بگوید وضعیت بعدی چیست، راحت‌تر می‌توان آن را آزمود تا حالتی که همان تابع هم وضعیت را از پایگاه داده بخواند، هم آن را عوض کند، هم پیام بفرستد، هم زمان ثبت کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="این-نگاه-کجا-به-درد-معماری-میخورد">این نگاه کجا به درد معماری می‌خورد؟<a href="https://example.com/blog/functional-programming-and-state#%D8%A7%DB%8C%D9%86-%D9%86%DA%AF%D8%A7%D9%87-%DA%A9%D8%AC%D8%A7-%D8%A8%D9%87-%D8%AF%D8%B1%D8%AF-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D9%85%DB%8C%D8%AE%D9%88%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به این نگاه کجا به درد معماری می‌خورد؟" title="لینک مستقیم به این نگاه کجا به درد معماری می‌خورد؟" translate="no">​</a></h2>
<p>پیام تابعی‌نویسی فقط برای بخش‌های الگوریتمی یا محاسباتی نیست. در معماری هم مهم است، چون به ما کمک می‌کند جاهایی را که باید حساس‌تر باشیم، بهتر ببینیم. در قوانین مالی و محاسباتی، بهتر است منطق تا حد ممکن مستقل از وضعیت مشترک باشد. در کاربردهایی که هم‌زمانی دارند، باید روی نقاط تغییر وضعیت حساس‌تر باشیم. در پردازش پیام، باید به تکرار پیام و اجرای چندباره فکر کنیم. در کش، باید بدانیم داده‌ی ما چه زمانی کهنه می‌شود.</p>
<p>این نگاه کمک می‌کند منطق را به دو دسته تقسیم کنیم: بخشی که می‌تواند روشن، محلی و تقریباً تابعی بماند؛ و بخشی که ناچار است با دنیای بیرون، پایگاه داده، زمان، شبکه و تغییر وضعیت سروکار داشته باشد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>چه چیزهایی را جدی بررسی کنیم؟</div><div class="admonitionContent_hiHs"><p>هرجا داده‌ای مشترک داریم، هرجا چند پردازش هم‌زمان بر یک وضعیت اثر می‌گذارند، هرجا دوباره‌اجراشدن ممکن است، یا هرجا تصمیم و اثر جانبی در هم قاطی شده‌اند، همان‌جا بهترین نقطه برای به‌کارگیری نگاه تابعی است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/functional-programming-and-state#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>اگر می‌خواهی از این نگاه در یک پروژه‌ی واقعی استفاده کنی، اول دنبال جاهایی بگرد که وضعیت زیاد عوض می‌شود یا چند بخش مختلف روی آن اثر می‌گذارند. موجودی کیف پول، وضعیت سفارش، نتیجه‌ی پردازش پیام، کش و شمارنده‌ها معمولاً نقطه‌های خوبی برای بررسی‌اند.</p>
<p>بعد ببین کدام بخش از منطق را می‌توانی از اثرهای جانبی جدا کنی. شاید بتوانی تصمیم‌گیری را در یک تابع مستقل بیاوری. شاید بتوانی ورودی و خروجی را روشن‌تر کنی. شاید بتوانی تغییر وضعیت را به چند نقطه‌ی محدودتر منتقل کنی. شاید لازم باشد داده‌ی مشترک را کمتر کنی یا طراحی را در برابر اجرای دوباره مقاوم‌تر کنی.</p>
<p>در مقابل، بی‌دلیل دنبال بازنویسی کامل نرو. اگر بخشی از سامانه ساده است و مسئله‌ی هم‌زمانی یا وضعیت پیچیده ندارد، شاید هزینه‌ی تغییر بیشتر از فایده‌ی آن باشد. همچنین صرفاً با حذف واژه‌ی کلاس یا افزودن چند تابع، مسئله حل نمی‌شود. هدف، کم‌کردن پیچیدگی ناشی از وضعیت است، نه تغییر ظاهری سبک کد.</p>
<p>برای اعتبارسنجی هم فقط به اجرای دستی اکتفا نکن. برای بخش‌هایی که منطقشان را جدا کرده‌ای، آزمون‌های روشن بنویس. اگر پروژه از بررسی نوع و ساخت پشتیبانی می‌کند، فرمان‌های معمول را اجرا کن:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">pnpm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm typecheck</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm build</span><br></span></code></pre></div></div>
<p>اگر آزمون خودکار کافی نداری، دست‌کم چند سناریوی حساس را ثبت کن: دوباره‌اجراشدن یک پیام، دو درخواست هم‌زمان، خواندن از کش قدیمی، یا تغییر وضعیت در چند مرحله.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/functional-programming-and-state#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>بیشتر باگ‌ها از جایی آغاز می‌شوند که وضعیت عوض می‌شود. این جمله شاید مطلق نباشد، اما هشدار مهمی در خود دارد. هرجا وضعیت تغییرپذیر وارد ماجرا می‌شود، باید منتظر هزینه‌های بیشتری در فهم، آزمون، هم‌زمانی و تغییر باشیم.</p>
<p>تابعی‌نویسی به ما نمی‌گوید که حتماً باید تمام سامانه را به سبک خاصی بازنویسی کنیم. اما به ما یاد می‌دهد که تغییر وضعیت را ساده و بی‌خطر فرض نکنیم. این نگاه برای معماری بسیار ارزشمند است: بخش‌های تصمیم‌گیری را تا جای ممکن روشن‌تر و مستقل‌تر نگه داریم، اثرهای جانبی را محدود کنیم، و نقاط تغییر وضعیت را آگاهانه طراحی کنیم.</p>
<p>پرسش خوب این نیست که «آیا کدمان تابعی است؟» پرسش بهتر این است: «آیا می‌دانیم وضعیت در کجای سامانه تغییر می‌کند، چه کسی آن را تغییر می‌دهد، و این تغییر چه پیچیدگی‌ای وارد طراحی می‌کند؟» پاسخ این پرسش، همان جایی است که درس تابعی‌نویسی به درد معماری می‌خورد.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="تابعی‌نویسی" term="تابعی‌نویسی"/>
        <category label="وضعیت" term="وضعیت"/>
        <category label="همزمانی" term="همزمانی"/>
        <category label="معماری تمیز" term="معماری تمیز"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[شی‌گرایی یعنی کنترل وابستگی، نه فقط ساختن کلاس]]></title>
        <id>https://example.com/blog/oop-is-dependency-control</id>
        <link href="https://example.com/blog/oop-is-dependency-control"/>
        <updated>2026-03-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[توضیحی درباره‌ی اینکه قدرت مهم شی‌گرایی در معماری، امکان وارونه کردن جهت وابستگی‌هاست.]]></summary>
        <content type="html"><![CDATA[<p>پروژه از بیرون کاملاً شی‌گرا به نظر می‌رسید. تقریباً برای هر مفهوم، یک کلاس وجود داشت: <code>UserService</code>، <code>OrderManager</code>، <code>PaymentHandler</code>، <code>ReportGenerator</code> و چندین کلاس دیگر. کدها دیگر مثل یک اسکریپت بلند و تخت نبودند. هر چیز، جایی داشت و هر فایل، اسمی آشنا. اما وقتی تیم خواست پایگاه داده را برای آزمون‌ها جایگزین کند، یا بخشی از منطق پرداخت را بدون فریم‌ورک وب اجرا کند، واقعیت خودش را نشان داد: کلاس‌ها زیاد بودند، اما وابستگی‌ها همچنان به دیتابیس، فریم‌ورک و جزئیات بیرونی قفل شده بودند.</p>
<p>در عمل، بسیاری از کلاس‌ها فقط ظرف‌هایی شیک‌تر برای همان کدهای قبلی بودند. سازنده‌ی کلاس‌ها پر از اتصال مستقیم به ابزارها بود. متدهای اصلی، مدل‌های پایگاه داده را می‌شناختند. خطاهای دامنه با کدهای وضعیت HTTP قاطی شده بود. آزمون یک رفتار ساده، نیازمند راه‌اندازی نیمی از سامانه بود. پروژه «کلاس» داشت، اما از توان اصلی شی‌گرایی برای معماری استفاده نمی‌کرد.</p>
<p><img decoding="async" loading="lazy" alt="چند کلاس که به‌جای وابستگی مستقیم به جزئیات، از راه یک مرز انتزاعی با هم ارتباط دارند." src="https://example.com/assets/images/oop-is-dependency-control-481b99ec0f3bd3800157ea132cc67d1b.png" width="1536" height="1024" class="img_m9Pm"></p>
<p>فصل پنجم کتاب <em>معماری تمیز</em> (Clean Architecture) درباره‌ی برنامه‌نویسی شی‌گرا (Object-Oriented Programming) است، اما برداشت آن از شی‌گرایی با برداشت رایج «کلاس بساز، شیء بساز، متد بنویس» فرق دارد. در نگاه معماری، قدرت مهم شی‌گرایی فقط در بسته‌بندی داده و رفتار نیست. نقطه‌ی مهم‌تر، چندریختی و توان کنترل جهت وابستگی‌هاست.</p>
<p>اگر شی‌گرایی را فقط به ساختن کلاس تقلیل بدهیم، خیلی زود به کدی می‌رسیم که ظاهرش منظم است، اما از نظر معماری همچنان شکننده است. کلاس‌ها ممکن است زیاد شوند، اما اگر هسته‌ی سامانه هنوز جزئیات بیرونی را بشناسد، وابستگی‌ها وارونه نشده‌اند. فقط نام‌ها عوض شده‌اند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>کلاس‌محوری سطحی</div><div class="admonitionContent_hiHs"><p>داشتن کلاس‌های زیاد، به‌تنهایی نشانه‌ی طراحی شی‌گرا یا معماری خوب نیست. اگر کلاس‌های اصلی همچنان مستقیم به دیتابیس، فریم‌ورک، سرویس بیرونی یا قالب پاسخ وابسته‌اند، فقط کد را در ظرف‌های بیشتری ریخته‌ایم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مشکل-از-کجاست">مشکل از کجاست؟<a href="https://example.com/blog/oop-is-dependency-control#%D9%85%D8%B4%DA%A9%D9%84-%D8%A7%D8%B2-%DA%A9%D8%AC%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به مشکل از کجاست؟" title="لینک مستقیم به مشکل از کجاست؟" translate="no">​</a></h2>
<p>یکی از سوءبرداشت‌های رایج این است که شی‌گرایی را با «مدل‌کردن همه‌چیز به شکل کلاس» یکی بدانیم. در چنین نگاهی، برای هر اسم در دامنه یک کلاس می‌سازیم و برای هر عملیات یک سرویس. اما بعد از مدتی می‌بینیم رابطه‌ی میان این کلاس‌ها بیشتر شبیه زنجیره‌ای از وابستگی‌های مستقیم است تا طراحی قابل‌تغییر.</p>
<p>برای نمونه، چنین کدی ممکن است در پروژه‌ای ظاهراً شی‌گرا دیده شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">OrderService</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createOrder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> Database</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">isActive</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">HttpError</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'کاربر فعال نیست.'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> Database</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orders</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> SmsClient</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">phoneNumber</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'سفارش شما ثبت شد.'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این نمونه، مشکل این نیست که کلاس داریم. مشکل این است که کلاس اصلی، جزئیات زیادی را می‌شناسد: پایگاه داده، خطای HTTP، قالب درخواست، و سرویس پیامک. اگر قرار باشد همین رفتار از مسیر دیگری اجرا شود، یا اگر بخواهیم منطق ثبت سفارش را بدون پایگاه داده و پیامک آزمون کنیم، این وابستگی‌ها مانع می‌شوند.</p>
<p>کلاس در اینجا مرز نساخته است. فقط چند خط کد را در یک قالب شی‌گرا قرار داده است.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چندریختی-چرا-برای-معماری-مهم-است">چندریختی چرا برای معماری مهم است؟<a href="https://example.com/blog/oop-is-dependency-control#%DA%86%D9%86%D8%AF%D8%B1%DB%8C%D8%AE%D8%AA%DB%8C-%DA%86%D8%B1%D8%A7-%D8%A8%D8%B1%D8%A7%DB%8C-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D9%85%D9%87%D9%85-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به چندریختی چرا برای معماری مهم است؟" title="لینک مستقیم به چندریختی چرا برای معماری مهم است؟" translate="no">​</a></h2>
<p>چندریختی یعنی یک بخش از کد بتواند با یک قرارداد کلی کار کند، بدون آن‌که به پیاده‌سازی مشخص آن قرارداد وابسته باشد. این مفهوم در زبان‌های مختلف شکل‌های متفاوتی دارد: واسط، کلاس انتزاعی، پروتکل، تابع تزریق‌شده یا حتی قرارداد ساختاری. نام ابزار مهم نیست؛ اثر معماری مهم است.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>چندریختی در نگاه معماری</div><div class="admonitionContent_hiHs"><p>چندریختی یعنی سیاست اصلی سامانه بتواند با یک مرز انتزاعی کار کند و جزئیات بیرونی پشت آن مرز قرار بگیرند. به کمک این ویژگی، کدهای مهم‌تر مجبور نیستند پیاده‌سازی‌های کم‌ثبات‌تر را مستقیم بشناسند.</p></div></div>
<p>قدرت معماری چندریختی این است که می‌تواند جهت وابستگی را وارونه کند. در حالت ساده و خام، منطق اصلی مستقیماً به دیتابیس، پیامک یا فریم‌ورک وابسته می‌شود. اما با تعریف یک مرز، می‌توان کاری کرد که جزئیات بیرونی به قرارداد هسته وابسته شوند، نه برعکس.</p>
<p>برای نمونه، کاربرد ثبت سفارش می‌تواند به جای شناختن <code>SmsClient</code> و <code>Database</code>، با قراردادهای کوچک کار کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">UserRepository</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">User</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">OrderRepository</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> CreateOrderInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Order</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">NotificationPort</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">sendOrderCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> orderId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>بعد کاربرد اصلی فقط این قراردادها را می‌شناسد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">CreateOrderUseCase</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> users</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> UserRepository</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> orders</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> OrderRepository</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> notifications</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> NotificationPort</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">execute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> CreateOrderInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">isActive</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">InactiveUserError</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orders</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">notifications</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sendOrderCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اینجا هنوز پایگاه داده و پیام‌رسانی وجود دارند، اما در جای خودشان. کاربرد اصلی لازم نیست بداند داده از چه ابزاری خوانده می‌شود یا پیام با چه سرویسی ارسال می‌شود. پیاده‌سازی واقعی می‌تواند در لایه‌ی بیرونی قرار بگیرد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">PostgresOrderRepository</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">OrderRepository</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> CreateOrderInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> postgres</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orders</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">SmsNotificationPort</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">implements</span><span class="token plain"> </span><span class="token class-name">NotificationPort</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sendOrderCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> orderId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> smsClient</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">سفارش </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">orderId</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> ثبت شد.</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این شکل از طراحی، فقط برای قشنگ‌ترشدن کد نیست. اثر اصلی آن این است که منطق ثبت سفارش را می‌توان بدون پایگاه داده‌ی واقعی، بدون سرویس پیامک و بدون فریم‌ورک وب آزمون کرد. یعنی وابستگی‌ها کنترل شده‌اند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وارونهکردن-وابستگی-یعنی-چه">وارونه‌کردن وابستگی یعنی چه؟<a href="https://example.com/blog/oop-is-dependency-control#%D9%88%D8%A7%D8%B1%D9%88%D9%86%D9%87%DA%A9%D8%B1%D8%AF%D9%86-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به وارونه‌کردن وابستگی یعنی چه؟" title="لینک مستقیم به وارونه‌کردن وابستگی یعنی چه؟" translate="no">​</a></h2>
<p>در حالت طبیعی، وسوسه می‌شویم از مرکز سامانه به سمت ابزارها اشاره کنیم: کاربرد به دیتابیس وابسته می‌شود، موجودیت به مدل فریم‌ورک وابسته می‌شود، و قانون کسب‌وکار خطای HTTP تولید می‌کند. این جهت شاید در ابتدا ساده‌تر باشد، اما در بلندمدت هسته‌ی سامانه را به جزئیات بیرونی می‌دوزد.</p>
<p>وارونه‌کردن وابستگی یعنی قرارداد در جایی تعریف شود که برای سیاست اصلی معنا دارد، و پیاده‌سازی جزئیات بیرونی خودش را با آن قرارداد هماهنگ کند. به جای این‌که کاربرد بگوید «من با این کتابخانه‌ی خاص کار می‌کنم»، می‌گوید «من به چیزی نیاز دارم که بتواند سفارش را ذخیره کند.» بعد پایگاه داده‌ی واقعی، حافظه‌ی موقت یا نمونه‌ی آزمایشی، هرکدام می‌توانند این قرارداد را پیاده کنند.</p>
<p>این نقطه همان‌جایی است که شی‌گرایی برای معماری مهم می‌شود. شی‌گرایی فقط به ما اجازه نمی‌دهد چند شیء بسازیم؛ به ما اجازه می‌دهد مرزهایی بسازیم که جهت وابستگی را کنترل کنند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="کلاس-بدون-مرز-معماری-نمیسازد">کلاس بدون مرز، معماری نمی‌سازد<a href="https://example.com/blog/oop-is-dependency-control#%DA%A9%D9%84%D8%A7%D8%B3-%D8%A8%D8%AF%D9%88%D9%86-%D9%85%D8%B1%D8%B2-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D9%86%D9%85%DB%8C%D8%B3%D8%A7%D8%B2%D8%AF" class="hash-link" aria-label="لینک مستقیم به کلاس بدون مرز، معماری نمی‌سازد" title="لینک مستقیم به کلاس بدون مرز، معماری نمی‌سازد" translate="no">​</a></h2>
<p>ممکن است پروژه‌ای پر از کلاس باشد، اما همچنان هر تغییر کوچک در ابزارها، هسته‌ی سامانه را بلرزاند. مثلاً تغییر فریم‌ورک وب باعث تغییر کاربردها شود. تغییر پایگاه داده باعث تغییر موجودیت‌ها شود. تغییر سرویس پیامک باعث تغییر قانون ثبت سفارش شود. این‌ها نشانه‌هایی هستند که کلاس‌ها مرز واقعی نساخته‌اند.</p>
<p>در مقابل، ممکن است پروژه‌ای با کلاس‌های کمتر، اما مرزهای بهتر، تغییرپذیرتر باشد. مهم این نیست که چند کلاس داریم. مهم این است که کدام تصمیم‌ها پشت کدام مرزها محافظت شده‌اند. اگر قانون اصلی سامانه مستقل‌تر از ابزارها بماند، طراحی به هدف معماری نزدیک‌تر است.</p>
<p>این نکته به‌ویژه در زبان‌هایی مهم است که بدون کلاس هم می‌توانند قرارداد بسازند. در چنین زبان‌هایی، نباید فکر کنیم چون کلاس کم داریم، پس طراحی شی‌گرا یا معماری‌پذیر نداریم. اصل ماجرا، چندریختی و کنترل وابستگی است، نه شکل ظاهری دستور زبان.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نمونهی-آزمایشی-ساده">نمونه‌ی آزمایشی ساده<a href="https://example.com/blog/oop-is-dependency-control#%D9%86%D9%85%D9%88%D9%86%D9%87%DB%8C-%D8%A2%D8%B2%D9%85%D8%A7%DB%8C%D8%B4%DB%8C-%D8%B3%D8%A7%D8%AF%D9%87" class="hash-link" aria-label="لینک مستقیم به نمونه‌ی آزمایشی ساده" title="لینک مستقیم به نمونه‌ی آزمایشی ساده" translate="no">​</a></h2>
<p>وقتی مرز درست طراحی شده باشد، آزمون رفتار اصلی ساده‌تر می‌شود. برای نمونه، می‌توانیم به جای پایگاه داده و پیامک واقعی، نمونه‌های ساختگی بدهیم:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> users</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> UserRepository </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'u1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> isActive</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> orders</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> OrderRepository </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'o1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">...</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> notifications</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> NotificationPort </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sendOrderCreated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> useCase </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">CreateOrderUseCase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">users</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> orders</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> notifications</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> useCase</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">execute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'u1'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>این آزمون به فریم‌ورک وب، پایگاه داده و سرویس پیامک وابسته نیست. یعنی می‌توانیم رفتار اصلی را سریع‌تر، روشن‌تر و با خطایابی آسان‌تر بررسی کنیم. این همان تفاوتی است که کنترل وابستگی در عمل ایجاد می‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="طراحی-مرز-از-کجا-شروع-میشود">طراحی مرز از کجا شروع می‌شود؟<a href="https://example.com/blog/oop-is-dependency-control#%D8%B7%D8%B1%D8%A7%D8%AD%DB%8C-%D9%85%D8%B1%D8%B2-%D8%A7%D8%B2-%DA%A9%D8%AC%D8%A7-%D8%B4%D8%B1%D9%88%D8%B9-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به طراحی مرز از کجا شروع می‌شود؟" title="لینک مستقیم به طراحی مرز از کجا شروع می‌شود؟" translate="no">​</a></h2>
<p>طراحی مرز نباید با ساختن واسط برای همه‌چیز اشتباه گرفته شود. اگر برای هر کلاس و هر تابع یک واسط جدا بسازیم، فقط پیچیدگی را زیاد کرده‌ایم. مرز زمانی ارزش دارد که پشت آن، جزئیاتی ناپایدار یا دلیل تغییری متفاوت قرار دارد.</p>
<p>برای نمونه، پایگاه داده، سرویس بیرونی، فریم‌ورک وب، سامانه‌ی پیام‌رسانی و ساعت سیستم معمولاً جزئیاتی هستند که بهتر است هسته‌ی تصمیم‌های مهم، مستقیم به آن‌ها قفل نشود. اما یک تابع کوچک و پایدار داخلی شاید اصلاً نیازی به مرز انتزاعی نداشته باشد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>طراحی مرز</div><div class="admonitionContent_hiHs"><p>برای طراحی مرز، از خودت بپرس: کدام بخش تصمیم اصلی سامانه است و کدام بخش فقط روش اجرای آن؟ اگر ابزار بیرونی عوض شود، آیا قانون اصلی باید تغییر کند؟ آیا می‌توانم رفتار مهم را بدون راه‌اندازی دیتابیس، فریم‌ورک یا سرویس بیرونی آزمون کنم؟ اگر پاسخ منفی است، احتمالاً یک مرز مفید کم داریم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/oop-is-dependency-control#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای بررسی یک پروژه‌ی ظاهراً شی‌گرا، از تعداد کلاس‌ها شروع نکن. یک رفتار مهم را انتخاب کن و وابستگی‌های آن را دنبال کن. ببین آیا کلاس‌های اصلی مستقیم به دیتابیس، فریم‌ورک، درخواست وب، پاسخ بیرونی، پیام‌رسان یا ابزارهای عملیاتی وابسته‌اند یا نه. اگر هستند، فقط با «کلاس داشتن» به طراحی قابل‌تغییر نرسیده‌ای.</p>
<p>چیزهایی را که نباید بی‌دلیل تغییر بدهی هم مشخص کن. لازم نیست برای هر چیز کوچک واسط بسازی. لازم نیست هر کلاس را به چند کلاس کوچک‌تر بشکنی. لازم نیست فقط برای پیروی ظاهری از یک الگو، سازنده‌ها و قراردادهای زیادی وارد پروژه کنی. مرز وقتی ارزش دارد که یک تصمیم مهم را از جزئیات ناپایدار محافظت کند.</p>
<p>اگر تصمیم گرفتی مرزی بسازی، آن را با یک آزمون ساده اعتبارسنجی کن: آیا می‌توانی کاربرد را با پیاده‌سازی ساختگی اجرا کنی؟ آیا قانون اصلی بدون فریم‌ورک وب آزمون می‌شود؟ آیا تغییر ابزار بیرونی، امضای کاربرد را عوض نمی‌کند؟ سپس فرمان‌های مناسب پروژه را اجرا کن:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">pnpm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm typecheck</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm build</span><br></span></code></pre></div></div>
<p>اگر آزمون خودکار نداری، دست‌کم یک سناریوی دستی دقیق بنویس و بعد از جداسازی همان را اجرا کن. هدف این نیست که کد فقط شی‌گراتر به نظر برسد؛ هدف این است که تغییرهای آینده کم‌خطرتر شوند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/oop-is-dependency-control#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>شی‌گرایی یعنی کنترل وابستگی، نه فقط ساختن کلاس. اگر پروژه پر از کلاس باشد اما هسته‌ی سامانه هنوز به دیتابیس، فریم‌ورک و سرویس‌های بیرونی قفل شده باشد، معماری چندان جلو نرفته است. قدرت مهم شی‌گرایی در معماری، چندریختی و امکان وارونه‌کردن جهت وابستگی‌هاست.</p>
<p>وقتی سیاست‌های اصلی با قراردادهای پایدارتر کار می‌کنند و جزئیات بیرونی پشت آن قراردادها قرار می‌گیرند، سامانه آزمون‌پذیرتر و تغییرپذیرتر می‌شود. در این حالت، کلاس‌ها فقط ظرف کد نیستند؛ ابزار ساختن مرز هستند.</p>
<p>پرسش اصلی این نیست که «چند کلاس داریم؟» پرسش بهتر این است: «آیا کلاس‌ها و قراردادهای ما کمک کرده‌اند قانون‌های اصلی سامانه از جزئیات بیرونی مستقل‌تر بمانند؟» پاسخ همین پرسش نشان می‌دهد شی‌گرایی در پروژه فقط ظاهر کد است یا واقعاً به معماری کمک می‌کند.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="شی‌گرایی" term="شی‌گرایی"/>
        <category label="چندریختی" term="چندریختی"/>
        <category label="وابستگی" term="وابستگی"/>
        <category label="معماری تمیز" term="معماری تمیز"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[گاهی مرز معماری را می‌کشیم، اما هنوز دیوار نمی‌سازیم]]></title>
        <id>https://example.com/blog/partial-boundaries-in-real-projects</id>
        <link href="https://example.com/blog/partial-boundaries-in-real-projects"/>
        <updated>2026-03-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[نگاهی واقع‌بینانه به مرزهای معماری و اینکه چرا همیشه لازم نیست همه‌ی جداسازی‌ها را از روز نخست کامل پیاده کنیم.]]></summary>
        <content type="html"><![CDATA[<p>تیم وسط یک تصمیم سخت گیر کرده بود. از یک طرف، چند نفر می‌گفتند باید از همین حالا همه‌چیز را کامل جدا کنیم: ماژول مستقل، پایگاه داده‌ی جدا، قراردادهای رسمی، صف پیام، بسته‌بندی جداگانه و حتی شاید سرویس مستقل. از طرف دیگر، چند نفر می‌گفتند هنوز زود است؛ بهتر است همه‌چیز در همان کد فعلی بماند تا کار سریع‌تر جلو برود. بحث، ظاهراً درباره‌ی معماری بود، اما در عمل به دو گزینه‌ی افراطی تقلیل پیدا کرده بود: یا دیوار کامل بسازیم، یا اصلاً مرزی نکشیم.</p>
<p>مسئله این بود که هر دو طرف بخشی از حقیقت را می‌دیدند. جداکردن کامل، هزینه داشت: کد بیشتر، قرارداد بیشتر، آزمون بیشتر، هماهنگی بیشتر و اصطکاک بیشتر در توسعه‌ی روزمره. اما نساختن هیچ مرزی هم بی‌هزینه نبود. اگر بخش تازه واقعاً قرار بود رشد کند، به چند مسیر محصولی وصل شود و قانون‌های خودش را پیدا کند، چسباندن آن به کد فعلی، تغییرهای آینده را سخت‌تر می‌کرد. تیم نه می‌خواست اسیر طراحی زودهنگام شود، نه می‌خواست بعداً با توده‌ای از وابستگی‌های درهم روبه‌رو شود.</p>
<p><img decoding="async" loading="lazy" alt="نقشه‌ی ساختمانی با خط‌کشی مرزها، اما فقط بخشی از دیوارها ساخته شده‌اند؛ استعاره‌ای از مرزهای معماری نیمه‌کاره." src="https://example.com/assets/images/partial-boundaries-in-real-projects-367ba38a328234214c7d62563bd10f55.png" width="1731" height="909" class="img_m9Pm"></p>
<p>فصل بیست‌وچهارم کتاب <em>معماری تمیز</em> (Clean Architecture) دقیقاً به همین ناحیه‌ی خاکستری می‌پردازد: مرزهای نیمه‌کاره. این فصل یادآوری می‌کند که معماری فقط انتخاب میان «جداسازی کامل» و «بی‌مرزی کامل» نیست. گاهی لازم است مرز را ببینیم، جهت وابستگی را کنترل کنیم، قرارداد ذهنی یا کدی آن را مشخص کنیم، اما هنوز هزینه‌ی کامل جداسازی را نپردازیم.</p>
<p>این نگاه، برخلاف برداشت‌های سخت‌گیرانه از معماری، واقع‌بینانه‌تر است. پروژه‌های واقعی همیشه فرصت و بودجه‌ی ساختن دیوار کامل ندارند. از طرف دیگر، پروژه‌های واقعی آن‌قدر هم ساده نمی‌مانند که بتوانیم همه‌چیز را بی‌مرز و بی‌جهت کنار هم بگذاریم. مرز نیمه‌کاره، راهی برای فکرکردن به آینده است، بدون این‌که امروز را بی‌دلیل سنگین کنیم.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>مرز نیمه‌کاره چیست؟</div><div class="admonitionContent_hiHs"><p>مرز نیمه‌کاره یعنی تصمیم معماری را تشخیص داده‌ایم و جهت وابستگی را تا حد لازم کنترل می‌کنیم، اما هنوز همه‌ی هزینه‌های جداسازی کامل، مثل استقرار جدا، پایگاه داده‌ی جدا، بسته‌ی جدا یا ارتباط بین‌فرایندی را نمی‌پردازیم.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مرز-یعنی-چه-دیوار-یعنی-چه">مرز یعنی چه، دیوار یعنی چه؟<a href="https://example.com/blog/partial-boundaries-in-real-projects#%D9%85%D8%B1%D8%B2-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87-%D8%AF%DB%8C%D9%88%D8%A7%D8%B1-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به مرز یعنی چه، دیوار یعنی چه؟" title="لینک مستقیم به مرز یعنی چه، دیوار یعنی چه؟" translate="no">​</a></h2>
<p>مرز معماری یعنی دو بخش از سامانه دلیل‌های متفاوتی برای تغییر دارند و نباید بی‌محابا در هم فرو بروند. برای نمونه، بخش محاسبه‌ی قیمت ممکن است با قانون‌های مالی، سیاست‌های تخفیف و تصمیم‌های محصولی تغییر کند. بخش ارسال اعلان ممکن است با ابزار پیام‌رسانی، قالب پیام یا کانال ارتباطی تغییر کند. این دو بخش می‌توانند با هم همکاری کنند، اما لازم نیست جزئیات داخلی همدیگر را بشناسند.</p>
<p>دیوار کامل، شکل پرهزینه‌تر همین مرز است. مثلاً دو سرویس جدا می‌سازیم، هرکدام استقرار جدا دارند، ارتباطشان از راه شبکه است، قرارداد نسخه‌بندی‌شده دارند، و شاید داده‌هایشان هم جدا نگه‌داری شود. این دیوار گاهی لازم است، اما همیشه نه. اگر هنوز نمی‌دانیم مرز واقعاً پایدار است یا نه، ساختن دیوار کامل می‌تواند هزینه‌ای زودهنگام باشد.</p>
<p>مرز نیمه‌کاره بین این دو قرار می‌گیرد. یعنی می‌گوییم «این دو بخش را یکی نمی‌بینیم»، اما هنوز آن‌ها را به دو سرویس یا دو بسته‌ی کاملاً مستقل تبدیل نمی‌کنیم. شاید فقط یک واسط کوچک تعریف کنیم. شاید فقط وابستگی مستقیم را یک‌طرفه نگه داریم. شاید فقط دسترسی به داده را از راه یک درگاه انجام دهیم. شاید فقط نام‌گذاری و ساختار پوشه را طوری بچینیم که بعداً جداکردن آن ممکن باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-مرز-کامل-همیشه-تصمیم-خوبی-نیست">چرا مرز کامل همیشه تصمیم خوبی نیست؟<a href="https://example.com/blog/partial-boundaries-in-real-projects#%DA%86%D8%B1%D8%A7-%D9%85%D8%B1%D8%B2-%DA%A9%D8%A7%D9%85%D9%84-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%D8%AA%D8%B5%D9%85%DB%8C%D9%85-%D8%AE%D9%88%D8%A8%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به چرا مرز کامل همیشه تصمیم خوبی نیست؟" title="لینک مستقیم به چرا مرز کامل همیشه تصمیم خوبی نیست؟" translate="no">​</a></h2>
<p>گاهی تیم‌ها بعد از خواندن کتاب‌های معماری یا دیدن تجربه‌های شکست، به این نتیجه می‌رسند که هر مرزی باید از روز نخست کامل و سخت ساخته شود. این واکنش قابل‌درک است، اما همیشه درست نیست. مرز کامل هزینه دارد و اگر پیش از روشن‌شدن مسئله ساخته شود، ممکن است خودش به مانع تبدیل شود.</p>
<p>فرض کنیم تیم می‌خواهد بخش «امتیاز وفاداری کاربران» را اضافه کند. هنوز معلوم نیست این بخش فقط چند قانون ساده خواهد داشت یا در آینده به موتور مستقل پیشنهاد، گزارش، پاداش و کمپین تبدیل می‌شود. اگر از روز نخست آن را به یک سرویس جدا با پایگاه داده‌ی مستقل و قراردادهای رسمی تبدیل کنیم، شاید برای مسئله‌ای کوچک، بار عملیاتی و توسعه‌ای سنگینی ساخته باشیم. هر تغییر ساده نیازمند هماهنگی میان سرویس‌ها، داده‌ها و استقرارها می‌شود.</p>
<p>از طرف دیگر، اگر همین بخش را کاملاً داخل کنترلرها و مدل‌های فعلی پخش کنیم، بعداً بیرون‌کشیدن آن سخت می‌شود. قانون امتیازدهی در چند جای کد تکرار می‌شود، داده‌هایش با داده‌های دیگر قاطی می‌شود، و هر تغییر کوچک، مسیرهای نامرتبط را هم تحت تأثیر قرار می‌دهد.</p>
<p>مرز نیمه‌کاره اینجا کمک می‌کند. می‌توانیم هسته‌ی قانون امتیاز را در یک ماژول مشخص نگه داریم، ورودی و خروجی آن را روشن کنیم، وابستگی آن به جزئیات بیرونی را کنترل کنیم، اما هنوز سرویس جدا، پایگاه داده‌ی جدا یا قرارداد شبکه‌ای نسازیم.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>مرز زودهنگام هم می‌تواند بدهی بسازد</div><div class="admonitionContent_hiHs"><p>همیشه مشکل از کمبود معماری نیست. گاهی مرزهایی که خیلی زود، خیلی سخت و بدون شواهد کافی ساخته می‌شوند، خودشان هزینه‌ی تغییر را بالا می‌برند. جداسازی کامل وقتی ارزش دارد که دلیل تغییر، فشار رشد یا نیاز عملیاتی آن را توجیه کند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نمونهی-ساده-از-مرز-نیمهکاره">نمونه‌ی ساده از مرز نیمه‌کاره<a href="https://example.com/blog/partial-boundaries-in-real-projects#%D9%86%D9%85%D9%88%D9%86%D9%87%DB%8C-%D8%B3%D8%A7%D8%AF%D9%87-%D8%A7%D8%B2-%D9%85%D8%B1%D8%B2-%D9%86%DB%8C%D9%85%D9%87%DA%A9%D8%A7%D8%B1%D9%87" class="hash-link" aria-label="لینک مستقیم به نمونه‌ی ساده از مرز نیمه‌کاره" title="لینک مستقیم به نمونه‌ی ساده از مرز نیمه‌کاره" translate="no">​</a></h2>
<p>فرض کنید در یک سامانه‌ی فروش، بخشی برای محاسبه‌ی امتیاز وفاداری اضافه شده است. ساده‌ترین راه این است که محاسبه را همان‌جا بنویسیم که سفارش ثبت می‌شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createOrder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ordersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> points </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">floor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">totalPrice </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> usersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">addLoyaltyPoints</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> points</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد شاید در نسخه‌ی نخست کار کند، اما خیلی زود چند پرسش ایجاد می‌شود: اگر قانون امتیاز برای گروه‌های مختلف کاربر متفاوت شود چه؟ اگر بعضی کالاها امتیاز نداشته باشند چه؟ اگر بخواهیم امتیاز را در پنل پشتیبانی یا پردازش پس‌زمینه هم محاسبه کنیم چه؟ اگر بعداً این بخش واقعاً مستقل شد، از کجا باید آن را جدا کنیم؟</p>
<p>مرز نیمه‌کاره می‌تواند ساده‌تر از چیزی باشد که فکر می‌کنیم:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">order/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  create-order.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">loyalty/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  loyalty-policy.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  loyalty-port.ts</span><br></span></code></pre></div></div>
<p>و در کد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">LoyaltyPort</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">grantPoints</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> points</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createOrder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ordersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> points </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> loyaltyPolicy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">calculateForOrder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> loyaltyPort</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">grantPoints</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> points</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>اینجا هنوز سرویس جدا نداریم. هنوز ارتباط شبکه‌ای نداریم. شاید همه‌چیز در همان برنامه و همان مخزن کد باشد. اما مرز دیده شده است: قانون امتیازدهی در دل ثبت سفارش دفن نشده، و ثبت سفارش لازم نیست جزئیات ذخیره‌سازی امتیاز را بداند. اگر بعداً بخش وفاداری بزرگ‌تر شد، جداکردن آن از نقطه‌ای مشخص آغاز می‌شود، نه از میان کدی پخش‌شده و مبهم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مرز-نیمهکاره-یعنی-تعلیق-تصمیم-نه-فرار-از-تصمیم">مرز نیمه‌کاره یعنی تعلیق تصمیم، نه فرار از تصمیم<a href="https://example.com/blog/partial-boundaries-in-real-projects#%D9%85%D8%B1%D8%B2-%D9%86%DB%8C%D9%85%D9%87%DA%A9%D8%A7%D8%B1%D9%87-%DB%8C%D8%B9%D9%86%DB%8C-%D8%AA%D8%B9%D9%84%DB%8C%D9%82-%D8%AA%D8%B5%D9%85%DB%8C%D9%85-%D9%86%D9%87-%D9%81%D8%B1%D8%A7%D8%B1-%D8%A7%D8%B2-%D8%AA%D8%B5%D9%85%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به مرز نیمه‌کاره یعنی تعلیق تصمیم، نه فرار از تصمیم" title="لینک مستقیم به مرز نیمه‌کاره یعنی تعلیق تصمیم، نه فرار از تصمیم" translate="no">​</a></h2>
<p>نکته‌ی ظریف اینجاست: مرز نیمه‌کاره نباید بهانه‌ای برای شلختگی باشد. این‌که بگوییم «فعلاً کامل جدا نمی‌کنیم» با این‌که بگوییم «فعلاً هیچ مرزی نداریم» فرق دارد. در حالت اول، تصمیم معماری را دیده‌ایم، فقط هزینه‌ی کامل آن را عقب انداخته‌ایم. در حالت دوم، مسئله را نادیده گرفته‌ایم.</p>
<p>تعلیق سالم تصمیم، باید نشانه داشته باشد. باید بدانیم چه چیزی اگر رخ داد، مرز را جدی‌تر می‌کنیم. برای نمونه:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">مرز احتمالی:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">بخش امتیاز وفاداری از ثبت سفارش جدا نگه داشته شود.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">وضعیت فعلی:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">فعلاً در همان برنامه اجرا می‌شود، اما قانون امتیازدهی در ماژول جداست و ثبت سفارش فقط از درگاه وفاداری استفاده می‌کند.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">نشانه‌ی تکمیل مرز:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">اگر امتیاز وفاداری از بیش از دو مسیر محصولی استفاده شد، یا نیاز به زمان‌بندی مستقل پیدا کرد، یا تیم جداگانه‌ای مالک آن شد، جداسازی کامل بررسی شود.</span><br></span></code></pre></div></div>
<p>این یادداشت کوتاه، تصمیم را از یک حس مبهم به یک توافق قابل‌پیگیری تبدیل می‌کند. تیم می‌داند چرا امروز دیوار کامل نمی‌سازد، و می‌داند چه زمانی باید دوباره تصمیم را بررسی کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مرز-نیمهکاره-و-قانون-وابستگی">مرز نیمه‌کاره و قانون وابستگی<a href="https://example.com/blog/partial-boundaries-in-real-projects#%D9%85%D8%B1%D8%B2-%D9%86%DB%8C%D9%85%D9%87%DA%A9%D8%A7%D8%B1%D9%87-%D9%88-%D9%82%D8%A7%D9%86%D9%88%D9%86-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به مرز نیمه‌کاره و قانون وابستگی" title="لینک مستقیم به مرز نیمه‌کاره و قانون وابستگی" translate="no">​</a></h2>
<p>مرز نیمه‌کاره فقط زمانی مفید است که جهت وابستگی را جدی بگیریم. اگر دو بخش هنوز در یک فرایند و یک مخزن کد هستند، دست‌کم باید مراقب باشیم کدام بخش کدام را می‌شناسد. اگر هسته‌ی قانون امتیازدهی به کنترلر سفارش، مدل پایگاه داده یا جزئیات پیام‌رسانی وابسته شود، دیگر مرز نیمه‌کاره نداریم؛ فقط پوشه‌ای جدا داریم که از درون به همه‌چیز وصل است.</p>
<p>در مرز نیمه‌کاره، لازم نیست همه‌چیز را سخت و رسمی کنیم، اما باید چند چیز روشن باشد: ورودی و خروجی بخش چیست، چه کسی تصمیم اصلی را می‌گیرد، جزئیات بیرونی کجا قرار دارند، و اگر روزی خواستیم جداسازی را کامل کنیم، کدام وابستگی‌ها مانع خواهند شد.</p>
<p>به بیان دیگر، مرز نیمه‌کاره دیوار نیست، اما خط روی نقشه هم نباید تزئینی باشد. اگر خطی کشیده‌ایم، باید رفتار امروزمان با آن سازگار باشد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>تصمیم‌گیری عملی</div><div class="admonitionContent_hiHs"><p>وقتی میان جداسازی کامل و بی‌مرزی گیر کردی، سه پرسش بپرس: آیا این بخش دلیل تغییر مستقلی دارد؟ آیا احتمال دارد از چند مسیر استفاده شود؟ آیا می‌توان جهت وابستگی را امروز با هزینه‌ی کم کنترل کرد؟ اگر پاسخ‌ها مثبت‌اند، مرز نیمه‌کاره معمولاً انتخاب معقولی است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="کجا-مرز-نیمهکاره-کافی-نیست">کجا مرز نیمه‌کاره کافی نیست؟<a href="https://example.com/blog/partial-boundaries-in-real-projects#%DA%A9%D8%AC%D8%A7-%D9%85%D8%B1%D8%B2-%D9%86%DB%8C%D9%85%D9%87%DA%A9%D8%A7%D8%B1%D9%87-%DA%A9%D8%A7%D9%81%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به کجا مرز نیمه‌کاره کافی نیست؟" title="لینک مستقیم به کجا مرز نیمه‌کاره کافی نیست؟" translate="no">​</a></h2>
<p>مرز نیمه‌کاره برای همه‌چیز مناسب نیست. اگر دو بخش نیاز عملیاتی کاملاً متفاوت دارند، مالکیت تیمی جدا دارند، نرخ تغییر و مقیاس متفاوت دارند، یا خرابی یکی نباید دیگری را زمین بزند، شاید مرز نیمه‌کاره کافی نباشد. در چنین وضعی، دیوار واقعی لازم می‌شود: جداسازی استقرار، داده، قرارداد و مشاهده‌پذیری.</p>
<p>همچنین اگر داده‌ها یا قواعد یک بخش به‌قدری حساس‌اند که نباید از بیرون مستقیم لمس شوند، مرز شل ممکن است خطرناک باشد. برای نمونه، در بخش‌های مالی، حسابداری یا امنیت، گاهی نیاز داریم مرز را زودتر و محکم‌تر بسازیم، چون هزینه‌ی خطا زیاد است. واقع‌بینی یعنی هم از طراحی افراطی پرهیز کنیم، هم حساسیت دامنه را کوچک نشماریم.</p>
<p>پس پرسش درست این نیست که «مرز نیمه‌کاره خوب است یا بد؟» پرسش درست این است که «برای این تصمیم، در این مرحله از محصول، با این هزینه و این ریسک، چه مقدار مرز کافی است؟»</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/partial-boundaries-in-real-projects#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای هر مرزی که در ذهن داری، اول دلیل آن را بنویس. آیا دو بخش دلیل تغییر متفاوت دارند؟ آیا احتمال دارد در آینده مالکیت جدا پیدا کنند؟ آیا قانون یکی نباید زیر جزئیات دیگری پنهان شود؟ اگر نتوانی دلیل مرز را روشن بگویی، شاید هنوز زمان ساختن آن نرسیده باشد.</p>
<p>بعد مشخص کن امروز چه چیزهایی را باید کنترل کنی. شاید کافی باشد ورودی و خروجی یک ماژول را روشن کنی. شاید باید وابستگی مستقیم به پایگاه داده را پشت یک درگاه ببری. شاید فقط باید قانون دامنه را از کنترلر بیرون بکشی. لازم نیست بی‌دلیل سرویس جدا، صف پیام، پایگاه داده‌ی جدا یا بسته‌بندی پیچیده بسازی.</p>
<p>در مقابل، چیزهایی را هم بی‌دلیل رها نکن. اگر می‌دانی بخشی قرار است رشد کند، قانون‌های مستقل دارد و از چند مسیر استفاده می‌شود، آن را در کدهای حاشیه‌ای پخش نکن. حتی اگر امروز دیوار نمی‌سازی، دست‌کم خط مرز را طوری بکش که فردا بتوانی دیوار را از همان‌جا بالا ببری.</p>
<p>برای اعتبارسنجی، یک آزمون ساده انجام بده: ببین آیا می‌توانی بخش موردنظر را بدون اجرای مسیرهای نامرتبط صدا بزنی یا نه. اگر پروژه‌ی تو ابزارهای رایج جاوااسکریپت یا تایپ‌اسکریپت دارد، پس از تغییر ساختار، فرمان‌های مناسب را اجرا کن:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">pnpm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm typecheck</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm build</span><br></span></code></pre></div></div>
<p>اگر آزمون خودکار نداری، یک سناریوی دستی دقیق بنویس و بعد از جداسازی آن را اجرا کن. هدف از مرز نیمه‌کاره این نیست که خیالمان راحت شود؛ هدف این است که با هزینه‌ای متناسب، خطر تغییرهای آینده را کمتر کنیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/partial-boundaries-in-real-projects#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>گاهی مرز معماری را می‌کشیم، اما هنوز دیوار نمی‌سازیم. این کار نه عقب‌نشینی از معماری است، نه طراحی ناقص، اگر آگاهانه انجام شود. مرز نیمه‌کاره یعنی مسئله را دیده‌ایم، جهت وابستگی را کنترل کرده‌ایم، و هزینه‌ی کامل جداسازی را به زمانی موکول کرده‌ایم که شواهد کافی برای آن داریم.</p>
<p>تیم‌های واقعی نباید میان دو افراط زندانی شوند: جداسازی کامل از روز نخست، یا بی‌مرزی کامل تا روز بحران. معماری خوب فقط در طراحی‌های بزرگ و رسمی دیده نمی‌شود؛ گاهی در همین تصمیم‌های کوچک و میانی دیده می‌شود. در این‌که قانون را کجا می‌گذاریم، وابستگی را به کدام سمت نگه می‌داریم، و برای چه زمانی نشانه‌ی بازنگری تعریف می‌کنیم.</p>
<p>پرسش پایانی این نیست که «آیا دیوار ساخته‌ایم؟» پرسش بهتر این است: «آیا می‌دانیم دیوار احتمالی از کجا باید ساخته شود، و آیا امروز کاری نکرده‌ایم که ساختن آن در آینده بی‌دلیل سخت شود؟» همین پرسش، مرز نیمه‌کاره را از بی‌تصمیمی جدا می‌کند.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="معماری تمیز" term="معماری تمیز"/>
        <category label="مرز معماری" term="مرز معماری"/>
        <category label="طراحی تدریجی" term="طراحی تدریجی"/>
        <category label="پیچیدگی نرم‌افزار" term="پیچیدگی نرم‌افزار"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[معماری تمیز را با نقاشی دایره‌ها اشتباه نگیریم]]></title>
        <id>https://example.com/blog/clean-architecture-not-circles</id>
        <link href="https://example.com/blog/clean-architecture-not-circles"/>
        <updated>2026-03-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[نقدی بر برداشت سطحی از معماری تمیز که آن را به چند پوشه و چند دایره تقلیل می‌دهد.]]></summary>
        <content type="html"><![CDATA[<p>تیم تصمیم گرفته بود «معماری تمیز» را جدی‌تر وارد پروژه کند. چند پوشه‌ی تازه ساخته شد: <code>entity</code>، <code>usecase</code> و <code>adapter</code>. نمودار دایره‌ای معروف هم در جلسه‌ی فنی روی تخته کشیده شد. از بیرون، همه‌چیز شبیه یک حرکت درست به نظر می‌رسید: نام‌ها آشنا بودند، ساختار پروژه مرتب‌تر شده بود و کدها دیگر همگی در یک پوشه‌ی بزرگ کنار هم نبودند.</p>
<p>اما چند هفته بعد، مسئله‌ی قدیمی دوباره خودش را نشان داد. موجودیت‌ها هنوز نوع‌های مخصوص پایگاه داده را می‌شناختند. کاربردها مستقیم به فریم‌ورک وب وابسته بودند. آداپترها فقط اسمشان آداپتر بود، اما تصمیم‌های اصلی سامانه هنوز از دل کدهای دیتابیس و کنترلر بیرون می‌آمد. پروژه پوشه‌های معماری تمیز داشت، اما وابستگی‌ها همچنان به جزئیات بیرونی قفل شده بودند.</p>
<p><img decoding="async" loading="lazy" alt="چند دایره‌ی معماری روی کاغذ، همراه با فلش‌هایی که جهت درست و نادرست وابستگی را نشان می‌دهند؛ تصویری مفهومی، ساده و آموزشی." src="https://example.com/assets/images/clean-architecture-not-circles-24bcaa77817605ebc5af9e7b48a6d819.png" width="1731" height="909" class="img_m9Pm"></p>
<p>این همان جایی است که معماری تمیز با ظاهر معماری تمیز اشتباه گرفته می‌شود. داشتن پوشه‌هایی با نام‌های درست، به‌تنهایی هیچ تضمینی نمی‌دهد. همان‌طور که کشیدن چند دایره روی کاغذ، نرم‌افزار را قابل‌تغییرتر نمی‌کند. اصل معماری تمیز، نام پوشه‌ها نیست؛ اصل آن جهت وابستگی‌ها و جداسازی سیاست‌های اصلی از جزئیات بیرونی است.</p>
<p>فصل بیست‌ودوم کتاب <em>معماری تمیز</em> (Clean Architecture) دقیقاً درباره‌ی همین نکته است. نمودار دایره‌ها فقط یک تصویر آموزشی است، نه خود معماری. اگر تصویر را حفظ کنیم اما قانون پشت آن را نفهمیم، خیلی راحت به معماری نمایشی می‌رسیم: ساختاری که در نگاه نخست مرتب و مهندسی‌شده است، اما در عمل همان قفل‌شدگی قبلی را دارد.</p>
<div class="theme-admonition theme-admonition-danger admonition_xGDO alert alert--danger"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"></path></svg></span>خطر معماری نمایشی</div><div class="admonitionContent_hiHs"><p>معماری نمایشی زمانی رخ می‌دهد که نام‌ها و پوشه‌ها شبیه معماری تمیز هستند، اما جهت وابستگی‌ها همچنان از منطق اصلی به سمت دیتابیس، فریم‌ورک، پیام‌رسان، رابط وب یا ابزارهای بیرونی است. در چنین وضعی، کد فقط ظاهر معماری گرفته، نه خاصیت معماری.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مسئله-دایرهها-نیستند">مسئله دایره‌ها نیستند<a href="https://example.com/blog/clean-architecture-not-circles#%D9%85%D8%B3%D8%A6%D9%84%D9%87-%D8%AF%D8%A7%DB%8C%D8%B1%D9%87%D9%87%D8%A7-%D9%86%DB%8C%D8%B3%D8%AA%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به مسئله دایره‌ها نیستند" title="لینک مستقیم به مسئله دایره‌ها نیستند" translate="no">​</a></h2>
<p>دایره‌های معماری تمیز معمولاً این پیام را منتقل می‌کنند: هرچه به مرکز نزدیک‌تر می‌شویم، به سیاست‌های پایدارتر و مهم‌تر سامانه نزدیک‌تر هستیم؛ هرچه به بیرون می‌رویم، به جزئیات اجرایی، ابزارها و سازوکارهای تغییرپذیرتر می‌رسیم.</p>
<p>اما این تصویر وقتی خطرناک می‌شود که آن را به دستور ساخت پوشه تبدیل کنیم. بعضی تیم‌ها فکر می‌کنند اگر پوشه‌ای به نام <code>entities</code> و پوشه‌ای به نام <code>usecases</code> داشته باشند، معماری‌شان تمیز شده است. در حالی که ممکن است داخل همان <code>usecases</code>، وابستگی مستقیم به درخواست HTTP، شیء اتصال پایگاه داده یا کتابخانه‌ی صف پیام وجود داشته باشد.</p>
<p>معماری تمیز درباره‌ی این نیست که چند لایه داریم یا اسم آن‌ها چیست. درباره‌ی این است که کدام بخش‌ها به کدام بخش‌ها وابسته‌اند. اگر منطق اصلی سامانه برای اجرا یا آزمون‌شدن، مجبور باشد فریم‌ورک وب، پایگاه داده یا ابزار بیرونی خاصی را بشناسد، نام پوشه‌ها دیگر کمکی نمی‌کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="قانون-وابستگی-یعنی-چه">قانون وابستگی یعنی چه؟<a href="https://example.com/blog/clean-architecture-not-circles#%D9%82%D8%A7%D9%86%D9%88%D9%86-%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%DB%8C%D8%B9%D9%86%DB%8C-%DA%86%D9%87" class="hash-link" aria-label="لینک مستقیم به قانون وابستگی یعنی چه؟" title="لینک مستقیم به قانون وابستگی یعنی چه؟" translate="no">​</a></h2>
<p>در معماری تمیز، قانون وابستگی می‌گوید وابستگی‌های کد باید به سمت درون باشند. یعنی بخش‌های بیرونی می‌توانند بخش‌های درونی را بشناسند، اما بخش‌های درونی نباید به جزئیات بیرونی وابسته باشند.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>قانون وابستگی</div><div class="admonitionContent_hiHs"><p>قانون وابستگی می‌گوید کدهای نزدیک‌تر به مرکز، نباید نام، نوع، قرارداد یا جزئیات کدهای بیرونی را بشناسند. موجودیت‌ها و کاربردها نباید به فریم‌ورک، پایگاه داده، کنترلر، رابط کاربری یا ابزارهای ارسال پیام وابسته باشند.</p></div></div>
<p>این قانون، ساده به نظر می‌رسد؛ اما در عمل رعایت آن سخت است. چون ابزارهای بیرونی معمولاً زودتر در کد ظاهر می‌شوند. فریم‌ورک مسیر درخواست را تعریف می‌کند. پایگاه داده مدل‌ها را می‌سازد. کتابخانه‌ی پیام‌رسان روش ارسال را مشخص می‌کند. اگر مراقب نباشیم، همین جزئیات آرام‌آرام زبان اصلی سامانه را شکل می‌دهند.</p>
<p>به جای این‌که بگوییم «ثبت سفارش چه رفتاری دارد؟»، می‌گوییم «در این کنترلر چه چیزی ذخیره کنیم؟» به جای این‌که بپرسیم «قانون اعتبارسنجی سفارش چیست؟»، می‌پرسیم «این فیلد در سریالایزر چطور بررسی شود؟» به جای این‌که کاربرد را مستقل بفهمیم، آن را به مسیر وب یا جدول پایگاه داده گره می‌زنیم.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="سیاست-و-جزئیات-را-قاطی-نکنیم">سیاست و جزئیات را قاطی نکنیم<a href="https://example.com/blog/clean-architecture-not-circles#%D8%B3%DB%8C%D8%A7%D8%B3%D8%AA-%D9%88-%D8%AC%D8%B2%D8%A6%DB%8C%D8%A7%D8%AA-%D8%B1%D8%A7-%D9%82%D8%A7%D8%B7%DB%8C-%D9%86%DA%A9%D9%86%DB%8C%D9%85" class="hash-link" aria-label="لینک مستقیم به سیاست و جزئیات را قاطی نکنیم" title="لینک مستقیم به سیاست و جزئیات را قاطی نکنیم" translate="no">​</a></h2>
<p>در زبان معماری تمیز، سیاست همان تصمیم مهم و پایدارتر سامانه است. مثلاً این‌که «سفارش بدون پرداخت معتبر نیست»، یا «کاربر غیرفعال اجازه‌ی برداشت ندارد»، یا «تخفیف نباید قیمت نهایی را منفی کند». این‌ها به مسئله مربوط‌اند، نه به ابزار.</p>
<p>جزئیات، چیزهایی هستند که روش اجرای سیاست را ممکن می‌کنند: پایگاه داده، فریم‌ورک وب، قالب JSON، سرویس پیامک، سامانه‌ی لاگ، صف پیام، کتابخانه‌ی زمان‌بندی و مانند آن. این‌ها مهم‌اند، اما نباید مالک سیاست شوند.</p>
<p>مشکل زمانی آغاز می‌شود که سیاست در دل جزئیات نوشته شود. برای نمونه، چنین کدی شاید در پروژه‌ای با پوشه‌بندی ظاهراً تمیز هم دیده شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// usecase/create-order.ts</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createOrder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">users</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">isActive</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'کاربر غیرفعال است.'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">orders</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">insert</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">201</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>نام فایل می‌گوید با یک کاربرد روبه‌رو هستیم، اما وابستگی‌ها چیز دیگری می‌گویند. این کد درخواست وب را می‌شناسد، پاسخ وب را می‌سازد، به شیء پایگاه داده وابسته است، و قانون فعال‌بودن کاربر را هم همان‌جا اجرا می‌کند. اگر فردا همین کاربرد از مسیر صف پیام، پنل داخلی یا ابزار خط فرمان اجرا شود، این کد به‌راحتی قابل‌استفاده نیست.</p>
<p>نسخه‌ی سالم‌تر، قرار نیست حتماً بزرگ یا پیچیده باشد. فقط باید مرز را روشن‌تر کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">CreateOrderInput</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> OrderItem</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">UserRepository</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">User</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">OrderRepository</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">createPending</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> CreatePendingOrderInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Order</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createOrder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> CreateOrderInput</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> userRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">isActive</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">InactiveUserError</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> orderRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">createPending</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این نمونه، هنوز به مخزن داده نیاز داریم، اما کاربرد لازم نیست بداند داده از چه پایگاه داده‌ای می‌آید. هنوز خطا رخ می‌دهد، اما کاربرد لازم نیست بداند این خطا در HTTP با چه کدی نمایش داده می‌شود. کنترلر می‌تواند نتیجه را به پاسخ وب تبدیل کند، و آداپتر پایگاه داده می‌تواند قرارداد مخزن را با ابزار واقعی پیاده کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وابستگی-همیشه-با-import-دیده-نمیشود">وابستگی همیشه با import دیده نمی‌شود<a href="https://example.com/blog/clean-architecture-not-circles#%D9%88%D8%A7%D8%A8%D8%B3%D8%AA%DA%AF%DB%8C-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%D8%A8%D8%A7-import-%D8%AF%DB%8C%D8%AF%D9%87-%D9%86%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به وابستگی همیشه با import دیده نمی‌شود" title="لینک مستقیم به وابستگی همیشه با import دیده نمی‌شود" translate="no">​</a></h2>
<p>گاهی وابستگی مستقیم در قالب <code>import</code> دیده می‌شود؛ مثلاً کاربرد، کلاس کنترلر یا مدل پایگاه داده را وارد کرده است. این حالت ساده‌تر پیدا می‌شود. اما همیشه ماجرا به این وضوح نیست.</p>
<p>گاهی وابستگی از راه نوع داده وارد می‌شود. مثلاً موجودیت، نوعی را می‌پذیرد که فقط در کتابخانه‌ی پایگاه داده معنا دارد. گاهی از راه قرارداد ورودی وارد می‌شود؛ مثلاً کاربرد به جای ورودی مستقل، همان بدنه‌ی درخواست وب را مصرف می‌کند. گاهی از راه زبان خطا وارد می‌شود؛ مثلاً قانون کسب‌وکار، پیام و کد وضعیت مخصوص HTTP را تولید می‌کند.</p>
<p>به همین دلیل، بررسی معماری فقط با نگاه‌کردن به اسم پوشه‌ها کافی نیست. باید ببینیم اگر جزئیات بیرونی تغییر کنند، کدام بخش‌ها مجبور به تغییر می‌شوند. اگر عوض‌کردن پایگاه داده باعث تغییر موجودیت‌ها شود، اگر تغییر فریم‌ورک وب کاربردها را جابه‌جا کند، یا اگر تغییر قالب پاسخ روی قانون کسب‌وکار اثر بگذارد، جهت وابستگی‌ها احتمالاً درست نیست.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-تیمها-به-معماری-نمایشی-میافتند">چرا تیم‌ها به معماری نمایشی می‌افتند؟<a href="https://example.com/blog/clean-architecture-not-circles#%DA%86%D8%B1%D8%A7-%D8%AA%DB%8C%D9%85%D9%87%D8%A7-%D8%A8%D9%87-%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D9%86%D9%85%D8%A7%DB%8C%D8%B4%DB%8C-%D9%85%DB%8C%D8%A7%D9%81%D8%AA%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به چرا تیم‌ها به معماری نمایشی می‌افتند؟" title="لینک مستقیم به چرا تیم‌ها به معماری نمایشی می‌افتند؟" translate="no">​</a></h2>
<p>یکی از علت‌ها این است که ظاهر معماری راحت‌تر از خود معماری دیده می‌شود. پوشه ساختن سریع است. جابه‌جایی فایل‌ها حس پیشرفت می‌دهد. نمودار دایره‌ها هم در جلسه‌ها قانع‌کننده به نظر می‌رسد. اما تغییر جهت وابستگی‌ها سخت‌تر است، چون نیازمند تصمیم‌های دقیق‌تر، تعریف مرز، ساخت قراردادهای کوچک و گاهی بازنگری در آزمون‌هاست.</p>
<p>علت دیگر، فشار تحویل است. وقتی زمان کم است، ساده‌ترین کار این است که جزئیات بیرونی را مستقیم به منطق اصلی وصل کنیم. کنترلر داده را دارد، پایگاه داده آماده است، فریم‌ورک مسیر را ساخته، پس چرا همان‌جا قانون را ننویسیم؟ پاسخ این است که شاید امروز سریع‌تر شود، اما اگر این رفتار مهم و رو به تغییر باشد، فردا هزینه‌ی همان تصمیم برمی‌گردد.</p>
<p>معماری تمیز قرار نیست جلوی تحویل را بگیرد. اما قرار است نگذارد ابزارهای بیرونی، شکل منطق اصلی سامانه را تعیین کنند. تفاوت مهمی میان «ساده نگه‌داشتن» و «وابسته‌کردن همه‌چیز به جزئیات» وجود دارد.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>چک‌لیست ساده‌ی تشخیص معماری واقعی</div><div class="admonitionContent_hiHs"><p>برای تشخیص این‌که معماری فقط نمایشی نیست، چند پرسش بپرس: آیا کاربرد بدون HTTP قابل‌آزمون است؟ آیا موجودیت‌ها نوع‌های پایگاه داده را نمی‌شناسند؟ آیا تغییر قالب پاسخ، قانون کسب‌وکار را تغییر نمی‌دهد؟ آیا آداپترها به سمت هسته وابسته‌اند، نه برعکس؟ آیا می‌توان یک جزئیات بیرونی را با نمونه‌ی ساختگی جایگزین کرد؟</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/clean-architecture-not-circles#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای بررسی یک پروژه، از اسم پوشه‌ها آغاز نکن. از جهت وابستگی‌ها آغاز کن. یک کاربرد مهم را انتخاب کن و مسیر آن را دنبال کن: از کنترلر یا ورودی بیرونی شروع کن، ببین به کجا می‌رسد، چه نوع‌هایی را وارد می‌کند، چه چیزهایی را می‌شناسد، و برای آزمون آن به چه ابزارهایی نیاز داری.</p>
<p>چیزهایی را که نباید بی‌دلیل تغییر بدهی هم مشخص کن. اگر پروژه کوچک است و هنوز رفتار دامنه‌ای پیچیده‌ای ندارد، ساختن لایه‌های زیاد ممکن است فقط هزینه‌ی فهم را بالا ببرد. اگر یک مسیر ساده فقط داده‌ای را می‌خواند و برمی‌گرداند، شاید نیازی به بازطراحی بزرگ نداشته باشد. معماری تمیز یعنی مرزگذاری برای محافظت از سیاست‌های مهم، نه تقسیم مکانیکی همه‌ی فایل‌ها به چند پوشه‌ی از پیش تعیین‌شده.</p>
<p>اما اگر یک قانون مهم در کنترلر، مدل پایگاه داده یا سریالایزر پنهان شده، اگر کاربردها بدون فریم‌ورک اجرا نمی‌شوند، یا اگر آزمون یک تصمیم دامنه‌ای نیازمند راه‌اندازی چند ابزار بیرونی است، باید جداسازی را جدی گرفت. در چنین حالتی، یک گام کوچک کافی است: ورودی کاربرد را مستقل کن، قرارداد مخزن را به زبان دامنه تعریف کن، و تبدیل HTTP یا پایگاه داده را به آداپترها بسپار.</p>
<p>برای اعتبارسنجی، فقط به اجراشدن برنامه اکتفا نکن. آزمون مربوط به کاربرد را بدون فریم‌ورک وب اجرا کن. اگر پروژه بررسی نوع یا ساخت دارد، آن را هم اجرا کن:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">pnpm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm typecheck</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm build</span><br></span></code></pre></div></div>
<p>اگر هنوز آزمون کافی نداری، حداقل یک سناریوی مهم را دستی ثبت کن: ورودی چیست، خروجی چیست، کدام قانون باید اجرا شود، و کدام جزئیات بیرونی نباید در تصمیم اصلی دخالت کند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/clean-architecture-not-circles#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>معماری تمیز را نباید با نقاشی دایره‌ها اشتباه بگیریم. دایره‌ها فقط کمک می‌کنند ایده را ببینیم؛ خود ایده، قانون وابستگی است. اگر وابستگی‌ها به سمت درون نباشند، اگر سیاست‌های اصلی به دیتابیس و فریم‌ورک قفل شده باشند، و اگر منطق دامنه زیر جزئیات بیرونی دفن شده باشد، چند پوشه با نام‌های آشنا چیزی را عوض نمی‌کند.</p>
<p>معماری واقعی وقتی خودش را نشان می‌دهد که تغییر جزئیات بیرونی، قلب سامانه را نلرزاند. وقتی کاربردها با زبان مسئله خوانده شوند. وقتی موجودیت‌ها قرارداد وب و پایگاه داده را نشناسند. وقتی آداپترها به هسته وصل شوند، نه این‌که هسته را به ابزارها بکشانند.</p>
<p>در پایان، پرسش خوب این نیست که «آیا پوشه‌ی entity و usecase داریم؟» پرسش بهتر این است: «اگر فردا فریم‌ورک، پایگاه داده یا مسیر ورودی عوض شود، قانون‌های اصلی سامانه چقدر دست‌نخورده می‌مانند؟» پاسخ همین پرسش، تفاوت معماری تمیز با تصویر تمیز معماری را روشن می‌کند.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="معماری تمیز" term="معماری تمیز"/>
        <category label="قانون وابستگی" term="قانون وابستگی"/>
        <category label="طراحی نرم‌افزار" term="طراحی نرم‌افزار"/>
        <category label="معماری نرم‌افزار" term="معماری نرم‌افزار"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[قانون کسب‌وکار نباید ته کنترلر گم شود]]></title>
        <id>https://example.com/blog/business-rules-should-not-live-in-controller</id>
        <link href="https://example.com/blog/business-rules-should-not-live-in-controller"/>
        <updated>2026-03-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[تحلیلی درباره‌ی اینکه چرا منطق اصلی سامانه نباید در کنترلر، سریالایزر یا کدهای حاشیه‌ای پنهان شود.]]></summary>
        <content type="html"><![CDATA[<p>همه‌چیز از یک کنترلر ساده آغاز شد. قرار بود کاربر درخواست برداشت وجه ثبت کند، سامانه چند داده‌ی اولیه را بخواند، درخواست را ذخیره کند و پاسخ بدهد. نسخه‌ی نخست، چند خط بیشتر نبود: ورودی را می‌گرفت، یک رکورد می‌ساخت و شناسه‌ی آن را برمی‌گرداند. هیچ‌کس هم نگران نبود، چون کد کوتاه بود و قابلیت درست کار می‌کرد.</p>
<p>چند هفته بعد، یک شرط تازه اضافه شد: اگر کاربر بدهی معوق دارد، برداشت نباید ثبت شود. بعد گفتند حداقل مانده‌ی حساب باید رعایت شود. کمی بعد، پیامک اطلاع‌رسانی اضافه شد. سپس لاگ حسابداری، بررسی وضعیت احراز هویت، محدودیت روزانه، قالب‌بندی پاسخ رابط برنامه‌نویسی کاربردی (API)، و چند خط اعتبارسنجی دیگر هم به همان کنترلر چسبید. کنترلری که قرار بود فقط درگاه ورود درخواست باشد، کم‌کم به جایی تبدیل شد که قانون مالی، ذخیره‌سازی، پیام‌رسانی، لاگ و پاسخ اچ‌تی‌تی‌پی (HTTP) همه در آن گیر کرده بودند.</p>
<p><img decoding="async" loading="lazy" alt="یک کنترلر شلوغ که مسیرهای HTTP، دیتابیس، اعتبارسنجی، قانون مالی و پیام‌رسانی همه در آن گیر کرده‌اند؛ در کنار آن، یک هسته‌ی روشن و جدا برای قواعد کسب‌وکار." src="https://example.com/assets/images/business-rules-should-not-live-in-controller-202d21b1d8590cd928b5484b75c45358.png" width="1731" height="909" class="img_m9Pm"></p>
<p>این اتفاق معمولاً یک‌باره رخ نمی‌دهد. هیچ‌کس صبح به تیم نمی‌گوید «بیایید قانون کسب‌وکار را ته کنترلر دفن کنیم.» هر تغییر کوچک، منطقی و فوری به نظر می‌رسد. یک شرط اینجا، یک فراخوانی سرویس آنجا، یک پیام خطا کمی پایین‌تر. اما نتیجه‌ی نهایی، کدی است که از بیرون فقط یک نقطه‌ی ورود به نظر می‌رسد، ولی درونش تصمیم‌های اصلی سامانه پنهان شده‌اند.</p>
<p>فصل بیستم کتاب <em>معماری تمیز</em> (Clean Architecture) درباره‌ی همین مرزهاست: قواعد کسب‌وکار، موجودیت‌ها و کاربردها. حرف اصلی این فصل این نیست که همه‌ی پروژه‌ها باید تعداد زیادی لایه و پوشه داشته باشند. حرف مهم‌تر این است که باید بفهمیم قلب نرم‌افزار کجاست و اجازه ندهیم زیر جزئیات حاشیه‌ای گم شود.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>قانون کسب‌وکار چیست؟</div><div class="admonitionContent_hiHs"><p>قانون کسب‌وکار تصمیمی است که از خود مسئله می‌آید، نه از ابزار اجرای آن. این قانون حتی اگر فریم‌ورک وب، پایگاه داده، قالب پاسخ یا روش پیام‌رسانی عوض شود، همچنان معنا دارد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="کنترلر-چه-کاری-باید-انجام-دهد">کنترلر چه کاری باید انجام دهد؟<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%DA%A9%D9%86%D8%AA%D8%B1%D9%84%D8%B1-%DA%86%D9%87-%DA%A9%D8%A7%D8%B1%DB%8C-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A7%D9%86%D8%AC%D8%A7%D9%85-%D8%AF%D9%87%D8%AF" class="hash-link" aria-label="لینک مستقیم به کنترلر چه کاری باید انجام دهد؟" title="لینک مستقیم به کنترلر چه کاری باید انجام دهد؟" translate="no">​</a></h2>
<p>کنترلر در معماری وب، نقطه‌ی تماس سامانه با درخواست بیرونی است. درخواست از راه شبکه می‌رسد، داده‌ها در قالبی مشخص وارد می‌شوند، و کنترلر باید آن‌ها را به شکلی قابل‌استفاده برای بخش‌های درونی تبدیل کند. سپس نتیجه را به قالبی برگرداند که مصرف‌کننده‌ی بیرونی بتواند بفهمد.</p>
<p>به بیان ساده، کنترلر باید مترجم مرز بیرونی باشد، نه محل تصمیم‌گیری اصلی. او باید بداند درخواست از کجا آمده، ورودی چگونه خوانده می‌شود، کد وضعیت پاسخ چیست، و خطا چگونه به کاربر بیرونی گزارش می‌شود. اما نباید تصمیم اصلی سامانه را در خود نگه دارد.</p>
<p>برای نمونه، این‌که «اگر کاربر از سقف برداشت روزانه عبور کرده، درخواست رد شود»، یک قانون کسب‌وکار است. اما این‌که این خطا با کد وضعیت ۴۰۰ برگردد یا ۴۲۲، جزئیات ارائه‌ی پاسخ است. این دو نباید در ذهن ما یکی شوند، چون دلیل تغییرشان یکی نیست.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="نمونهی-رایج-کنترلری-که-آرامآرام-چاق-میشود">نمونه‌ی رایج: کنترلری که آرام‌آرام چاق می‌شود<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%D9%86%D9%85%D9%88%D9%86%D9%87%DB%8C-%D8%B1%D8%A7%DB%8C%D8%AC-%DA%A9%D9%86%D8%AA%D8%B1%D9%84%D8%B1%DB%8C-%DA%A9%D9%87-%D8%A2%D8%B1%D8%A7%D9%85%D8%A2%D8%B1%D8%A7%D9%85-%DA%86%D8%A7%D9%82-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به نمونه‌ی رایج: کنترلری که آرام‌آرام چاق می‌شود" title="لینک مستقیم به نمونه‌ی رایج: کنترلری که آرام‌آرام چاق می‌شود" translate="no">​</a></h2>
<p>کدی شبیه نمونه‌ی زیر در بسیاری از پروژه‌ها دیده می‌شود. زبان و نام‌ها مهم نیستند؛ مسئله، جای قرارگرفتن تصمیم‌هاست:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createWithdrawalRequest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> usersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> amount </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Number</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">isVerified</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'کاربر احراز هویت نشده است.'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">amount </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100_000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'مبلغ برداشت کمتر از حد مجاز است.'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> todayTotal </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> withdrawalsRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sumToday</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">todayTotal </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> amount </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000_000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'سقف برداشت روزانه پر شده است.'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">balance </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> amount </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">minimumRequiredBalance</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'مانده‌ی حساب کافی نیست.'</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> withdrawal </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> withdrawalsRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    status</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'pending'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> auditLogger</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'withdrawal_created'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> withdrawal</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> notificationGateway</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">phoneNumber</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'درخواست برداشت شما ثبت شد.'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">201</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> withdrawal</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد شاید در نسخه‌ی نخست قابل‌قبول به نظر برسد، چون همه‌چیز جلوی چشم است. اما همین «همه‌چیز جلوی چشم بودن» به‌تدریج تبدیل به مشکل می‌شود. کنترلر هم ورودی را می‌خواند، هم قانون احراز هویت را می‌سنجد، هم سقف برداشت را حساب می‌کند، هم با پایگاه داده حرف می‌زند، هم پیام می‌فرستد، هم پاسخ بیرونی را می‌سازد.</p>
<p>در چنین وضعی، تغییر یک قانون مالی ممکن است نیازمند تغییر کنترلر وب باشد. آزمون‌کردن قانون برداشت هم وابسته به شبیه‌سازی درخواست وب، پاسخ وب، پایگاه داده و پیام‌رسانی می‌شود. بدتر از آن، اگر همین قانون از مسیر دیگری هم لازم شود، مثلاً از پنل پشتیبانی یا یک پردازش زمان‌بندی‌شده، یا باید آن را کپی کنیم یا کنترلر را از جایی صدا بزنیم که اساساً نباید به وب وابسته باشد.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>کنترلر چاق فقط کد بلند نیست</div><div class="admonitionContent_hiHs"><p>مشکل کنترلر چاق صرفاً تعداد خط‌های آن نیست. مشکل اصلی این است که دلیل‌های متفاوت تغییر در یک نقطه جمع می‌شوند: قرارداد HTTP، قالب ورودی، قانون مالی، ذخیره‌سازی، لاگ، پیام‌رسانی و ساخت پاسخ. وقتی این‌ها با هم قاطی شوند، تغییرهای کوچک هم پرریسک می‌شوند.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="قانون-کسبوکار-کاربرد-کنترلر-و-جزئیات-بیرونی">قانون کسب‌وکار، کاربرد، کنترلر و جزئیات بیرونی<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%D9%82%D8%A7%D9%86%D9%88%D9%86-%DA%A9%D8%B3%D8%A8%D9%88%DA%A9%D8%A7%D8%B1-%DA%A9%D8%A7%D8%B1%D8%A8%D8%B1%D8%AF-%DA%A9%D9%86%D8%AA%D8%B1%D9%84%D8%B1-%D9%88-%D8%AC%D8%B2%D8%A6%DB%8C%D8%A7%D8%AA-%D8%A8%DB%8C%D8%B1%D9%88%D9%86%DB%8C" class="hash-link" aria-label="لینک مستقیم به قانون کسب‌وکار، کاربرد، کنترلر و جزئیات بیرونی" title="لینک مستقیم به قانون کسب‌وکار، کاربرد، کنترلر و جزئیات بیرونی" translate="no">​</a></h2>
<p>برای جداکردن مسئله، باید تفاوت چند مفهوم را روشن کنیم.</p>
<p>قانون کسب‌وکار، قاعده‌ای است که از خود دامنه‌ی مسئله می‌آید. مثلاً «برداشت نباید مانده‌ی حساب را از حداقل مجاز کمتر کند» یا «کاربر احراز هویت‌نشده اجازه‌ی برداشت ندارد». این قانون‌ها به وجود وب، پایگاه داده یا پیامک وابسته نیستند. اگر فردا به جای درخواست HTTP، همین عملیات از صف پیام یا ابزار داخلی اجرا شود، قانون همچنان معتبر است.</p>
<p>موجودیت، مفهومی پایدارتر از دامنه است. در زبان کتاب، موجودیت‌ها حامل قواعدی هستند که بیشترین عمر و کمترین وابستگی به جزئیات بیرونی را دارند. برای نمونه، حساب کاربر می‌تواند بداند بعد از برداشت، مانده‌اش نباید از حداقل مجاز پایین‌تر بیاید. این دانستن نباید وابسته به کنترلر باشد.</p>
<p>کاربرد (Use Case) یک سناریوی مشخص از کار سامانه است. «ثبت درخواست برداشت» یک کاربرد است. کاربرد معمولاً چند موجودیت و چند درگاه بیرونی را هماهنگ می‌کند: کاربر را می‌خواند، قانون را اجرا می‌کند، درخواست را ذخیره می‌کند، و شاید رخدادی برای اطلاع‌رسانی منتشر کند. کاربرد نباید درگیر این باشد که درخواست از چه مسیر وبی آمده یا پاسخ نهایی چه قالبی دارد.</p>
<p>کنترلر، مترجم ورودی و خروجی بیرونی است. داده‌ی درخواست را می‌گیرد، آن را به ورودی کاربرد تبدیل می‌کند، کاربرد را صدا می‌زند، و نتیجه را به پاسخ مناسب تبدیل می‌کند. اگر کنترلر مجبور است درباره‌ی سقف مالی، مانده‌ی حساب یا سیاست احراز هویت تصمیم بگیرد، احتمالاً مرزها جابه‌جا شده‌اند.</p>
<p>جزئیات بیرونی هم ابزارها و قراردادهایی هستند که احتمال تغییرشان بالاست: پایگاه داده، سرویس پیامک، قالب پاسخ، کتابخانه‌ی وب، سامانه‌ی لاگ، صف پیام و مانند آن. این‌ها مهم‌اند، اما نباید صاحب منطق اصلی شوند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="یک-بازچینی-کوچک">یک بازچینی کوچک<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%DB%8C%DA%A9-%D8%A8%D8%A7%D8%B2%DA%86%DB%8C%D9%86%DB%8C-%DA%A9%D9%88%DA%86%DA%A9" class="hash-link" aria-label="لینک مستقیم به یک بازچینی کوچک" title="لینک مستقیم به یک بازچینی کوچک" translate="no">​</a></h2>
<p>همان مثال برداشت را می‌توان به شکلی چید که تصمیم اصلی در جای مناسب‌تری قرار بگیرد. لازم نیست از ابتدا ساختار پیچیده‌ای بسازیم. حتی یک جداسازی ساده می‌تواند تفاوت زیادی ایجاد کند:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">withdrawal/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  create-withdrawal-request.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  withdrawal-policy.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  withdrawal-repository.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">http/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  withdrawal-controller.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">notification/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  notification-gateway.ts</span><br></span></code></pre></div></div>
<p>در این چینش، کنترلر فقط ورودی را آماده می‌کند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createWithdrawalRequestController</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> createWithdrawalRequest</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">execute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Number</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">body</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">201</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>و کاربرد، سناریوی اصلی را پیش می‌برد:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createWithdrawalRequest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> user </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> usersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">findById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> todayTotal </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> withdrawalsRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sumToday</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  withdrawalPolicy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ensureCanCreate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    user</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    todayTotal</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> withdrawal </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> withdrawalsRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">createPending</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">amount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> events</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">publish</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'withdrawal_created'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> withdrawalId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> withdrawal</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> withdrawal</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>در این نمونه، هنوز پایگاه داده و رخداد بیرونی وجود دارند، اما تصمیم‌های اصلی از کنترلر بیرون آمده‌اند. کنترلر می‌تواند عوض شود، مثلاً از یک مسیر وب به یک فرمان داخلی تبدیل شود، بدون این‌که قانون برداشت دوباره نوشته شود.</p>
<p>هدف این نیست که هر پروژه را به چندین لایه‌ی سنگین تقسیم کنیم. هدف این است که قانون اصلی در جایی قرار بگیرد که با زبان مسئله خوانده شود، مستقل‌تر آزمون شود، و زیر جزئیات بیرونی پنهان نماند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="سریالایزر-هم-جای-قانون-اصلی-نیست">سریالایزر هم جای قانون اصلی نیست<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%D8%B3%D8%B1%DB%8C%D8%A7%D9%84%D8%A7%DB%8C%D8%B2%D8%B1-%D9%87%D9%85-%D8%AC%D8%A7%DB%8C-%D9%82%D8%A7%D9%86%D9%88%D9%86-%D8%A7%D8%B5%D9%84%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به سریالایزر هم جای قانون اصلی نیست" title="لینک مستقیم به سریالایزر هم جای قانون اصلی نیست" translate="no">​</a></h2>
<p>گاهی منطق از کنترلر بیرون می‌آید، اما در جای دیگری گم می‌شود: سریالایزر، فرم، مدل پایگاه داده یا حتی تابع کمکی کنار مسیر وب. این جابه‌جایی اگر فقط اسم مسئله را عوض کند، کمک زیادی نمی‌کند. اگر قانون مالی هنوز به قالب ورودی وب، نام فیلدهای درخواست یا خطای نمایشی وابسته باشد، فقط از یک گوشه‌ی شلوغ به گوشه‌ی شلوغ دیگری منتقل شده است.</p>
<p>سریالایزر می‌تواند بررسی کند که یک فیلد عدد است، خالی نیست، یا قالب تاریخ درست دارد. اما این‌که «این کاربر با این وضعیت مالی اجازه‌ی برداشت دارد یا نه» دیگر فقط اعتبارسنجی قالب داده نیست. این تصمیم بخشی از رفتار اصلی سامانه است و باید جایی قرار بگیرد که از مسیرهای مختلف قابل‌استفاده باشد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-این-جداسازی-به-درد-محصول-هم-میخورد">چرا این جداسازی به درد محصول هم می‌خورد؟<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%DA%86%D8%B1%D8%A7-%D8%A7%DB%8C%D9%86-%D8%AC%D8%AF%D8%A7%D8%B3%D8%A7%D8%B2%DB%8C-%D8%A8%D9%87-%D8%AF%D8%B1%D8%AF-%D9%85%D8%AD%D8%B5%D9%88%D9%84-%D9%87%D9%85-%D9%85%DB%8C%D8%AE%D9%88%D8%B1%D8%AF" class="hash-link" aria-label="لینک مستقیم به چرا این جداسازی به درد محصول هم می‌خورد؟" title="لینک مستقیم به چرا این جداسازی به درد محصول هم می‌خورد؟" translate="no">​</a></h2>
<p>گاهی جداسازی قانون کسب‌وکار از کنترلر، شبیه دغدغه‌ای صرفاً فنی دیده می‌شود. اما اثر آن کاملاً محصولی است. محصولی که رشد می‌کند، مسیرهای تازه پیدا می‌کند: وب، برنامه‌ی همراه، پنل پشتیبانی، پردازش‌های پس‌زمینه، اتصال‌های بیرونی و ابزارهای عملیاتی. اگر قانون اصلی به یک کنترلر خاص چسبیده باشد، هر مسیر تازه یا قانون را کپی می‌کند، یا برای استفاده از آن به مسیر نامناسبی وابسته می‌شود.</p>
<p>در مقابل، وقتی قانون در هسته‌ی روشن‌تری قرار دارد، اضافه‌کردن مسیرهای تازه ساده‌تر می‌شود. کنترلر وب، فرمان داخلی یا پردازش پس‌زمینه، همگی می‌توانند همان کاربرد را صدا بزنند. این یعنی رفتار یکسان‌تر، آزمون‌پذیری بهتر و احتمال کمتر برای اختلاف‌های عجیب میان مسیرهای مختلف محصول.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>نشانه‌های عملی جداسازی</div><div class="admonitionContent_hiHs"><p>اگر برای آزمون یک قانون کسب‌وکار مجبور می‌شوی درخواست HTTP بسازی، اگر یک شرط مالی در چند کنترلر تکرار شده، اگر تغییر متن پاسخ باعث دست‌زدن به قانون اصلی می‌شود، یا اگر مسیر پس‌زمینه نمی‌تواند بدون کنترلر همان رفتار را اجرا کند، احتمالاً زمان جداسازی رسیده است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای بررسی یک کنترلر شلوغ، اول از خودت بپرس هر خط کد به کدام دسته تعلق دارد: خواندن ورودی، تبدیل قالب، اجرای قانون کسب‌وکار، هماهنگی یک کاربرد، ذخیره‌سازی، فراخوانی سرویس بیرونی، لاگ، یا ساخت پاسخ. همین دسته‌بندی ساده معمولاً نشان می‌دهد کنترلر واقعاً چه مقدار مسئولیت اضافه به دوش می‌کشد.</p>
<p>چیزهایی را که فقط به قرارداد بیرونی مربوط‌اند، بی‌دلیل وارد هسته‌ی سامانه نکن. کد وضعیت HTTP، نام فیلدهای درخواست، شکل پاسخ و جزئیات فریم‌ورک باید نزدیک مرز بیرونی بمانند. از آن طرف، قانون‌های مالی، محدودیت‌های دامنه، تصمیم‌های وضعیت و رفتارهای اصلی را صرفاً برای کوتاه‌شدن مسیر، داخل کنترلر یا سریالایزر پنهان نکن.</p>
<p>بی‌دلیل هم همه‌چیز را جابه‌جا نکن. اگر کنترلری کوچک است، فقط ورودی را تبدیل می‌کند و هیچ تصمیم دامنه‌ای در آن نیست، شاید نیازی به تغییر نداشته باشد. جداسازی زمانی ارزش دارد که دلیل تغییرها با هم قاطی شده‌اند یا یک رفتار مهم باید از چند مسیر قابل‌استفاده باشد.</p>
<p>برای اعتبارسنجی، پس از جداسازی دست‌کم سه چیز را بررسی کن: آزمون‌های مربوط به رفتار اصلی، آزمون یا سناریوی مسیر بیرونی، و ساخت پروژه. در این مخزن‌ها بسته به ابزارها، فرمان‌هایی شبیه این نقطه‌ی سرآغاز خوبی هستند:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">pnpm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm typecheck</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm build</span><br></span></code></pre></div></div>
<p>اگر آزمون خودکار کافی نداری، پیش از جابه‌جایی منطق، یک سناریوی دستی دقیق بنویس: ورودی چیست، وضعیت داده‌ها چیست، خروجی مورد انتظار چیست، و کدام اثر جانبی باید رخ دهد یا رخ ندهد. جداسازی بدون سنجش، خیلی راحت می‌تواند فقط شکل کد را بهتر کند، نه رفتار آن را.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/business-rules-should-not-live-in-controller#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>قانون کسب‌وکار نباید ته کنترلر گم شود. کنترلر برای ترجمه‌ی مرز بیرونی است، نه برای نگه‌داری تصمیم‌های اصلی سامانه. وقتی منطق دامنه، اعتبارسنجی قالب، ذخیره‌سازی، پیام‌رسانی، لاگ و پاسخ بیرونی در یک نقطه جمع می‌شوند، نرم‌افزار شاید در لحظه کار کند، اما تغییر آن سخت‌تر و پرخطرتر می‌شود.</p>
<p>فصل بیستم معماری تمیز به ما یادآوری می‌کند که قلب سامانه را باید از جزئیات بیرونی جدا نگه داریم. موجودیت‌ها و کاربردها قرار نیست تزئین معماری باشند؛ قرار است جایی باشند که رفتار اصلی با زبان مسئله بیان شود. کنترلر خوب، کنترلری نیست که همه‌چیز را خودش انجام دهد. کنترلر خوب، مرز را می‌شناسد، درخواست را ترجمه می‌کند، کاربرد مناسب را صدا می‌زند، و کنار می‌رود.</p>
<p>هر بار که خواستی یک شرط تازه به کنترلر اضافه کنی، کمی مکث کن. از خودت بپرس: این شرط واقعاً مربوط به مسیر وب است، یا یکی از قانون‌های اصلی سامانه است که فقط اتفاقاً امروز از راه این کنترلر اجرا می‌شود؟ پاسخ همین پرسش، اغلب جای درست کد را روشن می‌کند.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="معماری تمیز" term="معماری تمیز"/>
        <category label="قواعد کسب‌وکار" term="قواعد کسب‌وکار"/>
        <category label="کنترلر" term="کنترلر"/>
        <category label="طراحی نرم‌افزار" term="طراحی نرم‌افزار"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[نرم‌افزاری که فقط کار می‌کند، هنوز لزوماً خوب نیست]]></title>
        <id>https://example.com/blog/software-that-only-works</id>
        <link href="https://example.com/blog/software-that-only-works"/>
        <updated>2026-03-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[روایتی درباره‌ی اینکه چرا کار کردن امروز نرم‌افزار برای خوب بودن آن کافی نیست و ارزش پنهان معماری در توان تغییرپذیری آینده آشکار می‌شود.]]></summary>
        <content type="html"><![CDATA[<p>تیم با سرعت خوبی پیش می‌رفت. هر هفته چند قابلیت تازه به محصول اضافه می‌شد، مدیر محصول از روند تحویل راضی بود، مشتری‌ها تغییرها را می‌دیدند، و از بیرون همه‌چیز نشانه‌ی یک نرم‌افزار موفق را داشت. نخستین نسخه‌ی پرداخت، گزارش‌های مدیریتی، اعلان‌ها و چند قانون تخفیف، یکی پس از دیگری آماده شده بودند. هر بار هم که کسی می‌پرسید «وضعیت فنی چطور است؟»، پاسخ کوتاه و مطمئن بود: «کار می‌کند.»</p>
<p>اما چند ماه بعد، همین جمله دیگر آرامش‌بخش نبود. یک تغییر کوچک در قانون تخفیف، چند بخش نامرتبط را درگیر می‌کرد. افزودن یک نوع تازه از اعلان، نیازمند تغییر در چند فایل و چند مسیر اجرایی بود. آزمون‌ها یا بیش از حد می‌شکستند، یا چیزی را که باید، نمی‌سنجیدند. نرم‌افزار از بیرون روشن بود، اما درون آن چرخ‌دنده‌هایی فشرده و سخت‌دسترس قرار داشت.</p>
<p><img decoding="async" loading="lazy" alt="یک ماشین نرم‌افزاری که از بیرون روشن و سالم دیده می‌شود، اما درون آن چرخ‌دنده‌هایی فشرده، پیچیده و سخت‌دسترس قرار دارد؛ تصویری مفهومی، مینیمال و فنی." src="https://example.com/assets/images/software-that-only-works-70f4286a09942350864174efa01bb959.png" width="1536" height="1024" class="img_m9Pm"></p>
<p>این موقعیت برای بیشتر تیم‌های نرم‌افزاری آشناست. مسئله معمولاً این نیست که تیم بی‌تجربه است یا کسی به کیفیت اهمیت نمی‌دهد. مسئله این است که «کار کردن» نرم‌افزار، خیلی زود با «خوب بودن» آن اشتباه گرفته می‌شود. نرم‌افزاری که امروز نیاز کاربر را پاسخ می‌دهد، ممکن است هم‌زمان آینده‌ی تیم را گران‌تر، کندتر و شکننده‌تر کند.</p>
<p>فصل‌های نخست کتاب <em>معماری تمیز</em> (Clean Architecture) از همین شکاف آغاز می‌کنند: نرم‌افزار فقط برای اجرای رفتار امروز ساخته نمی‌شود؛ نرم‌افزار باید بتواند در برابر تغییرهای فردا هم دوام بیاورد. این حرف ساده به نظر می‌رسد، اما در عمل یکی از دشوارترین بحث‌های میان مهندسان، مدیران محصول و ذی‌نفعان کسب‌وکار است.</p>
<div class="theme-admonition theme-admonition-note admonition_xGDO alert alert--secondary"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>حرف اصلی</div><div class="admonitionContent_hiHs"><p>نرم‌افزار دو ارزش دارد: ارزشی که امروز از راه رفتار درست به کاربر می‌دهد، و ارزشی که در آینده از راه تغییرپذیری به تیم و کسب‌وکار برمی‌گرداند. معماری، بیش از هر چیز، نگهبان ارزش دوم است.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="دو-ارزش-نرمافزار">دو ارزش نرم‌افزار<a href="https://example.com/blog/software-that-only-works#%D8%AF%D9%88-%D8%A7%D8%B1%D8%B2%D8%B4-%D9%86%D8%B1%D9%85%D8%A7%D9%81%D8%B2%D8%A7%D8%B1" class="hash-link" aria-label="لینک مستقیم به دو ارزش نرم‌افزار" title="لینک مستقیم به دو ارزش نرم‌افزار" translate="no">​</a></h2>
<p>وقتی می‌گوییم نرم‌افزار «کار می‌کند»، معمولاً درباره‌ی ارزش رفتاری آن حرف می‌زنیم. یعنی کاربر می‌تواند سفارش ثبت کند، گزارش بگیرد، پرداخت انجام دهد، پیام دریافت کند، یا هر رفتار دیگری را که از سامانه انتظار دارد، تجربه کند. این ارزش واقعی و ضروری است. بدون آن، نرم‌افزار فایده‌ای ندارد.</p>
<p>اما نرم‌افزار یک ارزش دیگر هم دارد: توان تغییرپذیری. یعنی وقتی نیاز جدیدی مطرح می‌شود، تیم بتواند با هزینه‌ای معقول، با خطر قابل‌قبول و با اطمینان کافی، نرم‌افزار را تغییر دهد. این ارزش در روز نخست کمتر دیده می‌شود، چون هنوز تغییرهای زیاد از راه نرسیده‌اند. ارزش آن زمانی آشکار می‌شود که محصول زنده می‌ماند، رشد می‌کند، و خواسته‌های تازه مطرح می‌شوند.</p>
<p>رفتار فعلی نرم‌افزار را می‌توان با اجرای برنامه، دیدن صفحه‌ها یا بررسی چند نمودار سنجید. اما تغییرپذیری معمولاً در نبود تغییر دیده نمی‌شود. معماری نرم‌افزار دقیقاً در همین نقطه معنا پیدا می‌کند: مجموعه‌ای از تصمیم‌ها که تعیین می‌کند تغییرهای آینده چقدر محلی، قابل‌فهم، کم‌خطر و قابل‌آزمون باشند.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="چرا-ارزش-نخست-بهتر-دیده-میشود">چرا ارزش نخست بهتر دیده می‌شود؟<a href="https://example.com/blog/software-that-only-works#%DA%86%D8%B1%D8%A7-%D8%A7%D8%B1%D8%B2%D8%B4-%D9%86%D8%AE%D8%B3%D8%AA-%D8%A8%D9%87%D8%AA%D8%B1-%D8%AF%DB%8C%D8%AF%D9%87-%D9%85%DB%8C%D8%B4%D9%88%D8%AF" class="hash-link" aria-label="لینک مستقیم به چرا ارزش نخست بهتر دیده می‌شود؟" title="لینک مستقیم به چرا ارزش نخست بهتر دیده می‌شود؟" translate="no">​</a></h2>
<p>مدیر محصول معمولاً با ارزش رفتاری نرم‌افزار سروکار مستقیم دارد. او با نیاز کاربر، زمان عرضه، بازخورد بازار، نرخ تبدیل، درآمد و اولویت‌های کسب‌وکار درگیر است. قابلیتی که امروز آماده می‌شود، قابل‌دیدن و قابل‌اندازه‌گیری است. می‌توان آن را در جلسه نشان داد، درباره‌اش بازخورد گرفت و اثرش را روی محصول سنجید.</p>
<p>در مقابل، تغییرپذیری آینده اغلب به شکل یک «هزینه‌ی نداده» یا «خطر هنوز رخ‌نداده» دیده می‌شود. وقتی مهندس می‌گوید «اگر این بخش را همین‌طور ادامه بدهیم، تغییرهای بعدی سخت می‌شود»، ممکن است حرفش شبیه نگرانی انتزاعی به نظر برسد. مدیر محصول حق دارد بپرسد: «سخت می‌شود یعنی چه؟ چقدر سخت؟ الان چه چیزی را از دست می‌دهیم؟»</p>
<p>مشکل از جایی آغاز می‌شود که مهندس نتواند ارزش تغییرپذیری را به زبان روشن توضیح دهد. اگر بحث معماری فقط با جمله‌هایی مثل «تمیز نیست»، «بدهی فنی داریم» یا «این طراحی درست نیست» پیش برود، احتمالاً به نتیجه‌ی خوبی نمی‌رسد. بهتر است اثر تصمیم فنی به هزینه‌ی تغییر وصل شود:</p>
<ul>
<li class="">اگر قانون تخفیف را داخل مسیر ثبت سفارش نگه داریم، هر تغییر در تخفیف نیازمند تغییر در ثبت سفارش هم می‌شود.</li>
<li class="">اگر ارسال اعلان، محاسبه‌ی قیمت و ذخیره‌ی سفارش را در یک بخش واحد ترکیب کنیم، آزمون‌کردن هرکدام به تنهایی سخت می‌شود.</li>
<li class="">اگر وابستگی به سرویس بیرونی را مستقیم در منطق اصلی پخش کنیم، جایگزینی آن سرویس یا ساختن حالت آزمایشی پرهزینه می‌شود.</li>
</ul>
<p>این‌ها دیگر حرف‌های مبهم درباره‌ی «کد تمیز» نیستند. این‌ها توضیح می‌دهند چرا یک تصمیم امروز، مسیر تغییر فردا را تنگ می‌کند.</p>
<div class="theme-admonition theme-admonition-warning admonition_xGDO alert alert--warning"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>سوءبرداشت رایج</div><div class="admonitionContent_hiHs"><p>دفاع از معماری به معنای کندکردن تیم، طلاکاری فنی یا ساختن لایه‌های بی‌مصرف نیست. معماری خوب قرار نیست امروز را قربانی آینده کند؛ قرار است کاری کند که تحویل امروز، راه تغییر فردا را نبندد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="وقتی-فقط-رفتار-را-میبینیم-چه-چیزی-پنهان-میماند">وقتی فقط رفتار را می‌بینیم، چه چیزی پنهان می‌ماند؟<a href="https://example.com/blog/software-that-only-works#%D9%88%D9%82%D8%AA%DB%8C-%D9%81%D9%82%D8%B7-%D8%B1%D9%81%D8%AA%D8%A7%D8%B1-%D8%B1%D8%A7-%D9%85%DB%8C%D8%A8%DB%8C%D9%86%DB%8C%D9%85-%DA%86%D9%87-%DA%86%DB%8C%D8%B2%DB%8C-%D9%BE%D9%86%D9%87%D8%A7%D9%86-%D9%85%DB%8C%D9%85%D8%A7%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به وقتی فقط رفتار را می‌بینیم، چه چیزی پنهان می‌ماند؟" title="لینک مستقیم به وقتی فقط رفتار را می‌بینیم، چه چیزی پنهان می‌ماند؟" translate="no">​</a></h2>
<p>فرض کنید در سامانه‌ای برای فروش، سه کار باید انجام شود: ثبت سفارش، محاسبه‌ی تخفیف و ارسال اعلان. در نسخه‌ی نخست، ساده‌ترین راه این است که همه‌چیز در یک تابع یا یک سرویس بزرگ پیاده‌سازی شود:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">placeOrder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> discount </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">totalPrice </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_000_000</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.1</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> finalPrice </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">totalPrice </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> discount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ordersRepository</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    items</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">items</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    finalPrice</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> notificationClient</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">userId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'سفارش شما ثبت شد.'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> order</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>این کد ممکن است کاملاً کار کند. اما اگر کمی جلوتر برویم، پرسش‌های سخت‌تر پیدا می‌شوند: اگر قانون تخفیف بر اساس گروه کاربر تغییر کند چه؟ اگر اعلان پیامکی با اعلان درون‌برنامه‌ای متفاوت شود چه؟ اگر ثبت سفارش باید بدون وابستگی مستقیم به سرویس اعلان آزمون شود چه؟</p>
<p>مشکل این نمونه، تعداد خط‌ها نیست. مشکل این است که چند دلیل متفاوت برای تغییر، در یک نقطه جمع شده‌اند. تغییر قانون تخفیف، تغییر شیوه‌ی ذخیره‌سازی سفارش و تغییر سازوکار اعلان، همگی همین بخش را درگیر می‌کنند.</p>
<p>یک مرزبندی ساده‌تر می‌تواند چنین باشد:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">src/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  order/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    order-service.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    order-repository.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  pricing/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    discount-policy.ts</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  notification/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    notification-gateway.ts</span><br></span></code></pre></div></div>
<p>در این ساختار، هدف این نیست که برای هر چیز کوچکی یک پوشه بسازیم. هدف این است که دلیل‌های تغییر را از هم جدا کنیم. قانون تخفیف در جایی قرار می‌گیرد که زبان و آزمون خودش را دارد. اعلان پشت یک درگاه قرار می‌گیرد تا منطق سفارش مجبور نباشد جزئیات سرویس بیرونی را بشناسد.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="مهندس-باید-از-چه-چیزی-دفاع-کند">مهندس باید از چه چیزی دفاع کند؟<a href="https://example.com/blog/software-that-only-works#%D9%85%D9%87%D9%86%D8%AF%D8%B3-%D8%A8%D8%A7%DB%8C%D8%AF-%D8%A7%D8%B2-%DA%86%D9%87-%DA%86%DB%8C%D8%B2%DB%8C-%D8%AF%D9%81%D8%A7%D8%B9-%DA%A9%D9%86%D8%AF" class="hash-link" aria-label="لینک مستقیم به مهندس باید از چه چیزی دفاع کند؟" title="لینک مستقیم به مهندس باید از چه چیزی دفاع کند؟" translate="no">​</a></h2>
<p>مهندس نباید از سلیقه‌ی شخصی خود دفاع کند. نباید هر ترجیح فنی را به نام معماری جا بزند. دفاع درست از معماری، دفاع از توان تغییرپذیری سامانه است.</p>
<p>این دفاع وقتی جدی می‌شود که به چند پرسش پاسخ بدهد:</p>
<ul>
<li class="">این تصمیم، کدام تغییر آینده را سخت‌تر یا آسان‌تر می‌کند؟</li>
<li class="">اگر نیاز محصول عوض شود، چند نقطه باید تغییر کند؟</li>
<li class="">آیا می‌توان این رفتار را بدون راه‌اندازی همه‌ی وابستگی‌ها آزمود؟</li>
<li class="">آیا بخش‌های پرنوسان از بخش‌های پایدار جدا شده‌اند؟</li>
<li class="">آیا نام‌گذاری و مرزبندی کد، زبان مسئله را نشان می‌دهد یا فقط جزئیات فنی را؟</li>
</ul>
<p>وقتی مهندس چنین پرسش‌هایی را مطرح می‌کند، بحث از «من این مدل را دوست دارم» به «این تصمیم هزینه‌ی تغییر را بالا یا پایین می‌برد» منتقل می‌شود.</p>
<div class="theme-admonition theme-admonition-tip admonition_xGDO alert alert--success"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>پرسش‌های عملی هنگام طراحی</div><div class="admonitionContent_hiHs"><p>پیش از اضافه‌کردن یک قابلیت، از خودت بپرس: این قابلیت به احتمال زیاد از کجا تغییر می‌کند؟ کدام بخش آن قانون کسب‌وکار است و کدام بخش فقط جزئیات اتصال به ابزارهاست؟ اگر فردا بخواهم همین رفتار را با یک ورودی، خروجی یا وابستگی دیگر اجرا کنم، کدام قسمت‌ها باید دست‌نخورده بمانند؟</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="معماری-خوب-همیشه-از-روز-اول-بزرگ-نیست">معماری خوب همیشه از روز اول بزرگ نیست<a href="https://example.com/blog/software-that-only-works#%D9%85%D8%B9%D9%85%D8%A7%D8%B1%DB%8C-%D8%AE%D9%88%D8%A8-%D9%87%D9%85%DB%8C%D8%B4%D9%87-%D8%A7%D8%B2-%D8%B1%D9%88%D8%B2-%D8%A7%D9%88%D9%84-%D8%A8%D8%B2%D8%B1%DA%AF-%D9%86%DB%8C%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به معماری خوب همیشه از روز اول بزرگ نیست" title="لینک مستقیم به معماری خوب همیشه از روز اول بزرگ نیست" translate="no">​</a></h2>
<p>یکی از سوءتفاهم‌های رایج درباره‌ی معماری این است که فکر کنیم از همان ابتدا باید سامانه‌ای بزرگ، چندلایه و پر از انتزاع بسازیم. چنین کاری معمولاً نتیجه‌ی خوبی ندارد. اگر مسئله هنوز کوچک و نامطمئن است، طراحی بیش از حد می‌تواند خودش به مانع تغییر تبدیل شود.</p>
<p>معماری تمیز بیشتر از آن‌که درباره‌ی زیادکردن لایه‌ها باشد، درباره‌ی نگه‌داشتن گزینه‌هاست. یعنی تصمیم‌های پرهزینه را بی‌دلیل زود قطعی نکنیم. یعنی جزئیاتی را که احتمال تغییرشان بالاست، در مرکز منطق اصلی پخش نکنیم.</p>
<p>برای نمونه، اگر هنوز نمی‌دانیم شیوه‌ی ارسال اعلان در محصول چه خواهد شد، بهتر است منطق اصلی سفارش به جای وابستگی مستقیم به یک کتابخانه یا سرویس خاص، با یک درگاه ساده حرف بزند:</p>
<div class="language-ts codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-ts codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">NotificationGateway</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">sendOrderCreatedMessage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">userId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> orderId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">Promise</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token keyword" style="color:#00009f">void</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>این انتزاع کوچک قرار نیست همه‌ی آینده را پیش‌بینی کند. فقط یک مرز ساده می‌گذارد: منطق سفارش لازم نیست بداند پیام با چه ابزار و از چه مسیری ارسال می‌شود.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="بعداً-درستش-میکنیم-چرا-خطرناک-است">«بعداً درستش می‌کنیم» چرا خطرناک است؟<a href="https://example.com/blog/software-that-only-works#%D8%A8%D8%B9%D8%AF%D8%A7%D9%8B-%D8%AF%D8%B1%D8%B3%D8%AA%D8%B4-%D9%85%DB%8C%DA%A9%D9%86%DB%8C%D9%85-%DA%86%D8%B1%D8%A7-%D8%AE%D8%B7%D8%B1%D9%86%D8%A7%DA%A9-%D8%A7%D8%B3%D8%AA" class="hash-link" aria-label="لینک مستقیم به «بعداً درستش می‌کنیم» چرا خطرناک است؟" title="لینک مستقیم به «بعداً درستش می‌کنیم» چرا خطرناک است؟" translate="no">​</a></h2>
<p>گاهی تیم آگاهانه تصمیم می‌گیرد سریع‌تر پیش برود و بعضی مرزها را بعداً اصلاح کند. این تصمیم همیشه غلط نیست. در محصول‌های نوپا، گاهی یادگیری سریع مهم‌تر از طراحی دقیق است. اما جمله‌ی «بعداً درستش می‌کنیم» فقط وقتی قابل‌قبول است که تیم بداند «بعداً» یعنی چه، چه نشانه‌ای زمان اصلاح را مشخص می‌کند، و هزینه‌ی عقب‌انداختن تصمیم چقدر است.</p>
<p>یک راه بهتر این است که تصمیم‌های موقت را آشکار کنیم:</p>
<div class="language-text codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-text codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">تصمیم موقت:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">در نسخه‌ی نخست، قانون تخفیف داخل سرویس سفارش پیاده‌سازی شد.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">دلیل:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">فعلاً فقط یک قانون ساده داریم و هنوز مشخص نیست مدل تخفیف در محصول چگونه رشد می‌کند.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">نشانه‌ی بازنگری:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">اگر بیش از دو قانون تخفیف اضافه شد، یا تخفیف به گروه کاربر وابسته شد، منطق تخفیف به بخش جداگانه منتقل شود.</span><br></span></code></pre></div></div>
<p>چنین یادداشتی جلوی بدهی فنی را به‌تنهایی نمی‌گیرد، اما آن را از یک حس مبهم به یک تصمیم قابل‌پیگیری تبدیل می‌کند.</p>
<div class="theme-admonition theme-admonition-info admonition_xGDO alert alert--info"><div class="admonitionHeading_WNFr"><span class="admonitionIcon_FtpR"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>نکته‌ی مهم</div><div class="admonitionContent_hiHs"><p>همه‌ی تغییرهای آینده قابل‌پیش‌بینی نیستند. هدف معماری این نیست که آینده را دقیق حدس بزند؛ هدف این است که نرم‌افزار در برابر تغییرهای معقول، بی‌دلیل شکننده نباشد.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="پیشنهاد-عملی">پیشنهاد عملی<a href="https://example.com/blog/software-that-only-works#%D9%BE%DB%8C%D8%B4%D9%86%D9%87%D8%A7%D8%AF-%D8%B9%D9%85%D9%84%DB%8C" class="hash-link" aria-label="لینک مستقیم به پیشنهاد عملی" title="لینک مستقیم به پیشنهاد عملی" translate="no">​</a></h2>
<p>برای هر تغییر مهم، پیش از پیاده‌سازی چند چیز را بررسی کن. نخست، ببین این تغییر فقط یک رفتار تازه اضافه می‌کند یا مرزهای فعلی سامانه را هم تحت فشار می‌گذارد. دوم، دلیل‌های تغییر را پیدا کن: قانون کسب‌وکار، شیوه‌ی ذخیره‌سازی، ارتباط با سرویس بیرونی، نمایش به کاربر و زمان‌بندی اجرا معمولاً دلیل‌های متفاوتی برای تغییر دارند. سوم، بررسی کن آیا آزمون‌کردن رفتار اصلی بدون وابستگی‌های بیرونی ممکن است یا نه.</p>
<p>در مقابل، چند چیز را بی‌دلیل تغییر نده. صرفاً چون نام یک الگو را می‌دانی، ساختار پروژه را عوض نکن. فقط برای «تمیزتر شدن» کد، مرزهای تازه نساز، مگر این‌که دلیل تغییر مشخصی پشت آن باشد. اگر بخشی از سامانه پایدار است، کم‌تغییر است و فهم آن سخت نیست، شاید دست‌زدن به آن ارزش چندانی نداشته باشد.</p>
<p>برای اعتبارسنجی هم به یک نشانه اکتفا نکن. اگر پروژه آزمون خودکار دارد، آزمون‌ها را اجرا کن. اگر بررسی نوع یا ساخت وجود دارد، آن را هم اجرا کن. در یک پروژه‌ی جاوااسکریپتی یا تایپ‌اسکریپتی، بسته به اسکریپت‌های موجود، چیزی شبیه این می‌تواند نقطه‌ی سرآغاز باشد:</p>
<div class="language-bash codeBlockContainer_G9Q3 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_KPzx"><pre tabindex="0" class="prism-code language-bash codeBlock_X5zZ thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_IHTV"><span class="token-line" style="color:#393A34"><span class="token plain">pnpm test</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm typecheck</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">pnpm build</span><br></span></code></pre></div></div>
<p>اگر چنین فرمان‌هایی در پروژه وجود ندارد، پیش از تغییرهای معماری، دست‌کم یک مسیر رفتاری مهم را با آزمون یا سناریوی دستی دقیق ثبت کن. بازآرایی بدون امکان سنجش، بیشتر حرکت در تاریکی است تا بهبود مهندسی.</p>
<h2 class="anchor anchorTargetStickyNavbar_UCMm" id="جمعبندی">جمع‌بندی<a href="https://example.com/blog/software-that-only-works#%D8%AC%D9%85%D8%B9%D8%A8%D9%86%D8%AF%DB%8C" class="hash-link" aria-label="لینک مستقیم به جمع‌بندی" title="لینک مستقیم به جمع‌بندی" translate="no">​</a></h2>
<p>نرم‌افزاری که فقط کار می‌کند، هنوز لزوماً خوب نیست. کار کردن امروز، شرط لازم است؛ اما کافی نیست. اگر هر تغییر کوچک نیازمند درگیرکردن چند بخش نامرتبط باشد، اگر آزمون‌ها به جای رفتار به جزئیات چسبیده باشند، اگر وابستگی‌های بیرونی در مرکز منطق اصلی پخش شده باشند، نرم‌افزار شاید از بیرون سالم دیده شود، اما از درون برای تغییر آماده نیست.</p>
<p>ارزش پنهان معماری همین‌جاست. معماری خوب کمک می‌کند رفتار امروز حفظ شود، بدون آن‌که تغییر فردا به فرسایش روزمره تبدیل شود. مدیر محصول ارزش رفتاری را بهتر می‌بیند، چون مستقیم در محصول دیده می‌شود. مهندس باید ارزش تغییرپذیری را توضیح‌پذیر، قابل‌سنجش و مرتبط با هزینه‌ی آینده کند.</p>
<p>پرسش اصلی این نیست که «آیا نرم‌افزار کار می‌کند؟» پرسش کامل‌تر این است: «آیا نرم‌افزاری که امروز کار می‌کند، فردا هم با هزینه‌ای معقول قابل‌تغییر خواهد بود؟» پاسخ به این پرسش، همان جایی است که معماری از یک بحث نظری به یک ضرورت روزمره‌ی مهندسی تبدیل می‌شود.</p>]]></content>
        <author>
            <name>مهدی مالوردی</name>
            <uri>https://github.com/mahdimalverdi</uri>
        </author>
        <category label="معماری نرم‌افزار" term="معماری نرم‌افزار"/>
        <category label="معماری تمیز" term="معماری تمیز"/>
        <category label="تغییرپذیری" term="تغییرپذیری"/>
        <category label="طراحی نرم‌افزار" term="طراحی نرم‌افزار"/>
    </entry>
</feed>